230 lines
9.5 KiB
TypeScript
230 lines
9.5 KiB
TypeScript
'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>
|
||
)
|
||
} |