dramaling-vocab-learning/frontend/components/FlashcardForm.tsx

204 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect } from 'react'
import { flashcardsService, type CreateFlashcardRequest, type CardSet } from '@/lib/services/flashcards'
interface FlashcardFormProps {
cardSets: CardSet[]
initialData?: Partial<CreateFlashcardRequest & { id: string }>
isEdit?: boolean
onSuccess: () => void
onCancel: () => void
}
export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess, onCancel }: FlashcardFormProps) {
const [formData, setFormData] = useState<CreateFlashcardRequest>({
cardSetId: initialData?.cardSetId || (cardSets[0]?.id || ''),
english: initialData?.english || '',
chinese: initialData?.chinese || '',
pronunciation: initialData?.pronunciation || '',
partOfSpeech: initialData?.partOfSpeech || '名詞',
example: initialData?.example || '',
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const partOfSpeechOptions = [
'名詞', '動詞', '形容詞', '副詞', '介詞', '連詞', '感嘆詞', '代詞', '冠詞'
]
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 || '操作失敗')
}
} catch (err) {
setError('操作失敗,請重試')
} finally {
setLoading(false)
}
}
const handleChange = (field: keyof CreateFlashcardRequest, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">
{isEdit ? '編輯詞卡' : '新增詞卡'}
</h2>
<button
onClick={onCancel}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p className="text-red-600 text-sm">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* 詞卡集合選擇 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<select
value={formData.cardSetId}
onChange={(e) => handleChange('cardSetId', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
required
>
{cardSets.map(set => (
<option key={set.id} value={set.id}>
{set.name}
</option>
))}
</select>
</div>
{/* 英文單字 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.english}
onChange={(e) => handleChange('english', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如negotiate"
required
/>
</div>
{/* 中文翻譯 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.chinese}
onChange={(e) => handleChange('chinese', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如:談判,協商"
required
/>
</div>
{/* 詞性和發音 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<select
value={formData.partOfSpeech}
onChange={(e) => handleChange('partOfSpeech', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
required
>
{partOfSpeechOptions.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
value={formData.pronunciation}
onChange={(e) => handleChange('pronunciation', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如:/nɪˈɡoʊʃieɪt/"
/>
</div>
</div>
{/* 例句 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
value={formData.example}
onChange={(e) => handleChange('example', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如We need to negotiate the contract terms."
/>
</div>
{/* 操作按鈕 */}
<div className="flex justify-end space-x-4 pt-4">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
>
</button>
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? '處理中...' : (isEdit ? '更新詞卡' : '新增詞卡')}
</button>
</div>
</form>
</div>
</div>
</div>
)
}