2429 lines
92 KiB
TypeScript
2429 lines
92 KiB
TypeScript
'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>
|
||
)
|
||
} |