150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
import React from 'react'
|
|
import { Play } from 'lucide-react'
|
|
import { Modal } from '@/components/ui/Modal'
|
|
import { ContentBlock } from '@/components/shared/ContentBlock'
|
|
import { getCEFRColor } from '@/lib/utils/flashcardUtils'
|
|
import { useWordAnalysis } from './hooks/useWordAnalysis'
|
|
import type { WordAnalysis } from './types'
|
|
|
|
interface WordPopupProps {
|
|
selectedWord: string | null
|
|
analysis: Record<string, WordAnalysis>
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
onSaveWord?: (word: string, analysis: WordAnalysis) => Promise<{ success: boolean; error?: string }>
|
|
isSaving?: boolean
|
|
}
|
|
|
|
export const WordPopup: React.FC<WordPopupProps> = ({
|
|
selectedWord,
|
|
analysis,
|
|
isOpen,
|
|
onClose,
|
|
onSaveWord,
|
|
isSaving = false
|
|
}) => {
|
|
const { getWordProperty } = useWordAnalysis()
|
|
|
|
if (!selectedWord || !analysis?.[selectedWord]) {
|
|
return null
|
|
}
|
|
|
|
const wordAnalysis = analysis[selectedWord]
|
|
|
|
const handlePlayPronunciation = () => {
|
|
const word = getWordProperty(wordAnalysis, 'word') || selectedWord
|
|
const utterance = new SpeechSynthesisUtterance(word)
|
|
utterance.lang = 'en-US'
|
|
utterance.rate = 0.8
|
|
speechSynthesis.speak(utterance)
|
|
}
|
|
|
|
const handleSaveWord = async () => {
|
|
if (onSaveWord) {
|
|
await onSaveWord(selectedWord, wordAnalysis)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} onClose={onClose} size="md">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-4 sm:p-5 border-b border-blue-200">
|
|
<div className="mb-3">
|
|
<h3 className="text-xl sm:text-2xl font-bold text-gray-900 break-words">
|
|
{getWordProperty(wordAnalysis, 'word')}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3">
|
|
<span className="text-xs sm:text-sm bg-gray-100 text-gray-700 px-2 sm:px-3 py-1 rounded-full w-fit">
|
|
{getWordProperty(wordAnalysis, 'partOfSpeech')}
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm sm:text-base text-gray-600 break-all">
|
|
{getWordProperty(wordAnalysis, 'pronunciation')}
|
|
</span>
|
|
<button
|
|
onClick={handlePlayPronunciation}
|
|
className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-600 hover:bg-blue-700 text-white transition-colors"
|
|
title="播放發音"
|
|
>
|
|
<Play size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<span className={`px-3 py-1 rounded-full text-sm font-medium border ${getCEFRColor(getWordProperty(wordAnalysis, 'cefr'))}`}>
|
|
{getWordProperty(wordAnalysis, 'cefr')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-3 sm:p-4 space-y-3 sm:space-y-4 max-h-96 overflow-y-auto">
|
|
{/* Translation */}
|
|
<ContentBlock title="中文翻譯" variant="green">
|
|
<p className="text-green-800 font-medium text-left">
|
|
{getWordProperty(wordAnalysis, 'translation')}
|
|
</p>
|
|
</ContentBlock>
|
|
|
|
{/* Definition */}
|
|
<ContentBlock title="英文定義" variant="gray">
|
|
<p className="text-gray-700 text-left text-sm leading-relaxed">
|
|
{getWordProperty(wordAnalysis, 'definition')}
|
|
</p>
|
|
</ContentBlock>
|
|
|
|
{/* Example */}
|
|
{(() => {
|
|
const example = getWordProperty(wordAnalysis, 'example')
|
|
return example && example !== 'null' && example !== 'undefined'
|
|
})() && (
|
|
<ContentBlock title="例句" variant="blue">
|
|
<div className="space-y-2">
|
|
<p className="text-blue-800 text-left text-sm italic">
|
|
"{getWordProperty(wordAnalysis, 'example')}"
|
|
</p>
|
|
<p className="text-blue-700 text-left text-sm">
|
|
{getWordProperty(wordAnalysis, 'exampleTranslation')}
|
|
</p>
|
|
</div>
|
|
</ContentBlock>
|
|
)}
|
|
|
|
{/* Synonyms */}
|
|
{(() => {
|
|
const synonyms = getWordProperty(wordAnalysis, 'synonyms')
|
|
return synonyms && Array.isArray(synonyms) && synonyms.length > 0
|
|
})() && (
|
|
<ContentBlock title="同義詞" variant="purple">
|
|
<div className="flex flex-wrap gap-2">
|
|
{getWordProperty(wordAnalysis, 'synonyms')?.map((synonym: string, index: number) => (
|
|
<span
|
|
key={index}
|
|
className="bg-purple-100 text-purple-700 px-2 py-1 rounded-full text-xs font-medium"
|
|
>
|
|
{synonym}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</ContentBlock>
|
|
)}
|
|
</div>
|
|
|
|
{/* Save Button */}
|
|
{onSaveWord && (
|
|
<div className="p-3 sm:p-4 pt-2">
|
|
<button
|
|
onClick={handleSaveWord}
|
|
disabled={isSaving}
|
|
className="w-full bg-primary text-white py-3 rounded-lg font-medium hover:bg-primary-hover transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
|
|
>
|
|
<span className="font-medium">{isSaving ? '保存中...' : '保存到詞卡'}</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
)
|
|
} |