255 lines
8.6 KiB
TypeScript
255 lines
8.6 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState } from 'react'
|
||
import { flashcardsService, type CreateFlashcardRequest, type Flashcard } from '@/lib/services/flashcards'
|
||
import { BluePlayButton } from '@/components/shared/BluePlayButton'
|
||
|
||
interface FlashcardFormProps {
|
||
cardSets?: any[] // 保持相容性
|
||
initialData?: Partial<Flashcard>
|
||
isEdit?: boolean
|
||
onSuccess: () => void
|
||
onCancel: () => void
|
||
}
|
||
|
||
export function FlashcardForm({ initialData, isEdit = false, onSuccess, onCancel }: FlashcardFormProps) {
|
||
const [isPlayingWord, setIsPlayingWord] = useState(false)
|
||
|
||
// TTS 播放邏輯
|
||
const handleToggleWordTTS = (text: string, lang?: string) => {
|
||
if (isPlayingWord) {
|
||
speechSynthesis.cancel()
|
||
setIsPlayingWord(false)
|
||
return
|
||
}
|
||
|
||
const utterance = new SpeechSynthesisUtterance(text)
|
||
utterance.lang = lang || 'en-US'
|
||
utterance.rate = 0.8
|
||
|
||
utterance.onstart = () => setIsPlayingWord(true)
|
||
utterance.onend = () => setIsPlayingWord(false)
|
||
utterance.onerror = () => setIsPlayingWord(false)
|
||
|
||
speechSynthesis.speak(utterance)
|
||
}
|
||
|
||
const [formData, setFormData] = useState<CreateFlashcardRequest>({
|
||
word: initialData?.word || '',
|
||
translation: initialData?.translation || '',
|
||
definition: initialData?.definition || '',
|
||
pronunciation: initialData?.pronunciation || '',
|
||
partOfSpeech: initialData?.partOfSpeech || 'noun',
|
||
example: initialData?.example || '',
|
||
exampleTranslation: initialData?.exampleTranslation || '',
|
||
cefr: initialData?.cefr || 'A2',
|
||
})
|
||
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||
const { name, value } = e.target
|
||
setFormData(prev => ({ ...prev, [name]: value }))
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setLoading(true)
|
||
setError(null)
|
||
|
||
try {
|
||
let result
|
||
if (isEdit && initialData?.id) {
|
||
result = await flashcardsService.updateFlashcard(initialData.id, formData)
|
||
} else {
|
||
result = await flashcardsService.createFlashcard(formData)
|
||
}
|
||
|
||
if (result.success) {
|
||
onSuccess()
|
||
} else {
|
||
setError(result.error || `Failed to ${isEdit ? 'update' : 'create'} flashcard`)
|
||
}
|
||
} catch (error) {
|
||
setError(error instanceof Error ? error.message : 'An unexpected error occurred')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
{error && (
|
||
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<label htmlFor="word" className="block text-sm font-medium text-gray-700 mb-2">
|
||
單字 *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="word"
|
||
name="word"
|
||
value={formData.word}
|
||
onChange={handleChange}
|
||
required
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="請輸入單字"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="translation" className="block text-sm font-medium text-gray-700 mb-2">
|
||
翻譯 *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="translation"
|
||
name="translation"
|
||
value={formData.translation}
|
||
onChange={handleChange}
|
||
required
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="請輸入中文翻譯"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="definition" className="block text-sm font-medium text-gray-700 mb-2">
|
||
定義 *
|
||
</label>
|
||
<textarea
|
||
id="definition"
|
||
name="definition"
|
||
value={formData.definition}
|
||
onChange={handleChange}
|
||
required
|
||
rows={3}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="請輸入英文定義"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="pronunciation" className="block text-sm font-medium text-gray-700 mb-2">
|
||
發音
|
||
</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
id="pronunciation"
|
||
name="pronunciation"
|
||
value={formData.pronunciation}
|
||
onChange={handleChange}
|
||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="例如: /wɜːrd/"
|
||
/>
|
||
{formData.pronunciation && (
|
||
<BluePlayButton
|
||
text={formData.word}
|
||
isPlaying={isPlayingWord}
|
||
onToggle={handleToggleWordTTS}
|
||
size="sm"
|
||
title="播放單詞"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="partOfSpeech" className="block text-sm font-medium text-gray-700 mb-2">
|
||
詞性 *
|
||
</label>
|
||
<select
|
||
id="partOfSpeech"
|
||
name="partOfSpeech"
|
||
value={formData.partOfSpeech}
|
||
onChange={handleChange}
|
||
required
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="noun">名詞 (noun)</option>
|
||
<option value="verb">動詞 (verb)</option>
|
||
<option value="adjective">形容詞 (adjective)</option>
|
||
<option value="adverb">副詞 (adverb)</option>
|
||
<option value="preposition">介詞 (preposition)</option>
|
||
<option value="interjection">感歎詞 (interjection)</option>
|
||
<option value="phrase">片語 (phrase)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="cefr" className="block text-sm font-medium text-gray-700 mb-2">
|
||
CEFR 難度等級
|
||
</label>
|
||
<select
|
||
id="cefr"
|
||
name="cefr"
|
||
value={formData.cefr}
|
||
onChange={handleChange}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="A1">A1 - 基礎</option>
|
||
<option value="A2">A2 - 基礎</option>
|
||
<option value="B1">B1 - 中級</option>
|
||
<option value="B2">B2 - 中高級</option>
|
||
<option value="C1">C1 - 高級</option>
|
||
<option value="C2">C2 - 精通</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="example" className="block text-sm font-medium text-gray-700 mb-2">
|
||
例句 *
|
||
</label>
|
||
<textarea
|
||
id="example"
|
||
name="example"
|
||
value={formData.example}
|
||
onChange={handleChange}
|
||
required
|
||
rows={2}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="請輸入例句"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="exampleTranslation" className="block text-sm font-medium text-gray-700 mb-2">
|
||
例句翻譯
|
||
</label>
|
||
<textarea
|
||
id="exampleTranslation"
|
||
name="exampleTranslation"
|
||
value={formData.exampleTranslation}
|
||
onChange={handleChange}
|
||
rows={2}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="請輸入例句的中文翻譯"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-4">
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200"
|
||
>
|
||
{loading ? (isEdit ? '更新中...' : '創建中...') : (isEdit ? '更新詞卡' : '創建詞卡')}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onCancel}
|
||
disabled={loading}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 disabled:bg-gray-100 transition-colors duration-200"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
</form>
|
||
)
|
||
} |