115 lines
3.1 KiB
TypeScript
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>
|
|
)
|
|
} |