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

280 lines
14 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.

import React from 'react'
import Link from 'next/link'
import { Flashcard } from '@/lib/services/flashcards'
import { getCEFRColor, getFlashcardImageUrl, getPartOfSpeechDisplay } from '@/lib/utils/flashcardUtils'
import { BluePlayButton } from '@/components/shared/BluePlayButton'
interface FlashcardCardProps {
flashcard: Flashcard
searchTerm?: string
onEdit: () => void
onDelete: () => void
onFavorite: () => void
onImageGenerate: () => void
isGenerating?: boolean
generationProgress?: string
highlightSearchTerm?: (text: string, term: string) => React.ReactNode
}
export const FlashcardCard: React.FC<FlashcardCardProps> = ({
flashcard,
searchTerm = '',
onEdit,
onDelete,
onFavorite,
onImageGenerate,
isGenerating = false,
generationProgress = '',
highlightSearchTerm = (text: string) => text
}) => {
const hasExampleImage = (card: Flashcard): boolean => {
return card.hasExampleImage || !!getFlashcardImageUrl(card)
}
const getExampleImage = (card: Flashcard): string | null => {
return getFlashcardImageUrl(card)
}
return (
<div className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all duration-200 relative overflow-hidden">
{/* CEFR標註 - 只在桌面版顯示 */}
<div className="absolute top-3 right-3 z-10 hidden md:block">
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor(flashcard.cefr || 'A1')}`}>
{flashcard.cefr || 'A1'}
</span>
</div>
{/* 手機版布局 */}
<div className="block md:hidden p-4">
{/* 頂部區域:圖片和詞彙信息 */}
<div className="flex gap-3 mb-3">
{/* 圖片區域 */}
<div className="w-16 h-16 bg-gray-100 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center flex-shrink-0">
{hasExampleImage(flashcard) ? (
<img
src={getExampleImage(flashcard)!}
alt={`${flashcard.word} example`}
className="w-full h-full object-cover"
/>
) : (
<div
className="w-full h-full flex items-center justify-center cursor-pointer hover:bg-blue-50 transition-colors group"
onClick={onImageGenerate}
title="點擊生成例句圖片"
>
{isGenerating ? (
<div className="animate-spin w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
) : (
<svg className="w-6 h-6 text-gray-400 group-hover:text-blue-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
)}
</div>
)}
</div>
{/* 詞彙信息 */}
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-900 mb-1 leading-tight">
{searchTerm ? highlightSearchTerm(flashcard.word || '未設定', searchTerm) : (flashcard.word || '未設定')}
</h3>
<p className="text-gray-900 font-medium leading-tight">
{searchTerm ? highlightSearchTerm(flashcard.translation || '未設定', searchTerm) : (flashcard.translation || '未設定')}
</p>
</div>
</div>
{/* 底部操作區域 */}
<div className="flex items-center justify-between mt-3">
{/* 左側CEFR + 詞性 + 播放按鈕 */}
<div className="flex items-center gap-2">
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor(flashcard.cefr || 'A1')}`}>
{flashcard.cefr || 'A1'}
</span>
<span className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs">
{getPartOfSpeechDisplay(flashcard.partOfSpeech)}
</span>
{flashcard.word && (
<BluePlayButton
text={flashcard.word}
lang="en-US"
size="md"
title="點擊聽詞彙發音"
className="w-6 h-6"
/>
)}
</div>
{/* 右側:操作按鈕 */}
<div className="flex items-center gap-1">
<button
onClick={onFavorite}
className={`p-2 rounded-lg transition-colors ${
flashcard.isFavorite ? 'text-yellow-600 bg-yellow-50' : 'text-gray-400 hover:text-yellow-600 hover:bg-yellow-50'
}`}
>
<svg className="w-5 h-5" fill={flashcard.isFavorite ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
</button>
<button
onClick={onEdit}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={onDelete}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<Link
href={`/flashcards/${flashcard.id}`}
className="p-2 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
</div>
</div>
</div>
{/* 桌面版布局 */}
<div className="hidden md:block p-4">
<div className="flex items-center gap-4">
{/* 圖片區域 */}
<div className="w-32 h-32 lg:w-36 lg:h-36 bg-gray-100 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center flex-shrink-0">
{hasExampleImage(flashcard) ? (
<img
src={getExampleImage(flashcard)!}
alt={`${flashcard.word} example`}
className="w-full h-full object-cover"
style={{ imageRendering: 'auto' }}
/>
) : (
<div
className="w-full h-full flex items-center justify-center cursor-pointer hover:bg-blue-50 transition-colors group"
onClick={onImageGenerate}
title="點擊生成例句圖片"
>
{isGenerating ? (
<div className="text-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto mb-1"></div>
<span className="text-xs text-blue-600">{generationProgress}</span>
</div>
) : (
<div className="text-center">
<svg className="w-8 h-8 mx-auto mb-2 text-gray-400 group-hover:text-blue-600 group-hover:scale-110 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<span className="text-xs text-gray-500 group-hover:text-blue-600 transition-colors"></span>
</div>
)}
</div>
)}
</div>
{/* 詞卡信息 */}
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">
{searchTerm ? highlightSearchTerm(flashcard.word || '未設定', searchTerm) : (flashcard.word || '未設定')}
</h3>
<span className="text-sm bg-gray-100 text-gray-700 px-2 py-1 rounded">
{getPartOfSpeechDisplay(flashcard.partOfSpeech)}
</span>
</div>
<div className="flex items-center gap-4 mt-1">
<span className="text-lg text-gray-900 font-medium">
{searchTerm ? highlightSearchTerm(flashcard.translation || '未設定', searchTerm) : (flashcard.translation || '未設定')}
</span>
{flashcard.pronunciation && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{flashcard.pronunciation}</span>
{flashcard.word && (
<BluePlayButton
text={flashcard.word}
lang="en-US"
size="md"
title="點擊聽詞彙發音"
className="w-6 h-6"
/>
)}
</div>
)}
</div>
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<span>: {new Date(flashcard.createdAt).toLocaleDateString()}</span>
</div>
</div>
{/* 桌面版操作按鈕 */}
<div className="flex items-center gap-2">
<button
onClick={onFavorite}
className={`px-3 py-2 rounded-lg font-medium transition-colors ${
flashcard.isFavorite
? 'bg-yellow-100 text-yellow-700 border border-yellow-300 hover:bg-yellow-200'
: 'bg-gray-100 text-gray-600 border border-gray-300 hover:bg-yellow-50 hover:text-yellow-600 hover:border-yellow-300'
}`}
>
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill={flashcard.isFavorite ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
<span className="text-sm">{flashcard.isFavorite ? '已收藏' : '收藏'}</span>
</div>
</button>
<button
onClick={onEdit}
className="px-3 py-2 bg-blue-100 text-blue-700 border border-blue-300 rounded-lg font-medium hover:bg-blue-200 transition-colors"
>
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<span className="text-sm"></span>
</div>
</button>
<button
onClick={onDelete}
className="px-3 py-2 bg-red-100 text-red-700 border border-red-300 rounded-lg font-medium hover:bg-red-200 transition-colors"
>
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span className="text-sm"></span>
</div>
</button>
<Link
href={`/flashcards/${flashcard.id}`}
className="px-4 py-2 bg-gray-100 text-gray-700 border border-gray-300 rounded-lg font-medium hover:bg-gray-200 hover:text-gray-900 transition-colors"
>
<div className="flex items-center gap-1">
<span className="text-sm"></span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</Link>
</div>
</div>
</div>
</div>
)
}