dramaling-vocab-learning/note/learn-backup/page-v1-original.tsx

2429 lines
92 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.

'use client'
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Navigation } from '@/components/Navigation'
import AudioPlayer from '@/components/AudioPlayer'
import VoiceRecorder from '@/components/VoiceRecorder'
import LearningComplete from '@/components/LearningComplete'
import ReviewTypeIndicator from '@/components/review/ReviewTypeIndicator'
import MasteryIndicator from '@/components/review/MasteryIndicator'
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
import { calculateCurrentMastery, getReviewTypesByDifficulty } from '@/lib/utils/masteryCalculator'
// 測驗項目接口
interface TestItem {
id: string; // 唯一ID: cardId + testType
cardId: string; // 所屬詞卡ID
word: string; // 詞卡單字
testType: string; // 測驗類型 (flip-memory, vocab-choice, etc.)
testName: string; // 測驗中文名稱
isCompleted: boolean; // 是否已完成
isCurrent: boolean; // 是否為當前測驗
order: number; // 執行順序 (1-8)
}
// 詞卡測驗分組接口
interface CardTestGroup {
cardId: string;
word: string;
context: string;
tests: TestItem[];
}
// 擴展的Flashcard接口包含智能複習需要的欄位
interface ExtendedFlashcard extends Omit<Flashcard, 'nextReviewDate'> {
nextReviewDate?: string; // 下次復習日期 (可選)
currentInterval?: number; // 當前間隔天數
isOverdue?: boolean; // 是否逾期
overdueDays?: number; // 逾期天數
baseMasteryLevel?: number; // 基礎熟悉度
lastReviewDate?: string; // 最後復習日期
synonyms?: string[]; // 同義詞
exampleImage?: string; // 例句圖片
// 注意userLevel和wordLevel已移除改用即時CEFR轉換
}
// 單個測驗結果接口
interface TestResult {
testType: string; // 測驗類型
isCorrect: boolean; // 是否正確
userAnswer?: string; // 用戶答案
confidenceLevel?: number; // 信心等級 (1-5, 用於flip-memory)
responseTimeMs: number; // 答題時間
completedAt: Date; // 完成時間
}
// 詞卡複習會話接口
interface CardReviewSession {
cardId: string; // 詞卡ID
word: string; // 詞卡單字
plannedTests: string[]; // 預定的測驗類型列表
completedTests: TestResult[]; // 已完成的測驗結果
startedAt: Date; // 開始時間
isCompleted: boolean; // 是否完成所有測驗
}
export default function LearnPage() {
const router = useRouter()
const [mounted, setMounted] = useState(false)
// 智能複習狀態
const [currentCard, setCurrentCard] = useState<ExtendedFlashcard | null>(null)
const [dueCards, setDueCards] = useState<ExtendedFlashcard[]>([])
const [currentCardIndex, setCurrentCardIndex] = useState(0)
const [isLoadingCard, setIsLoadingCard] = useState(false)
// 複習模式狀態 (系統自動選擇)
const [mode, setMode] = useState<'flip-memory' | 'vocab-choice' | 'vocab-listening' | 'sentence-listening' | 'sentence-fill' | 'sentence-reorder' | 'sentence-speaking'>('flip-memory')
const [isAutoSelecting, setIsAutoSelecting] = useState(true)
// 答題狀態
const [score, setScore] = useState({ correct: 0, total: 0 })
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
const [showResult, setShowResult] = useState(false)
const [fillAnswer, setFillAnswer] = useState('')
const [showHint, setShowHint] = useState(false)
const [isFlipped, setIsFlipped] = useState(false)
// 測驗進度狀態
const [totalTests, setTotalTests] = useState(0) // 所有測驗總數
const [completedTests, setCompletedTests] = useState(0) // 已完成測驗數
const [testItems, setTestItems] = useState<TestItem[]>([]) // 測驗項目列表
const [currentTestItemIndex, setCurrentTestItemIndex] = useState(0) // 當前測驗項目索引
// 詞卡複習會話狀態
const [cardReviewSessions, setCardReviewSessions] = useState<Map<string, CardReviewSession>>(new Map())
const [currentCardSession, setCurrentCardSession] = useState<CardReviewSession | null>(null)
const [completedCards, setCompletedCards] = useState(0) // 已完成復習的詞卡數
// UI狀態
const [modalImage, setModalImage] = useState<string | null>(null)
const [showReportModal, setShowReportModal] = useState(false)
const [reportReason, setReportReason] = useState('')
const [reportingCard, setReportingCard] = useState<any>(null)
const [showComplete, setShowComplete] = useState(false)
const [showNoDueCards, setShowNoDueCards] = useState(false)
const [showTaskListModal, setShowTaskListModal] = useState(false)
const [cardHeight, setCardHeight] = useState<number>(400)
// 題型特定狀態
const [quizOptions, setQuizOptions] = useState<string[]>([])
const [sentenceOptions, setSentenceOptions] = useState<string[]>([])
// 例句重組狀態
const [shuffledWords, setShuffledWords] = useState<string[]>([])
const [arrangedWords, setArrangedWords] = useState<string[]>([])
const [reorderResult, setReorderResult] = useState<boolean | null>(null)
// Refs for measuring card content heights
const cardFrontRef = useRef<HTMLDivElement>(null)
const cardBackRef = useRef<HTMLDivElement>(null)
const cardContainerRef = useRef<HTMLDivElement>(null)
// Calculate optimal card height based on content (only when card changes)
const calculateCardHeight = () => {
if (!cardFrontRef.current || !cardBackRef.current) return 400;
// Get the scroll heights to measure actual content
const frontHeight = cardFrontRef.current.scrollHeight;
const backHeight = cardBackRef.current.scrollHeight;
console.log('Heights calculated:', { frontHeight, backHeight }); // Debug log
// Use the maximum height with padding
const maxHeight = Math.max(frontHeight, backHeight);
const paddedHeight = maxHeight + 40; // Add padding for visual spacing
// Ensure minimum height for visual consistency
return Math.max(paddedHeight, 450);
};
// Update card height only when card content changes (not on flip)
useLayoutEffect(() => {
if (mounted && cardFrontRef.current && cardBackRef.current) {
// Wait for DOM to be fully rendered
const timer = setTimeout(() => {
const newHeight = calculateCardHeight();
setCardHeight(newHeight);
}, 50);
return () => clearTimeout(timer);
}
}, [currentCardIndex, mounted]);
// Client-side mounting
useEffect(() => {
setMounted(true)
loadDueCards() // 載入到期詞卡
}, [])
// 載入到期詞卡列表
const loadDueCards = async () => {
try {
setIsLoadingCard(true)
console.log('🔍 開始載入到期詞卡...');
// 完全使用後端API數據
const apiResult = await flashcardsService.getDueFlashcards(50);
console.log('📡 API回應結果:', apiResult);
if (apiResult.success && apiResult.data && apiResult.data.length > 0) {
const cardsToUse = apiResult.data;
console.log('✅ 載入後端API數據成功:', cardsToUse.length, '張詞卡');
console.log('📋 詞卡列表:', cardsToUse.map(c => c.word));
// 查詢已完成的測驗
const cardIds = cardsToUse.map(c => c.id);
let completedTests: any[] = [];
try {
const completedTestsResult = await flashcardsService.getCompletedTests(cardIds);
if (completedTestsResult.success && completedTestsResult.data) {
completedTests = completedTestsResult.data;
console.log('📊 已完成測驗:', completedTests.length, '個');
} else {
console.log('⚠️ 查詢已完成測驗失敗,使用空列表:', completedTestsResult.error);
}
} catch (error) {
console.error('💥 查詢已完成測驗異常:', error);
console.log('📝 繼續使用空的已完成測驗列表');
}
// 計算每張詞卡剩餘的測驗
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
let remainingTestItems: TestItem[] = [];
let order = 1;
cardsToUse.forEach(card => {
const wordCEFR = card.difficultyLevel || 'A2';
const allTestTypes = getReviewTypesByCEFR(userCEFR, wordCEFR);
// 找出該詞卡已完成的測驗類型
const completedTestTypes = completedTests
.filter(ct => ct.flashcardId === card.id)
.map(ct => ct.testType);
// 計算剩餘未完成的測驗類型
const remainingTestTypes = allTestTypes.filter(testType =>
!completedTestTypes.includes(testType)
);
console.log(`🎯 詞卡 ${card.word}: 總共${allTestTypes.length}個測驗, 已完成${completedTestTypes.length}個, 剩餘${remainingTestTypes.length}`);
// 為剩餘的測驗創建測驗項目
remainingTestTypes.forEach(testType => {
remainingTestItems.push({
id: `${card.id}-${testType}`,
cardId: card.id,
word: card.word,
testType,
testName: getModeLabel(testType),
isCompleted: false,
isCurrent: false,
order
});
order++;
});
});
if (remainingTestItems.length === 0) {
console.log('🎉 所有測驗都已完成!');
setShowComplete(true);
return;
}
console.log('📝 剩餘測驗項目:', remainingTestItems.length, '個');
// 設置狀態
setTotalTests(remainingTestItems.length);
setTestItems(remainingTestItems);
setCurrentTestItemIndex(0);
setCompletedTests(0);
// 找到第一個測驗項目對應的詞卡
const firstTestItem = remainingTestItems[0];
const firstCard = cardsToUse.find(c => c.id === firstTestItem.cardId);
if (firstCard && firstTestItem) {
setCurrentCard(firstCard);
setCurrentCardIndex(cardsToUse.findIndex(c => c.id === firstCard.id));
// 設置測驗模式為第一個測驗的類型
const modeMapping: { [key: string]: typeof mode } = {
'flip-memory': 'flip-memory',
'vocab-choice': 'vocab-choice',
'vocab-listening': 'vocab-listening',
'sentence-fill': 'sentence-fill',
'sentence-reorder': 'sentence-reorder',
'sentence-speaking': 'sentence-speaking',
'sentence-listening': 'sentence-listening'
};
const selectedMode = modeMapping[firstTestItem.testType] || 'flip-memory';
setMode(selectedMode);
setIsAutoSelecting(false);
// 標記第一個測驗項目為當前狀態
setTestItems(prev =>
prev.map((item, index) =>
index === 0
? { ...item, isCurrent: true }
: item
)
);
console.log(`🎯 恢復到未完成測驗: ${firstCard.word} - ${firstTestItem.testType}`);
}
} else {
// 沒有到期詞卡
console.log('❌ API回應:', {
success: apiResult.success,
dataExists: !!apiResult.data,
dataLength: apiResult.data?.length,
error: apiResult.error
});
setDueCards([]);
setCurrentCard(null);
setShowNoDueCards(true);
}
} catch (error) {
console.error('💥 載入到期詞卡失敗:', error);
setDueCards([]);
setCurrentCard(null);
setShowNoDueCards(true);
} finally {
setIsLoadingCard(false);
}
}
// 智能載入下一張卡片並自動選擇模式
const loadNextCardWithAutoMode = async (cardIndex: number) => {
try {
setIsAutoSelecting(true);
// 等待dueCards載入完成
if (dueCards.length === 0) {
console.log('等待詞卡載入...');
return;
}
const card = dueCards[cardIndex];
if (!card) {
setShowComplete(true);
setIsAutoSelecting(false);
return;
}
setCurrentCard(card);
setCurrentCardIndex(cardIndex);
// 系統自動選擇最適合的複習模式
const selectedMode = await selectOptimalReviewMode(card);
setMode(selectedMode);
// 重置所有答題狀態
resetAllStates();
console.log(`載入卡片: ${card.word}, 選擇模式: ${selectedMode}`);
} catch (error) {
console.error('載入卡片失敗:', error);
} finally {
setIsAutoSelecting(false);
}
}
// 系統自動選擇最適合的複習模式
const selectOptimalReviewMode = async (card: ExtendedFlashcard): Promise<typeof mode> => {
try {
// 使用CEFR字符串進行智能選擇
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFRLevel = card.difficultyLevel || 'A2';
console.log(`CEFR智能選擇: 用戶${userCEFRLevel} vs 詞彙${wordCEFRLevel}`);
const apiResult = await flashcardsService.getOptimalReviewMode(card.id, userCEFRLevel, wordCEFRLevel);
if (apiResult.success && apiResult.data?.selectedMode) {
const selectedMode = apiResult.data.selectedMode;
console.log(`後端智能選擇: ${selectedMode}`);
// 映射到前端模式名稱
const modeMapping: { [key: string]: typeof mode } = {
'flip-memory': 'flip-memory',
'vocab-choice': 'vocab-choice',
'vocab-listening': 'vocab-listening',
'sentence-fill': 'sentence-fill',
'sentence-reorder': 'sentence-reorder',
'sentence-speaking': 'sentence-speaking',
'sentence-listening': 'sentence-listening'
};
return modeMapping[selectedMode] || 'flip-memory';
} else {
console.log('後端API失敗使用前端邏輯');
}
} catch (error) {
console.error('智能選擇API錯誤:', error);
}
// 備用: 使用前端CEFR邏輯
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFRLevel = card.difficultyLevel || 'A2';
const availableModes = getReviewTypesByCEFR(userCEFRLevel, wordCEFRLevel);
const modeMapping: { [key: string]: typeof mode } = {
'flip-memory': 'flip-memory',
'vocab-choice': 'vocab-choice',
'vocab-listening': 'vocab-listening',
'sentence-fill': 'sentence-fill',
'sentence-reorder': 'sentence-reorder',
'sentence-speaking': 'sentence-speaking',
'sentence-listening': 'sentence-listening'
};
const selectedType = availableModes[0] || 'flip-memory';
console.log(`前端CEFR邏輯選擇: ${selectedType}`);
return modeMapping[selectedType] || 'flip-memory';
}
// 前端CEFR備用選擇邏輯
const getReviewTypesByCEFR = (userCEFR: string, wordCEFR: string): string[] => {
const userLevel = getCEFRToLevel(userCEFR);
const wordLevel = getCEFRToLevel(wordCEFR);
const difficulty = wordLevel - userLevel;
if (userCEFR === 'A1') {
return ['flip-memory', 'vocab-choice', 'vocab-listening'];
} else if (difficulty < -10) {
return ['sentence-reorder', 'sentence-fill'];
} else if (difficulty >= -10 && difficulty <= 10) {
return ['sentence-fill', 'sentence-reorder', 'sentence-speaking'];
} else {
return ['flip-memory', 'vocab-choice'];
}
}
// CEFR轉換為數值 (前端計算用)
const getCEFRToLevel = (cefr: string): number => {
const mapping: { [key: string]: number } = {
'A1': 20, 'A2': 35, 'B1': 50, 'B2': 65, 'C1': 80, 'C2': 95
};
return mapping[cefr] || 50;
}
// 計算每張詞卡的測驗數量
const calculateTestsForCard = (userCEFR: string, wordCEFR: string): number => {
const userLevel = getCEFRToLevel(userCEFR);
const wordLevel = getCEFRToLevel(wordCEFR);
const difficulty = wordLevel - userLevel;
if (userCEFR === 'A1') {
return 3; // A1學習者翻卡、選擇、聽力
} else if (difficulty < -10) {
return 2; // 簡單詞彙:填空、重組
} else if (difficulty >= -10 && difficulty <= 10) {
return 3; // 適中詞彙:填空、重組、口說
} else {
return 2; // 困難詞彙:翻卡、選擇
}
}
// 取得當前學習情境
const getCurrentContext = (userCEFR: string, wordCEFR: string): string => {
const userLevel = getCEFRToLevel(userCEFR);
const wordLevel = getCEFRToLevel(wordCEFR);
const difficulty = wordLevel - userLevel;
if (userCEFR === 'A1') return 'A1學習者';
if (difficulty < -10) return '簡單詞彙';
if (difficulty >= -10 && difficulty <= 10) return '適中詞彙';
return '困難詞彙';
}
// 生成完整四情境對照表數據
const generateContextTable = (currentUserCEFR: string, currentWordCEFR: string) => {
const currentContext = getCurrentContext(currentUserCEFR, currentWordCEFR);
const contexts = [
{
type: 'A1學習者',
icon: '🛡️',
reviewTypes: ['🔄翻卡記憶', '✅詞彙選擇', '🎧詞彙聽力'],
purpose: '建立基礎信心',
condition: '用戶等級 = A1',
description: '初學者保護機制使用最基礎的3種題型'
},
{
type: '簡單詞彙',
icon: '🎯',
reviewTypes: ['✏️例句填空', '🔀例句重組'],
purpose: '應用練習',
condition: '用戶等級 > 詞彙等級',
description: '詞彙對您較簡單,重點練習拼寫和語法應用'
},
{
type: '適中詞彙',
icon: '⚖️',
reviewTypes: ['✏️例句填空', '🔀例句重組', '🗣️例句口說'],
purpose: '全方位練習',
condition: '用戶等級 ≈ 詞彙等級',
description: '詞彙難度適中,進行聽說讀寫全方位練習'
},
{
type: '困難詞彙',
icon: '📚',
reviewTypes: ['🔄翻卡記憶', '✅詞彙選擇'],
purpose: '基礎重建',
condition: '用戶等級 < 詞彙等級',
description: '詞彙對您較困難,回歸基礎重建記憶'
}
];
return contexts.map(context => ({
...context,
isCurrent: context.type === currentContext
}));
}
// 取得題型圖標
const getModeIcon = (mode: string): string => {
const icons: { [key: string]: string } = {
'flip-memory': '🔄',
'vocab-choice': '✅',
'vocab-listening': '🎧',
'sentence-listening': '👂',
'sentence-fill': '✏️',
'sentence-reorder': '🔀',
'sentence-speaking': '🗣️'
};
return icons[mode] || '📝';
}
// 取得題型中文名稱
const getModeLabel = (mode: string): string => {
const labels: { [key: string]: string } = {
'flip-memory': '翻卡記憶',
'vocab-choice': '詞彙選擇',
'vocab-listening': '詞彙聽力',
'sentence-listening': '例句聽力',
'sentence-fill': '例句填空',
'sentence-reorder': '例句重組',
'sentence-speaking': '例句口說'
};
return labels[mode] || mode;
}
// 生成測驗項目列表
const generateTestItems = (cards: ExtendedFlashcard[], userCEFR: string): TestItem[] => {
const items: TestItem[] = [];
let order = 1;
cards.forEach(card => {
const wordCEFR = card.difficultyLevel || 'A2';
const testTypes = getReviewTypesByCEFR(userCEFR, wordCEFR);
testTypes.forEach(testType => {
items.push({
id: `${card.id}-${testType}`,
cardId: card.id,
word: card.word,
testType,
testName: getModeLabel(testType),
isCompleted: false,
isCurrent: false,
order
});
order++;
});
});
return items;
}
// 按詞卡分組測驗項目
const groupTestItemsByCard = (items: TestItem[]): CardTestGroup[] => {
const grouped = items.reduce((acc, item) => {
const cardId = item.cardId;
if (!acc[cardId]) {
const card = dueCards.find(c => c.id === cardId);
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFR = card?.difficultyLevel || 'A2';
acc[cardId] = {
cardId,
word: item.word,
context: getCurrentContext(userCEFR, wordCEFR),
tests: []
};
}
acc[cardId].tests.push(item);
return acc;
}, {} as Record<string, CardTestGroup>);
return Object.values(grouped);
}
// 初始化詞卡複習會話
const initializeCardReviewSession = (card: ExtendedFlashcard): CardReviewSession => {
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
const wordCEFR = card.difficultyLevel || 'A2';
const plannedTests = getReviewTypesByCEFR(userCEFR, wordCEFR);
return {
cardId: card.id,
word: card.word,
plannedTests,
completedTests: [],
startedAt: new Date(),
isCompleted: false
};
}
// 開始詞卡複習會話
const startCardReviewSession = (card: ExtendedFlashcard) => {
const session = initializeCardReviewSession(card);
setCurrentCardSession(session);
// 更新會話映射
setCardReviewSessions(prev => new Map(prev.set(card.id, session)));
console.log(`🎯 開始詞卡複習會話: ${card.word}`, {
plannedTests: session.plannedTests,
totalTests: session.plannedTests.length
});
}
// 檢查詞卡是否已完成所有測驗
const isCardReviewCompleted = (cardId: string): boolean => {
const session = cardReviewSessions.get(cardId);
return session?.isCompleted || false;
}
// 獲取詞卡的下一個測驗類型
const getNextTestTypeForCard = (cardId: string): string | null => {
const session = cardReviewSessions.get(cardId);
if (!session) return null;
const completedTestTypes = session.completedTests.map(t => t.testType);
const nextTestType = session.plannedTests.find(testType =>
!completedTestTypes.includes(testType)
);
return nextTestType || null;
}
// 重置所有答題狀態
const resetAllStates = () => {
setIsFlipped(false);
setSelectedAnswer(null);
setShowResult(false);
setFillAnswer('');
setShowHint(false);
setShuffledWords([]);
setArrangedWords([]);
setReorderResult(null);
setQuizOptions([]);
}
// Quiz options generation
useEffect(() => {
if (!currentCard) return;
const currentWord = currentCard.word;
// Generate quiz options with current word and other words
const otherWords = dueCards
.filter(card => card.id !== currentCard.id)
.map(card => card.word);
// 優先從其他詞卡生成選項,必要時使用備用詞彙
const selectedOtherWords: string[] = [];
// 從其他詞卡取得選項
for (const word of otherWords) {
if (selectedOtherWords.length >= 3) break;
if (word !== currentWord && !selectedOtherWords.includes(word)) {
selectedOtherWords.push(word);
}
}
// 如果詞卡不足,補充基礎詞彙
if (selectedOtherWords.length < 3) {
const backupWords = ['important', 'beautiful', 'interesting', 'difficult', 'wonderful', 'excellent'];
for (const word of backupWords) {
if (selectedOtherWords.length >= 3) break;
if (word !== currentWord && !selectedOtherWords.includes(word)) {
selectedOtherWords.push(word);
}
}
}
// 確保有4個選項當前詞彙 + 3個其他選項
const options = [currentWord, ...selectedOtherWords.slice(0, 3)].sort(() => Math.random() - 0.5);
setQuizOptions(options);
// Reset quiz state when card changes
setSelectedAnswer(null);
setShowResult(false);
}, [currentCard, dueCards])
// Sentence options generation for sentence listening
useEffect(() => {
if (!currentCard || mode !== 'sentence-listening') return;
const currentSentence = currentCard.example;
// Generate sentence options with current sentence and other sentences
const otherSentences = dueCards
.filter(card => card.id !== currentCard.id)
.map(card => card.example);
// 優先從其他詞卡的例句生成選項
const selectedOtherSentences: string[] = [];
// 從其他詞卡的例句取得選項
for (const sentence of otherSentences) {
if (selectedOtherSentences.length >= 3) break;
if (sentence !== currentSentence && !selectedOtherSentences.includes(sentence)) {
selectedOtherSentences.push(sentence);
}
}
// 如果例句不足,使用基礎例句補充
if (selectedOtherSentences.length < 3) {
const backupSentences = [
'This is a very important decision.',
'The weather looks beautiful today.',
'We need to find a good solution.',
'Learning English can be interesting.'
];
for (const sentence of backupSentences) {
if (selectedOtherSentences.length >= 3) break;
if (sentence !== currentSentence && !selectedOtherSentences.includes(sentence)) {
selectedOtherSentences.push(sentence);
}
}
}
// 確保有4個選項當前例句 + 3個其他選項
const options = [currentSentence, ...selectedOtherSentences.slice(0, 3)].sort(() => Math.random() - 0.5);
setSentenceOptions(options);
}, [currentCard, dueCards, mode])
// Initialize sentence reorder when card changes or mode switches to sentence-reorder
useEffect(() => {
if (mode === 'sentence-reorder' && currentCard) {
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
// 只在卡片或模式切換時重置結果,不在其他狀態變化時重置
if (reorderResult !== null) {
setReorderResult(null)
}
}
}, [currentCard, mode]) // 移除reorderResult依賴避免循環重置
// Sentence reorder handlers
const handleWordClick = (word: string) => {
// Move word from shuffled to arranged
setShuffledWords(prev => prev.filter(w => w !== word))
setArrangedWords(prev => [...prev, word])
setReorderResult(null)
}
const handleRemoveFromArranged = (word: string) => {
setArrangedWords(prev => prev.filter(w => w !== word))
setShuffledWords(prev => [...prev, word])
setReorderResult(null)
}
const handleCheckReorderAnswer = async () => {
if (!currentCard) return;
const userSentence = arrangedWords.join(' ')
const correctSentence = currentCard.example
const isCorrect = userSentence.toLowerCase().trim() === correctSentence.toLowerCase().trim()
setReorderResult(isCorrect)
// 更新分數
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 記錄測驗結果
await recordTestResult(isCorrect, userSentence);
}
const handleResetReorder = () => {
if (!currentCard) return;
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
setReorderResult(null)
}
const handleFlip = () => {
setIsFlipped(!isFlipped)
}
// 移動到下一個測驗或下一張詞卡
const handleNext = async () => {
if (!currentCard || !currentCardSession) return;
// 檢查當前詞卡是否還有未完成的測驗
const nextTestType = getNextTestTypeForCard(currentCard.id);
if (nextTestType) {
// 當前詞卡還有測驗未完成,切換到下一個測驗類型
const modeMapping: { [key: string]: typeof mode } = {
'flip-memory': 'flip-memory',
'vocab-choice': 'vocab-choice',
'vocab-listening': 'vocab-listening',
'sentence-fill': 'sentence-fill',
'sentence-reorder': 'sentence-reorder',
'sentence-speaking': 'sentence-speaking',
'sentence-listening': 'sentence-listening'
};
const nextMode = modeMapping[nextTestType] || 'flip-memory';
setMode(nextMode);
resetAllStates();
// 更新測驗項目的當前狀態
setTestItems(prev =>
prev.map(item =>
item.cardId === currentCard.id && item.testType === nextTestType
? { ...item, isCurrent: true }
: { ...item, isCurrent: false }
)
);
console.log(`🔄 切換到下一個測驗: ${nextTestType} for ${currentCard.word}`);
} else {
// 當前詞卡的所有測驗都已完成,移動到下一張詞卡
if (currentCardIndex < dueCards.length - 1) {
const nextCardIndex = currentCardIndex + 1;
const nextCard = dueCards[nextCardIndex];
// 開始新詞卡的複習會話
startCardReviewSession(nextCard);
await loadNextCardWithAutoMode(nextCardIndex);
console.log(`➡️ 移動到下一張詞卡: ${nextCard.word}`);
} else {
// 所有詞卡都已完成
setShowComplete(true);
console.log(`🎉 所有詞卡復習完成!`);
}
}
}
const handlePrevious = async () => {
// 暫時保持簡單的向前導航
if (currentCardIndex > 0) {
await loadNextCardWithAutoMode(currentCardIndex - 1);
}
}
const handleQuizAnswer = async (answer: string) => {
if (showResult || !currentCard) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.word
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 記錄測驗結果到資料庫
await recordTestResult(isCorrect, answer);
}
// 記錄測驗結果到資料庫(立即保存)
const recordTestResult = async (isCorrect: boolean, userAnswer?: string, confidenceLevel?: number) => {
if (!currentCard) return;
// 檢查認證狀態
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('❌ 未找到認證token請重新登入');
return;
}
try {
console.log('🔄 開始記錄測驗結果到資料庫...', {
flashcardId: currentCard.id,
testType: mode,
word: currentCard.word,
isCorrect,
hasToken: !!token
});
// 立即記錄到資料庫
const result = await flashcardsService.recordTestCompletion({
flashcardId: currentCard.id,
testType: mode,
isCorrect,
userAnswer,
confidenceLevel,
responseTimeMs: 2000
});
if (result.success) {
console.log('✅ 測驗結果已記錄到資料庫:', mode, 'for', currentCard.word);
// 更新本地UI狀態
setCompletedTests(prev => prev + 1);
// 標記當前測驗項目為完成
setTestItems(prev =>
prev.map((item, index) =>
index === currentTestItemIndex
? { ...item, isCompleted: true, isCurrent: false }
: item
)
);
// 移到下一個測驗項目
setCurrentTestItemIndex(prev => prev + 1);
// 檢查是否還有剩餘測驗
setTimeout(() => {
loadNextUncompletedTest();
}, 1500);
} else {
console.error('❌ 記錄測驗結果失敗:', result.error);
if (result.error?.includes('Test already completed') || result.error?.includes('already completed')) {
console.log('⚠️ 測驗已完成,跳到下一個');
loadNextUncompletedTest();
} else {
// 其他錯誤先更新UI狀態避免卡住
setCompletedTests(prev => prev + 1);
setCurrentTestItemIndex(prev => prev + 1);
setTimeout(() => {
loadNextUncompletedTest();
}, 1500);
}
}
} catch (error) {
console.error('💥 記錄測驗結果異常:', error);
// 即使出錯也更新進度,避免卡住
setCompletedTests(prev => prev + 1);
setCurrentTestItemIndex(prev => prev + 1);
setTimeout(() => {
loadNextUncompletedTest();
}, 1500);
}
}
// 載入下一個未完成的測驗
const loadNextUncompletedTest = () => {
if (currentTestItemIndex + 1 < testItems.length) {
// 還有測驗項目
const nextTestItem = testItems[currentTestItemIndex + 1];
const nextCard = dueCards.find(c => c.id === nextTestItem.cardId);
if (nextCard) {
setCurrentCard(nextCard);
setCurrentCardIndex(dueCards.findIndex(c => c.id === nextCard.id));
// 設置下一個測驗類型
const modeMapping: { [key: string]: typeof mode } = {
'flip-memory': 'flip-memory',
'vocab-choice': 'vocab-choice',
'vocab-listening': 'vocab-listening',
'sentence-fill': 'sentence-fill',
'sentence-reorder': 'sentence-reorder',
'sentence-speaking': 'sentence-speaking',
'sentence-listening': 'sentence-listening'
};
const nextMode = modeMapping[nextTestItem.testType] || 'flip-memory';
setMode(nextMode);
resetAllStates();
// 更新測驗項目的當前狀態
setTestItems(prev =>
prev.map((item, index) =>
index === currentTestItemIndex + 1
? { ...item, isCurrent: true }
: { ...item, isCurrent: false }
)
);
console.log(`🔄 載入下一個測驗: ${nextCard.word} - ${nextTestItem.testType}`);
}
} else {
// 所有測驗完成
console.log('🎉 所有測驗完成!');
setShowComplete(true);
}
}
// 完成詞卡複習並提交到後端
const completeCardReview = async (session: CardReviewSession) => {
try {
// 計算綜合表現指標
const correctCount = session.completedTests.filter(t => t.isCorrect).length;
const totalTests = session.completedTests.length;
const accuracy = totalTests > 0 ? correctCount / totalTests : 0;
// 計算平均信心等級(用於翻卡記憶測驗)
const confidenceTests = session.completedTests.filter(t => t.confidenceLevel !== undefined);
const avgConfidence = confidenceTests.length > 0
? confidenceTests.reduce((sum, t) => sum + (t.confidenceLevel || 3), 0) / confidenceTests.length
: 3;
// 計算平均答題時間
const avgResponseTime = session.completedTests.reduce((sum, t) => sum + t.responseTimeMs, 0) / totalTests;
// 確定主要測驗類型用於後端SM2算法
const primaryTestType = session.completedTests[0]?.testType || 'flip-memory';
console.log(`🔥 提交詞卡完整復習結果:`, {
word: session.word,
accuracy: `${Math.round(accuracy * 100)}%`,
avgConfidence,
avgResponseTime: `${avgResponseTime}ms`,
primaryTestType,
completedTests: session.completedTests.length
});
// 提交到後端
const result = await flashcardsService.submitReview(session.cardId, {
isCorrect: accuracy >= 0.7, // 70%以上正確率視為通過
confidenceLevel: Math.round(avgConfidence),
questionType: primaryTestType,
userAnswer: `綜合${totalTests}個測驗,正確率${Math.round(accuracy * 100)}%`,
timeTaken: Math.round(avgResponseTime)
});
if (result.success && result.data) {
console.log('✅ 詞卡復習結果提交成功:', result.data);
// 更新詞卡的熟悉度等資訊
if (currentCard && currentCard.id === session.cardId) {
setCurrentCard(prev => prev ? {
...prev,
masteryLevel: result.data!.masteryLevel,
nextReviewDate: result.data!.nextReviewDate
} : null);
}
// 增加已完成詞卡數量
setCompletedCards(prev => prev + 1);
console.log(`🎉 詞卡 ${session.word} 復習完成!新熟悉度: ${result.data.masteryLevel}%, 下次復習: ${result.data.nextReviewDate}`);
} else {
console.error('詞卡復習結果提交失敗:', result.error);
}
} catch (error) {
console.error('完成詞卡復習時發生錯誤:', error);
}
}
const handleFillAnswer = async () => {
if (showResult || !currentCard) return
setShowResult(true)
const isCorrect = fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 記錄測驗結果
await recordTestResult(isCorrect, fillAnswer);
}
const handleListeningAnswer = async (answer: string) => {
if (showResult || !currentCard) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.word
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 記錄測驗結果
await recordTestResult(isCorrect, answer);
}
const handleSpeakingAnswer = async (transcript: string) => {
if (!currentCard) return
setShowResult(true)
const isCorrect = transcript.toLowerCase().includes(currentCard.word.toLowerCase())
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 記錄測驗結果
await recordTestResult(isCorrect, transcript);
}
const handleSentenceListeningAnswer = async (answer: string) => {
if (showResult || !currentCard) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.example
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
// 記錄測驗結果
await recordTestResult(isCorrect, answer);
}
const handleReportSubmit = () => {
console.log('Report submitted:', {
card: reportingCard,
reason: reportReason
})
setShowReportModal(false)
setReportReason('')
setReportingCard(null)
}
const handleRestart = async () => {
setScore({ correct: 0, total: 0 })
setCompletedTests(0)
setTotalTests(0)
setTestItems([])
setCurrentTestItemIndex(0)
setShowComplete(false)
setShowNoDueCards(false)
await loadDueCards(); // 重新載入到期詞卡
}
// Show loading screen until mounted or while loading cards
if (!mounted || isLoadingCard) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-gray-500 text-lg">
{isAutoSelecting ? '系統正在選擇最適合的複習方式...' : '載入中...'}
</div>
</div>
)
}
// Show no due cards screen
if (showNoDueCards) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<Navigation />
<div className="max-w-4xl mx-auto px-4 py-8 flex items-center justify-center min-h-[calc(100vh-80px)]">
<div className="bg-white rounded-xl p-8 max-w-md w-full text-center shadow-lg">
<div className="text-6xl mb-4">📚</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="space-y-3 mb-6">
<div className="bg-blue-50 rounded-lg p-3 text-left">
<div className="font-medium text-blue-900 mb-1">💡 </div>
<ul className="text-blue-800 text-sm space-y-1">
<li> </li>
<li> </li>
<li> 調</li>
</ul>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => router.push('/flashcards')}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
</button>
<button
onClick={() => router.push('/dashboard')}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors font-medium"
>
</button>
</div>
<button
onClick={handleRestart}
className="w-full mt-3 py-2 text-blue-600 hover:text-blue-800 transition-colors text-sm"
>
🔄
</button>
</div>
</div>
</div>
)
}
// Show current card interface
if (!currentCard) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-gray-500 text-lg">...</div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Navigation */}
<Navigation />
<div className="max-w-4xl mx-auto px-4 py-8">
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between items-center mb-3">
<span className="text-sm font-medium text-gray-900"></span>
<div className="flex items-center gap-6">
{/* 詞卡進度 */}
<div className="text-sm text-gray-600">
<span className="font-medium">:</span>
<span className="ml-1 text-green-600 font-semibold">{completedCards}</span>
<span className="text-gray-500">/</span>
<span className="text-gray-600">{dueCards.length}</span>
{dueCards.length > 0 && (
<span className="text-blue-600 ml-1">
({Math.round((completedCards / dueCards.length) * 100)}%)
</span>
)}
</div>
{/* 測驗進度 */}
<button
onClick={() => setShowTaskListModal(true)}
className="text-sm text-gray-600 hover:text-blue-600 transition-colors cursor-pointer flex items-center gap-1"
title="點擊查看詳細任務清單"
>
<span className="font-medium">:</span>
<span className="ml-1 text-blue-600 font-semibold">{completedTests}</span>
<span className="text-gray-500">/</span>
<span className="text-gray-600">{totalTests}</span>
{totalTests > 0 && (
<span className="text-blue-600 ml-1">
({Math.round((completedTests / totalTests) * 100)}%)
</span>
)}
<span className="text-xs ml-1">📋</span>
</button>
</div>
</div>
{/* 雙層進度條 */}
<div className="space-y-2">
{/* 詞卡進度條 */}
<div className="flex items-center gap-3">
<span className="text-xs text-gray-500 w-12"></span>
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${dueCards.length > 0 ? (completedCards / dueCards.length) * 100 : 0}%` }}
></div>
</div>
<span className="text-xs text-gray-500 w-8">
{dueCards.length > 0 ? Math.round((completedCards / dueCards.length) * 100) : 0}%
</span>
</div>
{/* 測驗進度條 */}
<div className="flex items-center gap-3">
<span className="text-xs text-gray-500 w-12"></span>
<div
className="flex-1 bg-gray-200 rounded-full h-2 cursor-pointer hover:bg-gray-300 transition-colors"
onClick={() => setShowTaskListModal(true)}
title="點擊查看詳細任務清單"
>
<div
className="bg-blue-500 h-2 rounded-full transition-all hover:bg-blue-600"
style={{ width: `${totalTests > 0 ? (completedTests / totalTests) * 100 : 0}%` }}
></div>
</div>
<span className="text-xs text-gray-500 w-8">
{totalTests > 0 ? Math.round((completedTests / totalTests) * 100) : 0}%
</span>
</div>
</div>
</div>
{mode === 'flip-memory' ? (
/* Flip Card Mode */
<div className="relative">
{/* Error Report Button for Flip Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div
ref={cardContainerRef}
className="card-container"
onClick={handleFlip}
style={{ height: `${cardHeight}px` }}
>
<div className={`card ${isFlipped ? 'flipped' : ''}`}>
{/* Front */}
<div className="card-front">
<div
ref={cardFrontRef}
className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow p-8"
>
{/* Title and Instructions */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Word Display */}
<div className="flex-1 flex items-center justify-center mt-6">
<div className="bg-gray-50 rounded-lg p-8 w-full text-center">
<h3 className="text-4xl font-bold text-gray-900 mb-6">
{currentCard.word}
</h3>
<div className="flex items-center justify-center gap-3">
<span className="text-lg text-gray-500">
{currentCard.pronunciation}
</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
</div>
</div>
{/* Back */}
<div className="card-back">
<div
ref={cardBackRef}
className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow"
>
{/* Content Sections */}
<div className="space-y-4">
{/* Definition */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 text-left">{currentCard.definition}</p>
</div>
{/* Example */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="relative">
<p className="text-gray-700 italic mb-2 text-left pr-12">"{currentCard.example}"</p>
<div className="absolute bottom-0 right-0">
<AudioPlayer text={currentCard.example} />
</div>
</div>
<p className="text-gray-600 text-sm text-left">"{currentCard.exampleTranslation}"</p>
</div>
{/* Synonyms */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="flex flex-wrap gap-2">
{(currentCard.synonyms || []).map((synonym, index) => (
<span
key={index}
className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200"
>
{synonym}
</span>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'vocab-choice' ? (
/* Vocab Choice Mode - 詞彙選擇 */
<div className="relative">
{/* Error Report Button for Quiz Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
<div className="text-center mb-8">
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 text-left">{currentCard.definition}</p>
</div>
</div>
<div className="space-y-3 mb-6">
{quizOptions.map((option, idx) => (
<button
key={idx}
onClick={() => !showResult && handleQuizAnswer(option)}
disabled={showResult}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
showResult
? option === currentCard.word
? 'border-green-500 bg-green-50 text-green-700'
: option === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
{option}
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.word
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.word
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.word && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<div className="flex items-center text-gray-600">
<strong></strong>
<span className="mx-2">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-fill' ? (
/* Fill in the Blank Mode - 填空題 */
<div className="relative">
{/* Error Report Button for Fill Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Example Image */}
{currentCard.exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={currentCard.exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(currentCard.exampleImage || null)}
/>
</div>
</div>
)}
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Example Sentence with Blanks */}
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-6">
<div className="text-lg text-gray-700 leading-relaxed">
{currentCard.example.split(new RegExp(`(${currentCard.word})`, 'gi')).map((part, index) => {
const isTargetWord = part.toLowerCase() === currentCard.word.toLowerCase();
return isTargetWord ? (
<span key={index} className="relative inline-block mx-1">
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !showResult && fillAnswer.trim()) {
handleFillAnswer()
}
}}
placeholder=""
disabled={showResult}
className={`inline-block px-2 py-1 text-center bg-transparent focus:outline-none disabled:bg-gray-100 ${
fillAnswer
? 'border-b-2 border-blue-500'
: 'border-b-2 border-dashed border-gray-400 focus:border-blue-400 focus:border-solid'
}`}
style={{ width: `${Math.max(100, Math.max(currentCard.word.length * 12, fillAnswer.length * 12 + 20))}px` }}
/>
{!fillAnswer && (
<span
className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
style={{ paddingBottom: '8px' }}
>
____
</span>
)}
</span>
) : (
<span key={index}>{part}</span>
);
})}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3 mb-4">
{!showResult && fillAnswer.trim() && (
<button
onClick={handleFillAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
</button>
)}
<button
onClick={() => setShowHint(!showHint)}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
{showHint ? '隱藏提示' : '顯示提示'}
</button>
</div>
{/* Hint Section */}
{showHint && (
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-semibold text-yellow-800 mb-2"></h4>
<p className="text-yellow-800">{currentCard.definition}</p>
</div>
)}
{showResult && (
<div className={`mt-6 p-6 rounded-lg w-full ${
fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
? 'text-green-700'
: 'text-red-700'
}`}>
{fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase() ? '正確!' : '錯誤!'}
</p>
{fillAnswer.toLowerCase().trim() !== currentCard.word.toLowerCase() && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
<span className="mx-2">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</p>
</div>
<div className="text-left">
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'vocab-listening' ? (
/* Listening Test Mode - 聽力測試 */
<div className="relative">
{/* Error Report Button for Listening Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
()
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Content Sections */}
<div className="space-y-4 mb-8">
{/* Audio */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="flex items-center gap-3">
<span className="text-gray-700">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
{/* Word Options */}
<div className="grid grid-cols-2 gap-3 mb-6">
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => (
<button
key={word}
onClick={() => !showResult && handleListeningAnswer(word)}
disabled={showResult}
className={`p-4 text-center rounded-lg border-2 transition-all ${
showResult
? word === currentCard.word
? 'border-green-500 bg-green-50 text-green-700'
: word === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
{word}
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.word
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.word
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.word && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
<div className="text-gray-600 text-left mt-1 flex items-center gap-2">
<span>{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
)}
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-speaking' ? (
/* Speaking Test Mode - 口說測試 */
<div className="relative">
{/* Error Report Button for Speaking Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
<div className="w-full">
<VoiceRecorder
targetText={currentCard.example}
targetTranslation={currentCard.exampleTranslation}
exampleImage={currentCard.exampleImage}
instructionText="請看例句圖片並大聲說出完整的例句:"
onRecordingComplete={() => {
// 簡化處理:直接顯示結果
handleSpeakingAnswer(currentCard.example)
}}
/>
</div>
{showResult && (
<div className="mt-6 p-6 rounded-lg bg-blue-50 border border-blue-200 w-full">
<p className="text-blue-700 text-left text-xl font-semibold mb-2">
</p>
<p className="text-gray-600 text-left">
...
</p>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-listening' ? (
/* Sentence Listening Test Mode - 例句聽力題 */
<div className="relative">
{/* Error Report Button */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
<div className="text-center mb-8">
<div className="mb-6">
<AudioPlayer text={currentCard.example} />
<p className="text-sm text-gray-500 mt-2">
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-3 mb-6">
{sentenceOptions.map((sentence, idx) => (
<button
key={idx}
onClick={() => !showResult && handleSentenceListeningAnswer(sentence)}
disabled={showResult}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
showResult
? sentence === currentCard.example
? 'border-green-500 bg-green-50 text-green-700'
: sentence === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
<div className="text-sm text-gray-600 mb-1"> {String.fromCharCode(65 + idx)}:</div>
<div className="text-base">{sentence}</div>
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.example
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.example
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.example ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.example && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">"{currentCard.example}"</strong>
</p>
<p className="text-gray-600 text-left mt-1">
{currentCard.exampleTranslation}
</p>
</div>
)}
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-reorder' ? (
/* Sentence Reorder Mode - 例句重組題 */
<div className="relative">
{/* Error Report Button */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
{/* Example Image */}
{currentCard.exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={currentCard.exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(currentCard.exampleImage || null)}
/>
</div>
</div>
)}
{/* Arranged Sentence Area */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left"></h3>
<div className="relative min-h-[120px] bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-300">
{arrangedWords.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center text-gray-400 text-lg">
</div>
) : (
<div className="flex flex-wrap gap-2">
{arrangedWords.map((word, index) => (
<div
key={`arranged-${index}`}
className="inline-flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-blue-200 transition-colors"
onClick={() => handleRemoveFromArranged(word)}
>
{word}
<span className="ml-2 text-blue-600 hover:text-blue-800">×</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Shuffled Words */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left"></h3>
<div className="bg-white border border-gray-200 rounded-lg p-4 min-h-[80px]">
{shuffledWords.length === 0 ? (
<div className="text-center text-gray-400">
使
</div>
) : (
<div className="flex flex-wrap gap-2">
{shuffledWords.map((word, index) => (
<button
key={`shuffled-${index}`}
onClick={() => handleWordClick(word)}
className="bg-gray-100 text-gray-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-gray-200 active:bg-gray-300 transition-colors select-none"
>
{word}
</button>
))}
</div>
)}
</div>
</div>
{/* Control Buttons */}
<div className="flex gap-3 mb-6">
{arrangedWords.length > 0 && (
<button
onClick={handleCheckReorderAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
</button>
)}
<button
onClick={handleResetReorder}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
</button>
</div>
{/* Result Feedback */}
{reorderResult !== null && (
<div className={`p-6 rounded-lg w-full mb-6 ${
reorderResult
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
reorderResult ? 'text-green-700' : 'text-red-700'
}`}>
{reorderResult ? '正確!' : '錯誤!'}
</p>
{!reorderResult && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">"{currentCard.example}"</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
{currentCard.exampleTranslation}
</p>
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === dueCards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : null}
{/* Report Modal */}
{showReportModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="mb-4">
<p className="text-sm text-gray-600 mb-2">
{reportingCard?.word}
</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select
value={reportReason}
onChange={(e) => setReportReason(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
<option value=""></option>
<option value="translation"></option>
<option value="definition"></option>
<option value="pronunciation"></option>
<option value="example"></option>
<option value="image"></option>
<option value="other"></option>
</select>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowReportModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50"
>
</button>
<button
onClick={handleReportSubmit}
disabled={!reportReason}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
</div>
)}
{/* Image Modal */}
{modalImage && (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
onClick={() => setModalImage(null)}
>
<div className="relative max-w-4xl max-h-[90vh] mx-4">
<img
src={modalImage}
alt="放大圖片"
className="max-w-full max-h-full rounded-lg"
/>
<button
onClick={() => setModalImage(null)}
className="absolute top-4 right-4 bg-black bg-opacity-50 text-white p-2 rounded-full hover:bg-opacity-75"
>
</button>
</div>
</div>
)}
{/* Task List Modal */}
{showTaskListModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
📚
</h2>
<button
onClick={() => setShowTaskListModal(false)}
className="text-gray-400 hover:text-gray-600 text-2xl"
>
</button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[60vh]">
{/* 進度統計 */}
<div className="mb-6 bg-blue-50 rounded-lg p-4">
<div className="flex items-center justify-between text-sm">
<span className="text-blue-900 font-medium">
: {completedTests} / {totalTests} ({totalTests > 0 ? Math.round((completedTests / totalTests) * 100) : 0}%)
</span>
<div className="flex items-center gap-4 text-blue-800">
<span> : {testItems.filter(item => item.isCompleted).length}</span>
<span> : {testItems.filter(item => item.isCurrent).length}</span>
<span> : {testItems.filter(item => !item.isCompleted && !item.isCurrent).length}</span>
</div>
</div>
<div className="mt-3 w-full bg-blue-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${totalTests > 0 ? (completedTests / totalTests) * 100 : 0}%` }}
></div>
</div>
</div>
{/* 任務清單 */}
<div className="space-y-4">
{groupTestItemsByCard(testItems).map((cardGroup, cardIndex) => (
<div key={cardGroup.cardId} className="border border-gray-200 rounded-lg p-4 bg-white shadow-sm">
{/* 詞卡標題 */}
<div className="flex items-center gap-3 mb-3">
<span className="font-medium text-gray-900">
{cardIndex + 1}: {cardGroup.word}
</span>
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded">
{cardGroup.context}
</span>
<span className="text-xs text-gray-500">
{cardGroup.tests.length}
</span>
</div>
{/* 測驗項目 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{cardGroup.tests.map(test => (
<div
key={test.id}
className={`flex items-center gap-3 p-3 rounded-lg transition-all ${
test.isCompleted
? 'bg-green-50 border border-green-200'
: test.isCurrent
? 'bg-blue-50 border border-blue-300 shadow-sm'
: 'bg-gray-50 border border-gray-200'
}`}
>
{/* 狀態圖標 */}
<span className="text-lg">
{test.isCompleted ? '✅' : test.isCurrent ? '⏳' : '⚪'}
</span>
{/* 測驗資訊 */}
<div className="flex-1">
<div className="font-medium text-sm">
{test.order}. {test.testName}
</div>
<div className={`text-xs ${
test.isCompleted ? 'text-green-600' :
test.isCurrent ? 'text-blue-600' : 'text-gray-500'
}`}>
{test.isCompleted ? '已完成' :
test.isCurrent ? '進行中' : '待完成'}
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{testItems.length === 0 && (
<div className="text-center py-8 text-gray-500">
<div className="text-4xl mb-2">📚</div>
<p></p>
</div>
)}
</div>
</div>
</div>
)}
{/* Complete Modal */}
{showComplete && (
<LearningComplete
score={score}
mode={mode}
onRestart={handleRestart}
onBackToDashboard={() => router.push('/dashboard')}
/>
)}
{/* No Due Cards Modal */}
{showNoDueCards && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-8 max-w-md w-full mx-4 text-center">
<div className="text-6xl mb-4">📚</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-6">
</p>
<div className="space-y-3 mb-6">
<div className="bg-blue-50 rounded-lg p-3 text-left">
<div className="font-medium text-blue-900 mb-1">💡 </div>
<ul className="text-blue-800 text-sm space-y-1">
<li> </li>
<li> </li>
<li> 調</li>
</ul>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => router.push('/flashcards')}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
</button>
<button
onClick={() => router.push('/dashboard')}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors font-medium"
>
</button>
</div>
<button
onClick={handleRestart}
className="w-full mt-3 py-2 text-blue-600 hover:text-blue-800 transition-colors text-sm"
>
🔄
</button>
</div>
</div>
)}
</div>
<style jsx>{`
.card-container {
perspective: 1000px;
transition: height 0.3s ease;
overflow: visible;
position: relative;
}
.card {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s ease;
transform-style: preserve-3d;
}
.card.flipped {
transform: rotateY(180deg);
}
.card-front, .card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: stretch;
justify-content: center;
top: 0;
left: 0;
}
.card-back {
transform: rotateY(180deg);
}
.card-front > div {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: 2rem;
box-sizing: border-box;
}
.card-back > div {
width: 100%;
padding: 1.5rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
`}</style>
</div>
)
}