feat: 優化詞卡管理頁面體驗

- 重新設計手機版詞卡布局,圖片放左上角,翻譯在詞彙下方
- 新增播放按鈕到詞卡列表,桌面版在音標旁,手機版在詞性旁
- 移除手機版音標顯示,精簡界面
- 調整 CEFR 和詞性標籤位置,底部左右分布更合理
- Logo 導航從儀表板改為詞卡頁面,保持導航一致性

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-10-08 01:11:53 +08:00
parent 0ba66b6c60
commit 4866ff8e9c
4 changed files with 91 additions and 57 deletions

View File

@ -104,9 +104,9 @@ export default function LoginPage() {
/>
<span className="ml-2 text-sm text-gray-600"></span>
</label>
<Link href="/forgot-password" className="text-sm text-primary hover:text-primary-hover">
{/* <Link href="/forgot-password" className="text-sm text-primary hover:text-primary-hover">
</Link>
</Link> */}
</div>
<button
@ -118,7 +118,7 @@ export default function LoginPage() {
</button>
</form>
<div className="mt-6">
{/* <div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
@ -141,7 +141,7 @@ export default function LoginPage() {
</svg>
使 Google
</button>
</div>
</div> */}
<p className="mt-8 text-center text-sm text-gray-600">
{' '}

View File

@ -206,7 +206,7 @@ export default function RegisterPage() {
</button>
</form>
<div className="mt-6">
{/* <div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
@ -229,7 +229,7 @@ export default function RegisterPage() {
</svg>
使 Google
</button>
</div>
</div> */}
<p className="mt-8 text-center text-sm text-gray-600">
{' '}

View File

@ -2,6 +2,7 @@ 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
@ -36,8 +37,8 @@ export const FlashcardCard: React.FC<FlashcardCardProps> = ({
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">
{/* 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>
@ -45,26 +46,66 @@ export const FlashcardCard: React.FC<FlashcardCardProps> = ({
{/* 手機版布局 */}
<div className="block md:hidden p-4">
{/* 主要內容區 */}
<div className="pr-14 mb-4">
<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 mb-2 leading-tight">
{searchTerm ? highlightSearchTerm(flashcard.translation || '未設定', searchTerm) : (flashcard.translation || '未設定')}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-500">
<span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
{getPartOfSpeechDisplay(flashcard.partOfSpeech)}
</span>
{flashcard.pronunciation && (
<span>{flashcard.pronunciation}</span>
{/* 頂部區域:圖片和詞彙信息 */}
<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">
{/* 底部操作區域 */}
<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}
@ -86,33 +127,6 @@ export const FlashcardCard: React.FC<FlashcardCardProps> = ({
</svg>
</button>
{hasExampleImage(flashcard) ? (
<div className="w-8 h-8 bg-gray-100 rounded overflow-hidden">
<img src={getExampleImage(flashcard)!} alt="" className="w-full h-full object-cover" />
</div>
) : (
<button
onClick={onImageGenerate}
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
{isGenerating ? (
<div className="animate-spin w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full"></div>
) : (
<svg className="w-5 h-5" 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>
)}
</button>
)}
</div>
<div className="flex items-center gap-2">
<Link
href={`/flashcards/${flashcard.id}`}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded text-sm hover:bg-gray-200 transition-colors"
>
</Link>
<button
onClick={onDelete}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
@ -121,6 +135,15 @@ export const FlashcardCard: React.FC<FlashcardCardProps> = ({
<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>
@ -176,7 +199,18 @@ export const FlashcardCard: React.FC<FlashcardCardProps> = ({
{searchTerm ? highlightSearchTerm(flashcard.translation || '未設定', searchTerm) : (flashcard.translation || '未設定')}
</span>
{flashcard.pronunciation && (
<span className="text-sm text-gray-500">{flashcard.pronunciation}</span>
<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>

View File

@ -41,7 +41,7 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat
</svg>
</button>
<Link href="/dashboard" className="text-2xl font-bold text-primary">
<Link href="/flashcards" className="text-2xl font-bold text-primary">
DramaLing
</Link>
@ -91,12 +91,12 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat
</div>
<span className="text-sm font-medium text-gray-700">{user?.displayName || user?.username}</span>
</Link>
<button
{/* <button
onClick={logout}
className="text-sm text-gray-600 hover:text-gray-900 px-2 py-1 hover:bg-gray-100 rounded"
>
</button>
</button> */}
</div>
</>
)}
@ -134,7 +134,7 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat
</div>
<span className="text-sm font-medium">{user?.username}</span>
</Link>
<button
{/* <button
onClick={() => {
logout()
setIsMobileMenuOpen(false)
@ -142,7 +142,7 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat
className="w-full text-left py-2 px-3 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg"
>
</button>
</button> */}
</div>
</div>
</div>