diff options
author | polwex <polwex@sortug.com> | 2025-05-29 12:10:22 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-29 12:10:22 +0700 |
commit | a3f24ea79b14394b24c4b60a010651eb29eeb872 (patch) | |
tree | cb1c4937084116f66a59727ee752afd974714c8e /src/components | |
parent | 7abf2227438362ad30820ee236405ec1b57a40b6 (diff) |
glorious new db
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/Flashcard/ServerCard.tsx | 43 | ||||
-rw-r--r-- | src/components/Flashcard/Syllable.tsx | 44 | ||||
-rw-r--r-- | src/components/Flashcard/SyllableModal.tsx | 110 | ||||
-rw-r--r-- | src/components/Flashcard/SyllableSpan.tsx | 45 | ||||
-rw-r--r-- | src/components/lang/ThaiPhonology.tsx | 250 | ||||
-rw-r--r-- | src/components/tones/ToneSelectorClient.tsx | 199 | ||||
-rw-r--r-- | src/components/ui/skeleton.tsx | 13 |
7 files changed, 692 insertions, 12 deletions
diff --git a/src/components/Flashcard/ServerCard.tsx b/src/components/Flashcard/ServerCard.tsx index d377dce..df37ba8 100644 --- a/src/components/Flashcard/ServerCard.tsx +++ b/src/components/Flashcard/ServerCard.tsx @@ -23,13 +23,21 @@ import { WordData, } from "@/zoom/logic/types"; import { CardResponse } from "@/lib/types/cards"; -import { thaiData } from "@/pages/api/nlp"; +import { thaiData } from "@/lib/calls/nlp"; import { getRandomHexColor } from "@/lib/utils"; import { BookmarkIconito } from "./BookmarkButton"; +import SyllableSpan from "./SyllableSpan"; +import SyllableCard from "./Syllable"; -export async function CardFront({ data }: { data: CardResponse }) { +export async function CardFront({ + data, + needFetch = true, +}: { + data: CardResponse; + needFetch?: boolean; +}) { // const extraData = data.expression.lang - const extraData = await thaiData(data.expression.spelling); + const extraData = needFetch ? await thaiData(data.expression.spelling) : []; // console.log({ extraData }); return ( @@ -42,15 +50,26 @@ export async function CardFront({ data }: { data: CardResponse }) { } > <p className="text-5xl cursor-pointer font-semibold text-slate-800 dark:text-slate-100 text-center"> - {extraData[0]?.syllables.map((syl, i) => ( - <span - key={syl + i} - style={{ color: getRandomHexColor() }} - className="m-1 hover:text-6xl" - > - {syl} - </span> - ))} + {needFetch ? ( + extraData[0]?.syllables.map((syl, i) => ( + // <span + // key={syl + i} + // style={{ color: getRandomHexColor() }} + // className="m-1 hover:text-6xl" + // > + // {syl} + // </span> + <SyllableSpan + key={syl + i} + spelling={syl} + lang={data.expression.lang} + /> + )) + ) : ( + <p className="text-5xl cursor-pointer hover:text-blue-700 font-semibold text-slate-800 dark:text-slate-100 text-center"> + {data.expression.spelling} + </p> + )} </p> </Suspense> <IpaDisplay ipaEntries={data.expression.ipa} /> diff --git a/src/components/Flashcard/Syllable.tsx b/src/components/Flashcard/Syllable.tsx new file mode 100644 index 0000000..e470a4b --- /dev/null +++ b/src/components/Flashcard/Syllable.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { syllableAction, thaiAnalysis } from "@/actions/lang"; +import { CardResponse } from "@/lib/types/cards"; +import { ReactNode, useState, useTransition } from "react"; +import { Spinner } from "../ui/spinner"; +import Modal from "@/components/Modal"; +import { getRandomHexColor } from "@/lib/utils"; + +const SyllableCard: React.FC<{ data: CardResponse }> = ({ data }) => { + return ( + <div className="absolute w-full h-full bg-white dark:bg-slate-800 rounded-xl backface-hidden flex flex-col justify-center gap-8 items-center p-6"> + <p className="text-5xl cursor-pointer hover:text-blue-700 font-semibold text-slate-800 dark:text-slate-100 text-center"> + {data.expression.spelling} + </p> + <IpaDisplay ipaEntries={data.expression.ipa} /> + </div> + ); +}; + +export default SyllableCard; + +const IpaDisplay = ({ + ipaEntries, +}: { + ipaEntries: Array<{ ipa: string; tags?: string[] }>; +}) => { + if (!ipaEntries || ipaEntries.length === 0) return null; + return ( + <div className="flex items-center space-x-2 flex-wrap"> + {ipaEntries.map((entry, index) => { + const tags = entry.tags ? entry.tags : []; + return ( + <span key={index} className="text-lg text-blue-600 font-serif"> + {entry.ipa}{" "} + {tags.length > 0 && ( + <span className="text-xs text-gray-500">({tags.join(", ")})</span> + )} + </span> + ); + })} + </div> + ); +}; diff --git a/src/components/Flashcard/SyllableModal.tsx b/src/components/Flashcard/SyllableModal.tsx new file mode 100644 index 0000000..a00fd10 --- /dev/null +++ b/src/components/Flashcard/SyllableModal.tsx @@ -0,0 +1,110 @@ +// This is a Server Component +import React from "react"; +import db from "@/lib/db"; +import { + Card, + CardHeader, + CardDescription, + CardContent, + CardFooter, + CardTitle, +} from "@/components/ui/card"; +import { NLP } from "sortug-ai"; +import { + BookOpen, + Volume2, + Link as LinkIcon, + ChevronDown, + ChevronUp, + Search, + Info, + MessageSquareQuote, + Tags, + ListTree, + Lightbulb, +} from "lucide-react"; +import { + Example, + SubSense, + RelatedEntry, + Sense, + WordData, +} from "@/zoom/logic/types"; +import { isTonal } from "@/lib/lang/utils"; + +type WordProps = { text: string; lang: string }; +export default async function (props: WordProps) { + const { text, lang } = props; + const data = db.fetchWordBySpelling(text, lang); + + if (!data) return <p>oh...</p>; + console.log(data.senses[0]); + return ( + <Card className="overflow-y-scroll max-h-[80vh]"> + <CardHeader> + <CardTitle> + <h1 className="text-5xl">{text}</h1> + </CardTitle> + <CardDescription> + <IpaDisplay ipaEntries={data.ipa} /> + </CardDescription> + </CardHeader> + <CardContent> + {isTonal(text) ? <Tones {...props} /> : <NotTones {...props} />} + </CardContent> + <CardFooter></CardFooter> + </Card> + ); + // return ( + // <div className="p-6"> + // <h3 className="mb-2 text-2xl font-bold">{word}</h3> + // <p className="mb-1 text-xl text-green-600">${word.}</p> + // <p className="text-gray-700">{word}</p> + // <p className="mt-4 text-xs text-gray-500"> + // Content rendered on the server at: {new Date().toLocaleTimeString()} + // </p> + // </div> + // ); +} + +// Helper component for IPA display +const IpaDisplay = ({ + ipaEntries, +}: { + ipaEntries: Array<{ ipa: string; tags?: string[] }>; +}) => { + if (!ipaEntries || ipaEntries.length === 0) return null; + return ( + <div className="flex items-center space-x-2 flex-wrap"> + {ipaEntries.map((entry, index) => { + const tags = entry.tags ? entry.tags : []; + return ( + <span key={index} className="text-lg text-blue-600 font-serif"> + {entry.ipa}{" "} + {tags.length > 0 && ( + <span className="text-xs text-gray-500">({tags.join(", ")})</span> + )} + </span> + ); + })} + <button + className="p-1 text-blue-500 hover:text-blue-700 transition-colors" + title="Pronounce" + // onClick={() => { + // /* Pronunciation logic would be client-side or a server roundtrip for audio file. */ alert( + // "Pronunciation feature not implemented for server component.", + // ); + // }} + > + <Volume2 size={20} /> + </button> + </div> + ); +}; + +function Tones({ text, lang }: WordProps) { + return <div></div>; +} +function NotTones({ text, lang }: WordProps) { + return <div></div>; +} diff --git a/src/components/Flashcard/SyllableSpan.tsx b/src/components/Flashcard/SyllableSpan.tsx new file mode 100644 index 0000000..445895e --- /dev/null +++ b/src/components/Flashcard/SyllableSpan.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { syllableAction, thaiAnalysis } from "@/actions/lang"; +import { CardResponse } from "@/lib/types/cards"; +import { ReactNode, useState, useTransition } from "react"; +import { Spinner } from "../ui/spinner"; +import Modal from "@/components/Modal"; +import { getRandomHexColor } from "@/lib/utils"; + +const SyllableSpan: React.FC<{ spelling: string; lang: string }> = ({ + spelling, + lang, +}) => { + const [modalContent, setModalContent] = useState<ReactNode | null>(null); + + const closeModal = () => setModalContent(null); + + const [isPending, startTransition] = useTransition(); + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + startTransition(async () => { + const modal = await syllableAction(spelling, lang); + setModalContent(modal); + }); + }; + + return ( + <> + <span + onClick={handleClick} + className="m-1 hover:text-6xl" + style={{ color: getRandomHexColor() }} + > + {spelling} + </span> + {modalContent && ( + <Modal onClose={closeModal} isOpen={!!modalContent}> + {modalContent} + </Modal> + )} + </> + ); +}; + +export default SyllableSpan; diff --git a/src/components/lang/ThaiPhonology.tsx b/src/components/lang/ThaiPhonology.tsx new file mode 100644 index 0000000..199d0b8 --- /dev/null +++ b/src/components/lang/ThaiPhonology.tsx @@ -0,0 +1,250 @@ +// import React from "react"; + +// /** +// * ThaiConsonantGrid – a visual table of Thai consonants modelled after the +// * traditional Sanskrit‑style chart. Each cell shows the Thai glyph and its +// * IPA. Rows are places of articulation, columns are manners. Colours follow +// * the pedagogical convention from the reference screenshot. +// */ +// export default function ThaiConsonantGrid() { +// /** Column headers in display order */ +// const cols = [ +// "stopped", +// "aspirated", +// "voiced", +// "voiced‑aspirated", +// "nasal", +// "semiVowel", +// "sibilant", +// "H‑aspirate", +// "throatBase", +// "others", +// ] as const; + +// /** Row headers in display order */ +// const rows = [ +// "Deep Throat", +// "guttural", +// "palatal", +// "cerebral", +// "dental", +// "labial", +// ] as const; +// type ArticulationPoint = +// | "bilabial" +// | "labiodental" +// | "dental" +// | "alveolar" +// | "postalveolar" +// | "retroflex" +// | "palatal" +// | "velar" +// | "uvular" +// | "pharyngeal" +// | "glottal"; +// type ArticulationMode = +// | "plosive" +// | "nasal" +// | "trill" +// | "flap" +// | "fricative" +// | "affricate" +// | "lateral fricative" +// | "approximant" +// | "lateral approximant"; +// type Voicing = "unvoiced" | "voiced" | "aspirated" | "voiced aspirated"; +// type VowelHeight = "high" | "close-mid" | "open-mid" | "open"; +// type VowelFront = "front" | "central" | "back"; +// type VowelRound = "rounded" | "unrounded"; + +// /** +// * Minimal description for each consonant we want to render. Position is +// * given by its (rowIdx, colIdx). The colour is a Tailwind background class +// * so you can tweak the palette in one place. +// */ +// interface Cell { +// row: number; // 0‑based index into rows +// col: number; // 0‑based index into cols +// glyph: string; +// ipa: string; +// colour: string; // Tailwind bg‑* class +// highlight?: boolean; // optional neon border +// class: "high" | "mid" | "low"; +// } + +// const cells: Cell[] = [ +// // ───────────────────── guttural row (index 1) ────────────────────── +// { +// row: 1, +// col: 0, +// glyph: "ก", +// class: "high", +// ipa: "/k/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 1, +// glyph: "ข", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 1, +// glyph: "ฃ", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 2, +// glyph: "ค", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 2, +// glyph: "ฅ", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 2, +// glyph: "ฆ", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 4, +// glyph: "ง", +// ipa: "/ŋ/", +// colour: "bg-sky-500", +// highlight: true, +// }, + +// // ───────────────────── palatal row (index 2) ─────────────────────── +// { row: 2, col: 0, glyph: "จ", ipa: "/tɕ/", colour: "bg-pink-500" }, +// { row: 2, col: 1, glyph: "ฉ", ipa: "/tɕʰ/", colour: "bg-pink-500" }, +// { row: 2, col: 2, glyph: "ช", ipa: "/tɕʰ/", colour: "bg-pink-500" }, +// { row: 2, col: 2, glyph: "ซ", ipa: "/s/", colour: "bg-pink-500" }, +// { row: 2, col: 3, glyph: "ฌ", ipa: "/tɕʰ/", colour: "bg-pink-500" }, +// { row: 2, col: 5, glyph: "ญ", ipa: "/j/", colour: "bg-pink-500" }, + +// // ───────────────────── cerebral row (index 3) ────────────────────── +// { row: 3, col: 0, glyph: "ฎ", ipa: "/d/", colour: "bg-emerald-700" }, +// { row: 3, col: 0, glyph: "ฐ", ipa: "/t/", colour: "bg-emerald-700" }, +// { row: 3, col: 1, glyph: "ฏ", ipa: "/tʰ/", colour: "bg-emerald-700" }, +// { +// row: 3, +// col: 4, +// glyph: "ฑ", +// ipa: "/tʰ or d/", +// colour: "bg-emerald-700", +// highlight: true, +// }, +// { row: 3, col: 3, glyph: "ฒ", ipa: "/tʰ/", colour: "bg-emerald-700" }, +// { row: 3, col: 4, glyph: "ณ", ipa: "/n/", colour: "bg-emerald-700" }, +// { row: 3, col: 5, glyph: "ศ", ipa: "/s/", colour: "bg-emerald-700" }, +// { row: 3, col: 5, glyph: "ษ", ipa: "/s/", colour: "bg-emerald-700" }, + +// // ───────────────────── dental row (index 4) ──────────────────────── +// { row: 4, col: 0, glyph: "ต", ipa: "/d/", colour: "bg-emerald-600" }, +// { row: 4, col: 0, glyph: "ถ", ipa: "/t/", colour: "bg-emerald-600" }, +// { row: 4, col: 1, glyph: "ท", ipa: "/tʰ/", colour: "bg-emerald-600" }, +// { row: 4, col: 2, glyph: "ธ", ipa: "/tʰ/", colour: "bg-emerald-600" }, +// { row: 4, col: 4, glyph: "น", ipa: "/n/", colour: "bg-emerald-600" }, +// { row: 4, col: 6, glyph: "ส", ipa: "/s/", colour: "bg-emerald-600" }, + +// // ───────────────────── labial row (index 5) ──────────────────────── +// { row: 5, col: 0, glyph: "บ", ipa: "/b/", colour: "bg-orange-500" }, +// { row: 5, col: 0, glyph: "ป", ipa: "/p/", colour: "bg-orange-500" }, +// { row: 5, col: 1, glyph: "ผ", ipa: "/pʰ/", colour: "bg-orange-500" }, +// { row: 5, col: 2, glyph: "พ", ipa: "/pʰ/", colour: "bg-orange-500" }, +// { row: 5, col: 2, glyph: "ฟ", ipa: "/f/", colour: "bg-orange-500" }, +// { row: 5, col: 3, glyph: "ภ", ipa: "/pʰ/", colour: "bg-orange-500" }, +// { row: 5, col: 4, glyph: "ม", ipa: "/m/", colour: "bg-orange-500" }, +// { +// row: 5, +// col: 9, +// glyph: "ฟฬ", +// ipa: "/l/", +// colour: "bg-emerald-600", +// highlight: true, +// }, + +// // ───────────────────── extra column (index^?) – throat + others ───── +// { row: 1, col: 7, glyph: "ห", ipa: "/h/", colour: "bg-gray-400" }, +// { row: 1, col: 8, glyph: "อ", ipa: "/ʔ/", colour: "bg-gray-400" }, +// ]; + +// return ( +// <div className="overflow-x-auto p-4"> +// {/* Column header */} +// <div +// className="grid" +// style={{ +// gridTemplateColumns: `auto repeat(${cols.length}, minmax(4rem, 1fr))`, +// }} +// > +// {/* top‑left empty cell */} +// <div /> +// {cols.map((c) => ( +// <div +// key={c} +// className="bg-neutral-800 text-amber-300 text-center uppercase py-2 text-sm font-semibold border border-neutral-700" +// > +// {c} +// </div> +// ))} + +// {/* rows */} +// {rows.map((rowLabel, ri) => ( +// <React.Fragment key={rowLabel}> +// {/* row header */} +// <div className="bg-neutral-900 text-amber-300 flex items-center justify-center px-2 py-1 text-xs font-bold whitespace-nowrap border border-neutral-700"> +// {rowLabel} +// </div> +// {/* cells within the row */} +// {cols.map((_, ci) => { +// // We may have multiple consonants per slot; gather them. +// const here = cells.filter((c) => c.row === ri && c.col === ci); +// if (here.length === 0) +// return <div key={ci} className="border border-neutral-700" />; + +// return ( +// <div +// key={ci} +// className={[ +// "border border-neutral-700 rounded-md flex flex-col items-center justify-center gap-1 p-1 text-white", +// here[0].colour, +// here.some((c) => c.highlight) +// ? "ring-2 ring-green-400" +// : "", +// ].join(" ")} +// > +// {here.map((c, i) => ( +// <span key={i} className="text-sm leading-tight text-center"> +// <span className="block text-lg font-semibold"> +// {c.glyph} +// </span> +// <span className="block text-xs">{c.ipa}</span> +// </span> +// ))} +// </div> +// ); +// })} +// </React.Fragment> +// ))} +// </div> +// </div> +// ); +// } diff --git a/src/components/tones/ToneSelectorClient.tsx b/src/components/tones/ToneSelectorClient.tsx new file mode 100644 index 0000000..0ee9433 --- /dev/null +++ b/src/components/tones/ToneSelectorClient.tsx @@ -0,0 +1,199 @@ +'use client'; + +import { useState, useEffect, useTransition } from 'react'; +import { WordData } from '@/zoom/logic/types'; +import { fetchWordsByToneAndSyllables } from '@/actions/tones'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Skeleton } from '@/components/ui/skeleton'; // For loading state + +// Helper to display tones prominently +const ProminentToneDisplay = ({ wordData }: { wordData: WordData }) => { + if (!wordData.prosody || !Array.isArray(wordData.prosody)) { + return <p className="text-gray-500">No prosody data</p>; + } + + return ( + <div className="flex flex-col items-center mb-4"> + <h1 className="text-6xl font-bold text-blue-600 mb-2">{wordData.spelling}</h1> + <div className="flex space-x-4"> + {wordData.prosody.map((p, index) => ( + <div key={index} className="text-center"> + <p className="text-sm text-gray-500">Syllable {index + 1}</p> + <p className="text-5xl font-semibold text-indigo-500">{p.tone ?? '?'}</p> + </div> + ))} + </div> + {wordData.ipa && wordData.ipa.length > 0 && ( + <p className="text-xl text-gray-700 mt-2"> + {wordData.ipa.map(i => i.ipa).join(' / ')} + </p> + )} + </div> + ); +}; + + +export default function ToneSelectorClient({ initialWord }: { initialWord: WordData | null }) { + const [currentWord, setCurrentWord] = useState<WordData | null>(initialWord); + const [syllableCount, setSyllableCount] = useState<number>(initialWord?.syllables || 1); + const [selectedTones, setSelectedTones] = useState<(number | null)[]>( + initialWord?.prosody?.map(p => p.tone ?? null) || [null] + ); + const [isLoading, startTransition] = useTransition(); + + useEffect(() => { + // Adjust selectedTones array length when syllableCount changes + setSelectedTones(prevTones => { + const newTones = Array(syllableCount).fill(null); + for (let i = 0; i < Math.min(prevTones.length, syllableCount); i++) { + newTones[i] = prevTones[i]; + } + return newTones; + }); + }, [syllableCount]); + + const handleFetchWord = () => { + startTransition(async () => { + const word = await fetchWordsByToneAndSyllables(syllableCount, selectedTones); + setCurrentWord(word); + }); + }; + + const handleSyllableCountChange = (value: string) => { + const count = parseInt(value, 10); + if (!isNaN(count) && count > 0 && count <= 5) { // Max 5 syllables for simplicity + setSyllableCount(count); + } + }; + + const handleToneChange = (syllableIndex: number, value: string) => { + const tone = value === 'any' ? null : parseInt(value, 10); + setSelectedTones(prevTones => { + const newTones = [...prevTones]; + newTones[syllableIndex] = tone; + return newTones; + }); + }; + + const thaiTones = [ + { value: '1', label: '1 (Mid)' }, + { value: '2', label: '2 (Low)' }, + { value: '3', label: '3 (Falling)' }, + { value: '4', label: '4 (High)' }, + { value: '5', label: '5 (Rising)' }, + ]; + + return ( + <div className="container mx-auto p-4 max-w-2xl"> + <Card className="mb-6"> + <CardHeader> + <CardTitle>Thai Tone Explorer</CardTitle> + <CardDescription>Select syllable count and tones to find Thai words.</CardDescription> + </CardHeader> + <CardContent className="space-y-6"> + <div> + <Label htmlFor="syllable-count" className="text-lg font-medium">Number of Syllables</Label> + <Select + value={syllableCount.toString()} + onValueChange={handleSyllableCountChange} + > + <SelectTrigger id="syllable-count" className="w-full md:w-1/2 mt-1"> + <SelectValue placeholder="Select number of syllables" /> + </SelectTrigger> + <SelectContent> + {[1, 2, 3, 4, 5].map(num => ( + <SelectItem key={num} value={num.toString()}> + {num} Syllable{num > 1 ? 's' : ''} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {Array.from({ length: syllableCount }).map((_, index) => ( + <div key={index}> + <Label htmlFor={`tone-select-${index}`} className="text-lg font-medium"> + Tone for Syllable {index + 1} + </Label> + <Select + value={selectedTones[index]?.toString() || 'any'} + onValueChange={(value) => handleToneChange(index, value)} + > + <SelectTrigger id={`tone-select-${index}`} className="w-full md:w-1/2 mt-1"> + <SelectValue placeholder={`Select tone for syllable ${index + 1}`} /> + </SelectTrigger> + <SelectContent> + <SelectItem value="any">Any Tone</SelectItem> + {thaiTones.map(tone => ( + <SelectItem key={tone.value} value={tone.value}> + {tone.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + ))} + </CardContent> + <CardFooter> + <Button onClick={handleFetchWord} disabled={isLoading} className="w-full md:w-auto"> + {isLoading ? 'Searching...' : 'Find Word'} + </Button> + </CardFooter> + </Card> + + {isLoading && !currentWord && ( + <Card> + <CardHeader><Skeleton className="h-12 w-3/4" /></CardHeader> + <CardContent className="space-y-4"> + <Skeleton className="h-8 w-1/2" /> + <Skeleton className="h-20 w-full" /> + <Skeleton className="h-6 w-full" /> + </CardContent> + </Card> + )} + + {!isLoading && currentWord && ( + <Card> + <CardHeader> + <CardTitle className="text-center">Current Word</CardTitle> + </CardHeader> + <CardContent> + <ProminentToneDisplay wordData={currentWord} /> + {/* You can add more details from WordData here if needed, like definitions */} + {currentWord.senses && currentWord.senses.length > 0 && ( + <div className="mt-4 pt-4 border-t"> + <h3 className="text-lg font-semibold mb-2">Meanings:</h3> + {currentWord.senses.map((sense, sIdx) => ( + <div key={sIdx} className="mb-2 p-2 border rounded bg-gray-50"> + <p className="font-medium text-indigo-600">{sense.pos}</p> + {sense.senses && Array.isArray(sense.senses) && sense.senses.map((subSense, ssIdx) => ( + subSense.glosses && Array.isArray(subSense.glosses) && subSense.glosses.map((gloss: string, gIdx: number) => ( + <p key={`${ssIdx}-${gIdx}`} className="text-sm text-gray-700 ml-2">- {gloss}</p> + )) + ))} + </div> + ))} + </div> + )} + </CardContent> + </Card> + )} + + {!isLoading && !currentWord && ( + <Card> + <CardHeader> + <CardTitle className="text-center">No Word Found</CardTitle> + </CardHeader> + <CardContent> + <p className="text-center text-gray-600"> + Could not find a Thai word matching your criteria. Try different selections. + </p> + </CardContent> + </Card> + )} + </div> + ); +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="skeleton" + className={cn("bg-accent animate-pulse rounded-md", className)} + {...props} + /> + ) +} + +export { Skeleton } |