dramaling-vocab-learning/frontend/components/generate/ClickableTextV2.tsx

115 lines
3.1 KiB
TypeScript

'use client'
import { useState, useEffect, useMemo } from 'react'
import { WordPopup } from '@/components/word/WordPopup'
import { useWordAnalysis } from '@/hooks/word/useWordAnalysis'
import type { ClickableTextProps, WordAnalysis } from '@/lib/types/word'
export function ClickableTextV2({
text,
analysis,
onWordClick,
onSaveWord,
remainingUsage,
showIdiomsInline = true
}: ClickableTextProps) {
const [selectedWord, setSelectedWord] = useState<string | null>(null)
const [isSavingWord, setIsSavingWord] = useState(false)
const [mounted, setMounted] = useState(false)
const { findWordAnalysis, getWordClass, shouldShowStar } = useWordAnalysis()
useEffect(() => {
setMounted(true)
}, [])
const handleWordClick = (word: string) => {
const wordAnalysis = findWordAnalysis(word, analysis)
if (wordAnalysis) {
setSelectedWord(word)
onWordClick?.(word, wordAnalysis)
}
}
const closePopup = () => {
setSelectedWord(null)
}
const handleSaveWord = async (word: string, wordAnalysis: WordAnalysis) => {
if (!onSaveWord) return { success: false }
setIsSavingWord(true)
try {
const result = await onSaveWord(word, wordAnalysis)
if (result.success) {
closePopup()
}
return result
} finally {
setIsSavingWord(false)
}
}
const words = useMemo(() => {
const tokens = text.split(/(\s+)/)
return tokens.map((token, index) => {
const cleanToken = token.replace(/[^\w']/g, '')
if (!cleanToken || /^\s+$/.test(token)) {
return (
<span key={index} className="whitespace-pre">
{token}
</span>
)
}
const wordAnalysis = findWordAnalysis(cleanToken, analysis)
if (!wordAnalysis) {
return (
<span key={index} className="text-gray-900">
{token}
</span>
)
}
return (
<span key={index} className="relative">
<span
className={getWordClass(cleanToken, analysis)}
onClick={() => handleWordClick(cleanToken)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleWordClick(cleanToken)
}
}}
>
{token}
</span>
{shouldShowStar(wordAnalysis) && (
<span className="absolute -top-1 -right-1 text-xs text-yellow-500">
</span>
)}
</span>
)
})
}, [text, analysis, findWordAnalysis, getWordClass, shouldShowStar])
return (
<div className="relative">
<div className="text-lg leading-relaxed select-text">
{words}
</div>
<WordPopup
selectedWord={selectedWord}
analysis={analysis || {}}
isOpen={!!selectedWord && mounted}
onClose={closePopup}
onSaveWord={onSaveWord ? handleSaveWord : undefined}
isSaving={isSavingWord}
/>
</div>
)
}