dramaling-vocab-learning/frontend/app/learn/page.tsx

230 lines
9.5 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 } from 'react'
import { useRouter } from 'next/navigation'
import { Navigation } from '@/components/Navigation'
import LearningComplete from '@/components/LearningComplete'
// 標準架構:全域組件和 hooks
import { ProgressTracker } from '@/components/learn/ProgressTracker'
import { TaskListModal } from '@/components/learn/TaskListModal'
import { ReviewContainer } from '@/components/learn/ReviewContainer'
import { LoadingStates } from '@/components/learn/LoadingStates'
import { useReviewSession } from '@/hooks/learn/useReviewSession'
import { useTestQueue } from '@/hooks/learn/useTestQueue'
import { useProgressTracker } from '@/hooks/learn/useProgressTracker'
import { useTestAnswering } from '@/hooks/learn/useTestAnswering'
export default function LearnPage() {
const router = useRouter()
const [mounted, setMounted] = useState(false)
// UI 狀態
const [modalImage, setModalImage] = useState<string | null>(null)
const [showReportModal, setShowReportModal] = useState(false)
const [reportReason, setReportReason] = useState('')
const [reportingCard, setReportingCard] = useState<any>(null)
// 使用全域 hooks
const reviewSession = useReviewSession()
const testQueue = useTestQueue()
const progressTracker = useProgressTracker()
const testAnswering = useTestAnswering()
// 初始化
useEffect(() => {
setMounted(true)
initializeLearnSession()
}, [])
// 初始化學習會話
const initializeLearnSession = async () => {
await reviewSession.loadDueCards()
if (reviewSession.dueCards.length > 0) {
const cardIds = reviewSession.dueCards.map(c => c.id)
const completedTests = await testQueue.getCompletedTestsForCards(cardIds)
testQueue.initializeTestQueue(reviewSession.dueCards, completedTests)
if (testQueue.testItems.length > 0) {
const firstTestItem = testQueue.testItems[0]
const firstCard = reviewSession.dueCards.find(c => c.id === firstTestItem.cardId)
if (firstCard) {
reviewSession.setCurrentCard(firstCard)
reviewSession.setMode(firstTestItem.testType as any)
reviewSession.setIsAutoSelecting(false)
}
}
}
}
// 答題處理
const handleQuizAnswer = async (answer: string) => {
if (testAnswering.showResult || !reviewSession.currentCard) return
testAnswering.setSelectedAnswer(answer)
testAnswering.setShowResult(true)
const isCorrect = testAnswering.checkVocabChoice(reviewSession.currentCard.word)
progressTracker.updateScore(isCorrect)
await testQueue.recordTestResult(isCorrect, answer)
}
const handleFillAnswer = async () => {
if (testAnswering.showResult || !reviewSession.currentCard) return
testAnswering.setShowResult(true)
const isCorrect = testAnswering.checkSentenceFill(reviewSession.currentCard.word)
progressTracker.updateScore(isCorrect)
await testQueue.recordTestResult(isCorrect, testAnswering.fillAnswer)
}
const handleConfidenceLevel = async (level: number) => {
if (!reviewSession.currentCard) return
testAnswering.setShowResult(true)
progressTracker.updateScore(true)
await testQueue.recordTestResult(true, undefined, level)
}
const handleReorderAnswer = async () => {
if (!reviewSession.currentCard) return
const isCorrect = testAnswering.checkSentenceReorder(reviewSession.currentCard.example)
testAnswering.setReorderResult(isCorrect)
progressTracker.updateScore(isCorrect)
await testQueue.recordTestResult(isCorrect, testAnswering.arrangedWords.join(' '))
}
const handleNext = () => {
testQueue.loadNextUncompletedTest()
testAnswering.resetAllAnsweringStates()
if (testQueue.currentTestItemIndex < testQueue.testItems.length) {
const nextTestItem = testQueue.testItems[testQueue.currentTestItemIndex]
const nextCard = reviewSession.dueCards.find(c => c.id === nextTestItem.cardId)
if (nextCard) {
reviewSession.setCurrentCard(nextCard)
reviewSession.setMode(nextTestItem.testType as any)
}
} else {
reviewSession.setShowComplete(true)
}
}
const handleRestart = async () => {
progressTracker.resetScore()
testQueue.resetTestQueue()
await reviewSession.restart()
}
// 渲染邏輯
if (!mounted || reviewSession.isLoadingCard) {
return <LoadingStates isLoadingCard={reviewSession.isLoadingCard} isAutoSelecting={reviewSession.isAutoSelecting} />
}
if (reviewSession.showNoDueCards) {
return <LoadingStates showNoDueCards={true} onRestart={handleRestart} />
}
if (!reviewSession.currentCard) {
return <LoadingStates isLoadingCard={true} />
}
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">
<ProgressTracker
completedTests={testQueue.completedTests}
totalTests={testQueue.totalTests}
onShowTaskList={() => progressTracker.setShowTaskListModal(true)}
/>
<ReviewContainer
currentCard={reviewSession.currentCard}
mode={reviewSession.mode}
selectedAnswer={testAnswering.selectedAnswer}
showResult={testAnswering.showResult}
fillAnswer={testAnswering.fillAnswer}
showHint={testAnswering.showHint}
isFlipped={testAnswering.isFlipped}
quizOptions={testAnswering.quizOptions}
shuffledWords={testAnswering.shuffledWords}
arrangedWords={testAnswering.arrangedWords}
reorderResult={testAnswering.reorderResult}
currentCardIndex={reviewSession.currentCardIndex}
totalCards={reviewSession.dueCards.length}
onAnswer={handleQuizAnswer}
onFillSubmit={handleFillAnswer}
onFillAnswerChange={testAnswering.setFillAnswer}
onToggleHint={() => testAnswering.setShowHint(!testAnswering.showHint)}
onFlip={() => testAnswering.setIsFlipped(!testAnswering.isFlipped)}
onConfidenceLevel={handleConfidenceLevel}
onWordClick={testAnswering.addWordToArranged}
onRemoveFromArranged={testAnswering.removeWordFromArranged}
onCheckReorderAnswer={handleReorderAnswer}
onResetReorder={() => testAnswering.resetReorderTest(reviewSession.currentCard?.example || '')}
onReportError={() => {
setReportingCard(reviewSession.currentCard)
setShowReportModal(true)
}}
onNavigate={(direction) => direction === 'next' ? handleNext() : () => {}}
setModalImage={setModalImage}
/>
<TaskListModal
isOpen={progressTracker.showTaskListModal}
onClose={() => progressTracker.setShowTaskListModal(false)}
testItems={testQueue.testItems}
completedTests={testQueue.completedTests}
totalTests={testQueue.totalTests}
/>
{reviewSession.showComplete && (
<LearningComplete
score={progressTracker.score}
mode={reviewSession.mode}
onRestart={handleRestart}
onBackToDashboard={() => router.push('/dashboard')}
/>
)}
{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>
)}
{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={() => { console.log('Report submitted:', { card: reportingCard, reason: reportReason }); setShowReportModal(false); setReportReason(''); setReportingCard(null) }} 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>
)}
</div>
</div>
)
}