Skip to content

Commit

Permalink
Merge pull request #65 from anggoran/dev
Browse files Browse the repository at this point in the history
release/v0.5.0
  • Loading branch information
anggoran authored Sep 29, 2024
2 parents 425524f + 26c1fac commit 2c10188
Show file tree
Hide file tree
Showing 21 changed files with 136,195 additions and 38 deletions.
8 changes: 4 additions & 4 deletions controllers/hanzi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const getHanziList = async (
`
id,
hanzi_id (form, meaning),
pinyin_id (name)
pinyin_id (sound)
`,
{ count: "exact" },
)
Expand All @@ -31,7 +31,7 @@ export const getHanziList = async (
meaning: hanzi.meaning,
},
pinyin: {
name: pinyin.name,
sound: pinyin.sound,
},
};
}) as HanziPinyinModel[];
Expand All @@ -54,7 +54,7 @@ export const getHanziDetail = async (
`
id,
hanzi_id (form, meaning, type, etymology),
pinyin_id (name, latin, tone)
pinyin_id (sound, latin, tone)
`,
).eq("id", id);

Expand All @@ -70,7 +70,7 @@ export const getHanziDetail = async (
etymology: hanzi.etymology,
},
pinyin: {
name: pinyin.name,
sound: pinyin.sound,
latin: pinyin.latin,
tone: pinyin.tone,
},
Expand Down
14 changes: 14 additions & 0 deletions controllers/word.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { WordModel } from "../models/hanzi.ts";
import { supabase } from "../utils/supabase.ts";

export const getWordList = async (
{ keyword, scroll }: { keyword: string; scroll: number },
) => {
const contentPerScroll = 10;
const { data, count } = await supabase.rpc("word_algo", {
search_term: keyword,
offset_value: (scroll - 1) * contentPerScroll,
limit_value: contentPerScroll,
}, { count: "exact" });
return { word: data as WordModel[], count };
};
8 changes: 6 additions & 2 deletions fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@

import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $api_joke from "./routes/api/joke.ts";
import * as $api_word from "./routes/api/word.ts";
import * as $greet_name_ from "./routes/greet/[name].tsx";
import * as $hanzi_id_ from "./routes/hanzi/[id].tsx";
import * as $hanzi_index from "./routes/hanzi/index.tsx";
import * as $index from "./routes/index.tsx";
import * as $listening_index from "./routes/listening/index.tsx";
import * as $reading_quiz_ from "./routes/reading/[quiz].tsx";
import * as $reading_index from "./routes/reading/index.tsx";
import * as $word_index from "./routes/word/index.tsx";
import * as $writing_quiz_ from "./routes/writing/[quiz].tsx";
import * as $writing_index from "./routes/writing/index.tsx";
import * as $Counter from "./islands/Counter.tsx";
import * as $Dropdown from "./islands/Dropdown.tsx";
import * as $InfiniteWords from "./islands/InfiniteWords.tsx";
import * as $Label from "./islands/Label.tsx";
import * as $Menu from "./islands/Menu.tsx";
import * as $SolutionWriter from "./islands/SolutionWriter.tsx";
Expand All @@ -28,20 +30,22 @@ const manifest = {
routes: {
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/api/joke.ts": $api_joke,
"./routes/api/word.ts": $api_word,
"./routes/greet/[name].tsx": $greet_name_,
"./routes/hanzi/[id].tsx": $hanzi_id_,
"./routes/hanzi/index.tsx": $hanzi_index,
"./routes/index.tsx": $index,
"./routes/listening/index.tsx": $listening_index,
"./routes/reading/[quiz].tsx": $reading_quiz_,
"./routes/reading/index.tsx": $reading_index,
"./routes/word/index.tsx": $word_index,
"./routes/writing/[quiz].tsx": $writing_quiz_,
"./routes/writing/index.tsx": $writing_index,
},
islands: {
"./islands/Counter.tsx": $Counter,
"./islands/Dropdown.tsx": $Dropdown,
"./islands/InfiniteWords.tsx": $InfiniteWords,
"./islands/Label.tsx": $Label,
"./islands/Menu.tsx": $Menu,
"./islands/SolutionWriter.tsx": $SolutionWriter,
Expand Down
55 changes: 55 additions & 0 deletions islands/InfiniteWords.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { WordModel } from "../models/hanzi.ts";

export default function InfiniteWords({ keyword }: { keyword: string }) {
const [loading, setLoading] = useState(false);
const [words, setWords] = useState<WordModel[]>([]);
const [scroll, setScroll] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loaderRef = useRef(null);

const loadMore = async () => {
setLoading(true);
const { word: newWords, count } = await fetch(
`/api/word?keyword=${keyword}&scroll=${scroll}`,
).then((res) => res.json());
setWords((prevWords) => [...prevWords, ...newWords]);
if (count < 10) setHasMore(false);
setLoading(false);
};

useEffect(() => {
if (hasMore) loadMore();
}, [scroll]);

useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !loading) {
setScroll((prevScroll) => prevScroll + 1);
}
}, { threshold: 1.0 });

const currentLoader = loaderRef.current;

if (currentLoader) observer.observe(currentLoader);
return () => {
if (currentLoader) observer.unobserve(currentLoader);
};
}, [loading]);

return (
<>
<div className="space-y-4">
{words.map((w) => (
<div key={w.id} ref={loaderRef} className="p-4 border rounded">
<p>
<span class="font-bold">{w.hanzi}</span> {`(${w.pinyin})`}
</p>
<p className="text-gray-500">{w.english}</p>
</div>
))}
</div>
{loading && <p className="text-center mt-4">Loading items...</p>}
</>
);
}
9 changes: 8 additions & 1 deletion models/hanzi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export interface WordModel {
id: number;
hanzi: string;
pinyin: string;
english: string;
}

export interface HanziPinyinModel {
id: number;
hanzi: HanziModel;
Expand All @@ -14,7 +21,7 @@ export interface HanziModel {

export interface PinyinModel {
id: number;
name: string;
sound: string;
latin: string;
tone: number | null;
}
21 changes: 0 additions & 21 deletions routes/api/joke.ts

This file was deleted.

14 changes: 14 additions & 0 deletions routes/api/word.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FreshContext } from "$fresh/server.ts";
import { getWordList } from "../../controllers/word.ts";

export const handler = async (_req: Request, _ctx: FreshContext) => {
const url = new URL(_req.url);
const keyword = url.searchParams.get("keyword")!;
const scroll = parseInt(url.searchParams.get("scroll")!);

const data = await getWordList({ keyword, scroll });

return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
};
2 changes: 1 addition & 1 deletion routes/hanzi/(_islands)/HanziTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function HanziTable({ props }: { props: HanziTableProps }) {
<a href={"/hanzi" + `/${id}`}>{hanzi.form}</a>
</td>
<td className="w-20 p-1 border border-gray-500 text-center">
{pinyin.name}
{pinyin.sound}
</td>
<td className="w-96 text-sm px-2 py-1 border border-gray-500">
<ul className={styledCell(hanzi.meaning, index)}>
Expand Down
2 changes: 1 addition & 1 deletion routes/hanzi/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function Home(props: PageProps<Data>) {
<SolutionWriter character={hanzi.form} label="Animate" />
<h6 className="text-left text-lg font-bold outline outline-2 px-4 rounded-md my-2">
<SoundButton
text={pinyin.name}
text={pinyin.sound}
sound={pinyin.latin! + pinyin.tone!}
/>
</h6>
Expand Down
1 change: 1 addition & 0 deletions routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default function Home() {
"/reading",
"/writing",
"/hanzi",
"/word",
].map((e) => (
<li>
<a
Expand Down
32 changes: 32 additions & 0 deletions routes/word/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PageProps } from "$fresh/server.ts";
import InfiniteWords from "../../islands/InfiniteWords.tsx";

export default function WordPage(props: PageProps) {
const keyword = props.url.searchParams.get("keyword") ?? "";
return (
<>
<a href="/">Back to home</a>
<div className="h-auto content-center bg-white">
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Word Database</h1>
<form className="flex space-x-2 mb-4">
<input
type="text"
name="keyword"
value={keyword}
placeholder="Search for a word..."
className="p-2 border rounded w-full"
/>
<button
type="submit"
className="px-4 py-2 bg-black text-white rounded"
>
Search
</button>
</form>
{keyword && <InfiniteWords keyword={keyword} />}
</div>
</div>
</>
);
}
89 changes: 89 additions & 0 deletions static/data/create-hanzi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as CSV from "jsr:@std/csv";

interface Hanzi {
form: string;
meaning: string;
type: string;
etymology: string;
}

const readPinyinCSV = async () => {
const content = await Deno.readTextFile("./static/data/pinyin.csv");
return CSV.parse(content).map((row) => row[1] + row[2]).slice(1);
};

const readUnihanTXT = async () => {
const content = await Deno.readTextFile("./static/data/unihan.txt");
return content.trim().split("\n");
};

const readCedictTXT = async () => {
const cedictContent = await Deno.readTextFile("./static/data/cedict.txt");
return cedictContent.split("\n");
};

const createUnihanList = async () => {
const unihanLines = await readUnihanTXT();
return unihanLines.map((line) => {
const parsedLine = JSON.parse(line);
const hasPhilosophy = parsedLine["etymology"] !== undefined;
return {
character: parsedLine["character"],
definition: parsedLine["definition"],
type: hasPhilosophy ? parsedLine["etymology"]["type"] : "",
etymology: hasPhilosophy ? parsedLine["etymology"]["hint"] : "",
};
});
};

const createCedictList = async () => {
const cedictLines = await readCedictTXT();
return cedictLines.map((line) => {
const parts = line.split(" ");
const data = {
simplified: parts[1],
pinyin: parts[2].slice(1, -1).toLowerCase()
.replace("u:", "v").replace("5", ""),
};
if (data.simplified === "儿" && data.pinyin === "r5") {
data.pinyin = "er5";
} else if (data.simplified === "剋" && data.pinyin === "kei1") {
data.pinyin = "ke4";
} else if (data.simplified === "忒" && data.pinyin === "tei1") {
data.pinyin = "te4";
}
return data;
});
};

const createHanziList = async () => {
const pinyinList = await readPinyinCSV();
const cedictList = await createCedictList();
const unihanList = await createUnihanList();
const hanziSet = new Set<string>();
const hanziList: Hanzi[] = [];
cedictList.forEach((cedict) => {
const unihan = unihanList.find((e) => e.character === cedict.simplified);
const pinyin = pinyinList.find((e) => e === cedict.pinyin);
if (unihan && pinyin && !hanziSet.has(cedict.simplified)) {
hanziList.push({
form: cedict.simplified,
meaning: unihan.definition,
type: unihan.type,
etymology: unihan.etymology,
});
hanziSet.add(cedict.simplified);
}
});
return hanziList;
};

const writeHanziCSV = async () => {
const hanziList = await createHanziList();
const content = CSV.stringify(hanziList as unknown as CSV.DataItem[], {
columns: ["form", "meaning", "type", "etymology"],
});
await Deno.writeTextFile("./static/data/hanzi.csv", content);
};

await writeHanziCSV();
Loading

0 comments on commit 2c10188

Please sign in to comment.