summaryrefslogtreecommitdiff
path: root/src/components/tones
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-29 12:10:22 +0700
committerpolwex <polwex@sortug.com>2025-05-29 12:10:22 +0700
commita3f24ea79b14394b24c4b60a010651eb29eeb872 (patch)
treecb1c4937084116f66a59727ee752afd974714c8e /src/components/tones
parent7abf2227438362ad30820ee236405ec1b57a40b6 (diff)
glorious new db
Diffstat (limited to 'src/components/tones')
-rw-r--r--src/components/tones/ToneSelectorClient.tsx199
1 files changed, 199 insertions, 0 deletions
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>
+ );
+}