dramaling-vocab-learning/frontend/components/flashcards/FlashcardContentBlocks.tsx

210 lines
8.8 KiB
TypeScript

import React from 'react'
import type { Flashcard } from '@/lib/services/flashcards'
import { getFlashcardImageUrl } from '@/lib/utils/flashcardUtils'
import { BluePlayButton } from '@/components/shared/BluePlayButton'
interface FlashcardContentBlocksProps {
flashcard: Flashcard
isEditing: boolean
editedCard: any
onEditChange: (field: string, value: string) => void
isGeneratingImage: boolean
generationProgress: string
onGenerateImage: () => void
}
export const FlashcardContentBlocks: React.FC<FlashcardContentBlocksProps> = ({
flashcard,
isEditing,
editedCard,
onEditChange,
isGeneratingImage,
generationProgress,
onGenerateImage
}) => {
// 安全解析同義詞 JSON 字串
const parseSynonyms = (synonymsData: any): string[] => {
if (!synonymsData) return []
if (Array.isArray(synonymsData)) return synonymsData
if (typeof synonymsData === 'string') {
try {
const parsed = JSON.parse(synonymsData)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
const synonymsList = parseSynonyms((flashcard as any).synonyms)
return (
<div className="p-6 space-y-6">
{/* 翻譯區塊 */}
<div className="bg-green-50 rounded-lg p-4 border border-green-200">
<h3 className="font-semibold text-green-900 mb-3 text-left"></h3>
{isEditing ? (
<input
type="text"
value={editedCard?.translation || ''}
onChange={(e) => onEditChange('translation', e.target.value)}
className="w-full p-3 border border-green-300 rounded-lg focus:ring-2 focus:ring-green-500 bg-white"
placeholder="輸入中文翻譯"
/>
) : (
<p className="text-green-800 font-medium text-left text-lg">
{flashcard.translation}
</p>
)}
</div>
{/* 定義區塊 */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h3 className="font-semibold text-gray-900 mb-3 text-left"></h3>
{isEditing ? (
<textarea
value={editedCard?.definition || ''}
onChange={(e) => onEditChange('definition', e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-20 resize-none"
placeholder="輸入英文定義"
/>
) : (
<p className="text-gray-700 text-left leading-relaxed">
{flashcard.definition}
</p>
)}
</div>
{/* 例句區塊 */}
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
<h3 className="font-semibold text-blue-900 mb-3 text-left"></h3>
{/* 例句圖片 */}
<div className="mb-4 relative">
{getFlashcardImageUrl(flashcard) ? (
<div className="w-full max-w-sm mx-auto">
<img
src={getFlashcardImageUrl(flashcard)!}
alt={`${flashcard.word} example`}
className="w-full aspect-square object-cover rounded-lg border border-blue-300"
style={{ imageRendering: 'auto' }}
onError={(e) => {
console.error('圖片載入失敗:', getFlashcardImageUrl(flashcard))
// 隱藏破損的圖片,顯示無圖片狀態
e.currentTarget.style.display = 'none'
const parent = e.currentTarget.parentElement?.parentElement
if (parent) {
parent.innerHTML = `
<div class="w-full max-w-md mx-auto h-48 bg-gray-100 rounded-lg border-2 border-dashed border-gray-300 flex items-center justify-center">
<div class="text-center text-gray-500">
<svg class="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p class="text-sm">圖片載入失敗</p>
<p class="text-xs text-red-500 mt-1">請檢查後端服務是否正常運行</p>
</div>
</div>
`
}
}}
/>
</div>
) : (
<div className="w-full max-w-md mx-auto h-48 bg-gray-100 rounded-lg border-2 border-dashed border-gray-300 flex items-center justify-center">
<div className="text-center text-gray-500">
<svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-sm"></p>
<button
onClick={onGenerateImage}
disabled={isGeneratingImage}
className="mt-2 px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors disabled:opacity-50"
>
{isGeneratingImage ? generationProgress : '生成圖片'}
</button>
</div>
</div>
)}
{/* 圖片上的生成按鈕 */}
{getFlashcardImageUrl(flashcard) && !isGeneratingImage && (
<button
onClick={onGenerateImage}
className="absolute top-2 right-2 px-2 py-1 text-xs bg-white bg-opacity-90 text-gray-700 rounded-md hover:bg-opacity-100 transition-all shadow-sm"
>
</button>
)}
{/* 生成進度覆蓋 */}
{isGeneratingImage && getFlashcardImageUrl(flashcard) && (
<div className="absolute inset-0 bg-white bg-opacity-90 rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
<p className="text-sm text-gray-600">{generationProgress}</p>
</div>
</div>
)}
</div>
{/* 例句內容 */}
<div className="space-y-3">
{isEditing ? (
<>
<textarea
value={editedCard?.example || ''}
onChange={(e) => onEditChange('example', e.target.value)}
className="w-full p-3 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-20 resize-none"
placeholder="輸入例句"
/>
<textarea
value={editedCard?.exampleTranslation || ''}
onChange={(e) => onEditChange('exampleTranslation', e.target.value)}
className="w-full p-3 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-16 resize-none"
placeholder="輸入例句翻譯"
/>
</>
) : (
<>
<div className="relative">
<p className="text-blue-800 text-left italic text-lg pr-12">
"{flashcard.example}"
</p>
<div className="absolute bottom-0 right-0">
<BluePlayButton
text={flashcard.example}
lang="en-US"
size="md"
title="點擊聽例句發音"
/>
</div>
</div>
<p className="text-blue-700 text-left text-base">
"{flashcard.exampleTranslation}"
</p>
</>
)}
</div>
</div>
{/* 同義詞區塊 */}
{synonymsList.length > 0 && (
<div className="bg-purple-50 rounded-lg p-4 border border-purple-200">
<h3 className="font-semibold text-purple-900 mb-3 text-left"></h3>
<div className="flex flex-wrap gap-2">
{synonymsList.map((synonym: string, index: number) => (
<span
key={index}
className="bg-white text-purple-700 px-3 py-1 rounded-full text-sm border border-purple-200 font-medium"
>
{synonym}
</span>
))}
</div>
</div>
)}
</div>
)
}