280 lines
14 KiB
TypeScript
280 lines
14 KiB
TypeScript
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>
|
||
)
|
||
} |