254 lines
9.6 KiB
TypeScript
254 lines
9.6 KiB
TypeScript
'use client'
|
||
|
||
import React, { 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 getDefaultCardSetId = () => {
|
||
if (initialData?.cardSetId) return initialData.cardSetId
|
||
|
||
// 優先選擇預設卡組
|
||
const defaultCardSet = cardSets.find(set => set.isDefault)
|
||
if (defaultCardSet) return defaultCardSet.id
|
||
|
||
// 如果沒有預設卡組,選擇第一個卡組
|
||
if (cardSets.length > 0) return cardSets[0].id
|
||
|
||
// 如果沒有任何卡組,返回空字串
|
||
return ''
|
||
}
|
||
|
||
const [formData, setFormData] = useState<CreateFlashcardRequest>({
|
||
cardSetId: getDefaultCardSetId(),
|
||
english: initialData?.english || '',
|
||
chinese: initialData?.chinese || '',
|
||
pronunciation: initialData?.pronunciation || '',
|
||
partOfSpeech: initialData?.partOfSpeech || '名詞',
|
||
example: initialData?.example || '',
|
||
})
|
||
|
||
// 當 cardSets 改變時,重新設定 cardSetId(處理初始載入的情況)
|
||
React.useEffect(() => {
|
||
if (!formData.cardSetId && cardSets.length > 0) {
|
||
const defaultId = getDefaultCardSetId()
|
||
if (defaultId) {
|
||
setFormData(prev => ({ ...prev, cardSetId: defaultId }))
|
||
}
|
||
}
|
||
}, [cardSets])
|
||
|
||
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>
|
||
{cardSets.length === 0 ? (
|
||
<div className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500">
|
||
載入卡組中...
|
||
</div>
|
||
) : (
|
||
<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
|
||
>
|
||
{/* 如果沒有選中任何卡組,顯示提示 */}
|
||
{!formData.cardSetId && (
|
||
<option value="" disabled>
|
||
請選擇卡組
|
||
</option>
|
||
)}
|
||
{/* 先顯示預設卡組 */}
|
||
{cardSets
|
||
.filter(set => set.isDefault)
|
||
.map(set => (
|
||
<option key={set.id} value={set.id}>
|
||
📂 {set.name} (預設)
|
||
</option>
|
||
))}
|
||
{/* 再顯示其他卡組 */}
|
||
{cardSets
|
||
.filter(set => !set.isDefault)
|
||
.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 || cardSets.length === 0 || !formData.cardSetId}
|
||
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 ? '處理中...' :
|
||
cardSets.length === 0 ? '載入中...' :
|
||
(isEdit ? '更新詞卡' : '新增詞卡')}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
} |