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

267 lines
8.5 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 React, { useState, useMemo } from 'react'
import { Modal } from './ui/Modal'
import { Check, Loader2 } from 'lucide-react'
interface GeneratedCard {
word: string
translation: string
definition: string
partOfSpeech?: string
pronunciation?: string
example?: string
exampleTranslation?: string
synonyms?: string[]
difficultyLevel?: string
}
interface CardSet {
id: string
name: string
color: string
}
interface CardSelectionDialogProps {
isOpen: boolean
generatedCards: GeneratedCard[]
cardSets: CardSet[]
onClose: () => void
onSave: (selectedCards: GeneratedCard[], cardSetId?: string) => Promise<void>
}
export const CardSelectionDialog: React.FC<CardSelectionDialogProps> = ({
isOpen,
generatedCards,
cardSets,
onClose,
onSave
}) => {
const [selectedCardIndices, setSelectedCardIndices] = useState<Set<number>>(
new Set(generatedCards.map((_, index) => index)) // 預設全選
)
const [selectedCardSetId, setSelectedCardSetId] = useState<string>('')
const [isSaving, setIsSaving] = useState(false)
const selectedCount = selectedCardIndices.size
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedCardIndices(new Set(generatedCards.map((_, index) => index)))
} else {
setSelectedCardIndices(new Set())
}
}
const handleCardToggle = (index: number, checked: boolean) => {
const newSelected = new Set(selectedCardIndices)
if (checked) {
newSelected.add(index)
} else {
newSelected.delete(index)
}
setSelectedCardIndices(newSelected)
}
const handleSave = async () => {
if (selectedCount === 0) {
alert('請至少選擇一張詞卡')
return
}
setIsSaving(true)
try {
const selectedCards = Array.from(selectedCardIndices).map(index => generatedCards[index])
await onSave(selectedCards, selectedCardSetId || undefined)
} catch (error) {
console.error('Save error:', error)
alert(`保存失敗: ${error instanceof Error ? error.message : '未知錯誤'}`)
} finally {
setIsSaving(false)
}
}
const isAllSelected = selectedCount === generatedCards.length
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="選擇要保存的詞卡"
size="xl"
>
<div className="p-6 space-y-6">
{/* 操作工具列 */}
<div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-4">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={isAllSelected}
onChange={(e) => handleSelectAll(e.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium">
({selectedCount}/{generatedCards.length})
</span>
</label>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600"></span>
<select
value={selectedCardSetId}
onChange={(e) => setSelectedCardSetId(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value=""></option>
{cardSets.map(set => (
<option key={set.id} value={set.id}>
{set.name}
</option>
))}
</select>
</div>
</div>
{/* 詞卡列表 */}
<div className="space-y-3 max-h-96 overflow-y-auto">
{generatedCards.map((card, index) => (
<CardPreviewItem
key={index}
card={card}
index={index}
isSelected={selectedCardIndices.has(index)}
onToggle={(checked) => handleCardToggle(index, checked)}
/>
))}
</div>
{/* 底部操作按鈕 */}
<div className="flex justify-end space-x-3 pt-4 border-t">
<button
onClick={onClose}
disabled={isSaving}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition-colors disabled:opacity-50"
>
</button>
<button
onClick={handleSave}
disabled={selectedCount === 0 || isSaving}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</>
) : (
<>
<Check className="w-4 h-4" />
<span> {selectedCount} </span>
</>
)}
</button>
</div>
</div>
</Modal>
)
}
interface CardPreviewItemProps {
card: GeneratedCard
index: number
isSelected: boolean
onToggle: (checked: boolean) => void
}
const CardPreviewItem: React.FC<CardPreviewItemProps> = ({
card,
index,
isSelected,
onToggle
}) => {
return (
<div className={`
border rounded-lg p-4 transition-all duration-200
${isSelected
? 'border-blue-500 bg-blue-50 shadow-sm'
: 'border-gray-200 bg-white hover:border-gray-300'
}
`}>
<div className="flex items-start space-x-3">
<label className="flex items-center cursor-pointer mt-1">
<input
type="checkbox"
checked={isSelected}
onChange={(e) => onToggle(e.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
</label>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">
{card.word}
</h3>
{card.difficultyLevel && (
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 rounded">
{card.difficultyLevel}
</span>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div>
<span className="font-medium text-gray-700"></span>
<span className="text-gray-900">{card.translation}</span>
</div>
{card.partOfSpeech && (
<div>
<span className="font-medium text-gray-700"></span>
<span className="text-gray-900">{card.partOfSpeech}</span>
</div>
)}
{card.pronunciation && (
<div>
<span className="font-medium text-gray-700"></span>
<span className="text-gray-900">{card.pronunciation}</span>
</div>
)}
</div>
<div>
<span className="font-medium text-gray-700"></span>
<p className="text-gray-900 leading-relaxed">{card.definition}</p>
</div>
{card.example && (
<div>
<span className="font-medium text-gray-700"></span>
<p className="text-gray-900 italic">{card.example}</p>
{card.exampleTranslation && (
<p className="text-gray-600 text-sm mt-1">{card.exampleTranslation}</p>
)}
</div>
)}
{card.synonyms && card.synonyms.length > 0 && (
<div>
<span className="font-medium text-gray-700"></span>
<div className="flex flex-wrap gap-1 mt-1">
{card.synonyms.map((synonym, idx) => (
<span key={idx} className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
{synonym}
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}