feat: 優化詞卡管理頁面體驗
- 重新設計手機版詞卡布局,圖片放左上角,翻譯在詞彙下方 - 新增播放按鈕到詞卡列表,桌面版在音標旁,手機版在詞性旁 - 移除手機版音標顯示,精簡界面 - 調整 CEFR 和詞性標籤位置,底部左右分布更合理 - Logo 導航從儀表板改為詞卡頁面,保持導航一致性 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0ba66b6c60
commit
4866ff8e9c
|
|
@ -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">
|
||||
還沒有帳號?{' '}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
已經有帳號?{' '}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue