feat: 重構並整合 review-simple 組件系統
## 主要變更 - 重新組織檔案結構到標準 Next.js 目錄 - 簡化並整合 VocabChoiceTest 組件 - 優化狀態管理架構 - 統一兩種測驗類型的設計風格 ## 檔案重組 - components/review/simple/ - 統一測驗組件 - hooks/review/ - 複習相關 Hook - lib/data/ - 數據管理 - note/archive/ - 舊複雜系統備份 ## 新功能 - SimpleFlipCard: 翻卡記憶測驗 (信心度 0-2) - VocabChoiceTest: 詞彙選擇測驗 (正確2分/錯誤0分) - 統一接口設計和用戶體驗流程 ## 技術改進 - 移除過度複雜的依賴系統 - 使用 useReducer 優化狀態管理 - useMemo 提升性能 - 統一設計語言和組件風格 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
914c981c4b
commit
04def4bb85
|
|
@ -1,210 +0,0 @@
|
|||
import { useReducer, useEffect, useMemo } from 'react'
|
||||
import { SIMPLE_CARDS, CardState, sortCardsByPriority } from '../data'
|
||||
|
||||
interface ReviewState {
|
||||
cards: CardState[]
|
||||
score: { correct: number; total: number }
|
||||
isComplete: boolean
|
||||
}
|
||||
|
||||
type ReviewAction =
|
||||
| { type: 'LOAD_PROGRESS'; payload: ReviewState }
|
||||
| { type: 'ANSWER_CARD'; payload: { cardId: string; confidence: number } }
|
||||
| { type: 'SKIP_CARD'; payload: { cardId: string } }
|
||||
| { type: 'RESTART' }
|
||||
|
||||
// 內部狀態更新函數
|
||||
const updateCardState = (
|
||||
cards: CardState[],
|
||||
cardIndex: number,
|
||||
updates: Partial<CardState>
|
||||
): CardState[] => {
|
||||
return cards.map((card, index) =>
|
||||
index === cardIndex
|
||||
? { ...card, ...updates }
|
||||
: card
|
||||
)
|
||||
}
|
||||
|
||||
const reviewReducer = (state: ReviewState, action: ReviewAction): ReviewState => {
|
||||
switch (action.type) {
|
||||
case 'LOAD_PROGRESS':
|
||||
return action.payload
|
||||
|
||||
case 'ANSWER_CARD': {
|
||||
const { cardId, confidence } = action.payload
|
||||
const isCorrect = confidence >= 2
|
||||
|
||||
// 使用 Map 優化查找性能
|
||||
const cardMap = new Map(state.cards.map((card, index) => [card.id, { card, index }]))
|
||||
const cardData = cardMap.get(cardId)
|
||||
|
||||
if (!cardData) return state
|
||||
|
||||
const { index: cardIndex } = cardData
|
||||
const currentCard = state.cards[cardIndex]
|
||||
|
||||
const updatedCards = updateCardState(state.cards, cardIndex, {
|
||||
isCompleted: isCorrect,
|
||||
wrongCount: isCorrect ? currentCard.wrongCount : currentCard.wrongCount + 1
|
||||
})
|
||||
|
||||
const newScore = {
|
||||
correct: state.score.correct + (isCorrect ? 1 : 0),
|
||||
total: state.score.total + 1
|
||||
}
|
||||
|
||||
const remainingCards = updatedCards.filter(card => !card.isCompleted)
|
||||
const isComplete = remainingCards.length === 0
|
||||
|
||||
return {
|
||||
cards: updatedCards,
|
||||
score: newScore,
|
||||
isComplete
|
||||
}
|
||||
}
|
||||
|
||||
case 'SKIP_CARD': {
|
||||
const { cardId } = action.payload
|
||||
|
||||
// 使用 Map 優化查找性能
|
||||
const cardMap = new Map(state.cards.map((card, index) => [card.id, { card, index }]))
|
||||
const cardData = cardMap.get(cardId)
|
||||
|
||||
if (!cardData) return state
|
||||
|
||||
const { index: cardIndex } = cardData
|
||||
const currentCard = state.cards[cardIndex]
|
||||
|
||||
const updatedCards = updateCardState(state.cards, cardIndex, {
|
||||
skipCount: currentCard.skipCount + 1
|
||||
})
|
||||
|
||||
const remainingCards = updatedCards.filter(card => !card.isCompleted)
|
||||
const isComplete = remainingCards.length === 0
|
||||
|
||||
return {
|
||||
cards: updatedCards,
|
||||
score: state.score,
|
||||
isComplete
|
||||
}
|
||||
}
|
||||
|
||||
case 'RESTART':
|
||||
return {
|
||||
cards: SIMPLE_CARDS,
|
||||
score: { correct: 0, total: 0 },
|
||||
isComplete: false
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export function useReviewSession() {
|
||||
// 使用 useReducer 統一狀態管理
|
||||
const [state, dispatch] = useReducer(reviewReducer, {
|
||||
cards: SIMPLE_CARDS,
|
||||
score: { correct: 0, total: 0 },
|
||||
isComplete: false
|
||||
})
|
||||
|
||||
const { cards, score, isComplete } = state
|
||||
|
||||
// 智能排序獲取當前卡片 - 使用 useMemo 優化性能
|
||||
const sortedCards = useMemo(() => sortCardsByPriority(cards), [cards])
|
||||
const incompleteCards = useMemo(() =>
|
||||
sortedCards.filter((card: CardState) => !card.isCompleted),
|
||||
[sortedCards]
|
||||
)
|
||||
const currentCard = incompleteCards[0] // 總是選擇優先級最高的未完成卡片
|
||||
|
||||
// localStorage進度保存和載入
|
||||
useEffect(() => {
|
||||
// 載入保存的進度
|
||||
const savedProgress = localStorage.getItem('review-progress')
|
||||
if (savedProgress) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedProgress)
|
||||
const saveTime = new Date(parsed.timestamp)
|
||||
const now = new Date()
|
||||
const isToday = saveTime.toDateString() === now.toDateString()
|
||||
|
||||
if (isToday && parsed.cards) {
|
||||
dispatch({
|
||||
type: 'LOAD_PROGRESS',
|
||||
payload: {
|
||||
cards: parsed.cards,
|
||||
score: parsed.score || { correct: 0, total: 0 },
|
||||
isComplete: parsed.isComplete || false
|
||||
}
|
||||
})
|
||||
console.log('📖 載入保存的複習進度')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('進度載入失敗:', error)
|
||||
localStorage.removeItem('review-progress')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 保存進度到localStorage
|
||||
const saveProgress = () => {
|
||||
const progress = {
|
||||
cards,
|
||||
score,
|
||||
isComplete,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
localStorage.setItem('review-progress', JSON.stringify(progress))
|
||||
console.log('💾 進度已保存')
|
||||
}
|
||||
|
||||
// 處理答題 - 使用 dispatch 統一管理
|
||||
const handleAnswer = (confidence: number) => {
|
||||
if (!currentCard) return
|
||||
|
||||
dispatch({
|
||||
type: 'ANSWER_CARD',
|
||||
payload: { cardId: currentCard.id, confidence }
|
||||
})
|
||||
|
||||
// 保存進度
|
||||
setTimeout(() => saveProgress(), 100) // 延遲一點確保狀態更新
|
||||
}
|
||||
|
||||
// 處理跳過 - 使用 dispatch 統一管理
|
||||
const handleSkip = () => {
|
||||
if (!currentCard) return
|
||||
|
||||
dispatch({
|
||||
type: 'SKIP_CARD',
|
||||
payload: { cardId: currentCard.id }
|
||||
})
|
||||
|
||||
// 保存進度
|
||||
setTimeout(() => saveProgress(), 100) // 延遲一點確保狀態更新
|
||||
}
|
||||
|
||||
// 重新開始 - 重置所有狀態
|
||||
const handleRestart = () => {
|
||||
dispatch({ type: 'RESTART' })
|
||||
localStorage.removeItem('review-progress') // 清除保存的進度
|
||||
console.log('🔄 複習進度已重置')
|
||||
}
|
||||
|
||||
return {
|
||||
// 狀態
|
||||
cards,
|
||||
score,
|
||||
isComplete,
|
||||
currentCard,
|
||||
sortedCards,
|
||||
|
||||
// 動作
|
||||
handleAnswer,
|
||||
handleSkip,
|
||||
handleRestart
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import { Navigation } from '@/components/shared/Navigation'
|
||||
import './globals.css'
|
||||
import { SimpleFlipCard } from './components/SimpleFlipCard'
|
||||
import { SimpleProgress } from './components/SimpleProgress'
|
||||
import { SimpleResults } from './components/SimpleResults'
|
||||
import { SIMPLE_CARDS, CardState } from './data'
|
||||
import { useReviewSession } from './hooks/useReviewSession'
|
||||
import { SimpleFlipCard } from '@/components/review/simple/SimpleFlipCard'
|
||||
import { SimpleProgress } from '@/components/review/simple/SimpleProgress'
|
||||
import { SimpleResults } from '@/components/review/simple/SimpleResults'
|
||||
import { SIMPLE_CARDS, CardState } from '@/lib/data/reviewSimpleData'
|
||||
import { useReviewSession } from '@/hooks/review/useReviewSession'
|
||||
|
||||
export default function SimpleReviewPage() {
|
||||
// 使用自定義 Hook 管理複習狀態
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Navigation } from '@/components/shared/Navigation'
|
||||
import { VocabChoiceTest } from '@/components/review/simple/VocabChoiceTest'
|
||||
import { SIMPLE_CARDS, CardState } from '@/lib/data/reviewSimpleData'
|
||||
|
||||
export default function TestVocabChoicePage() {
|
||||
const [currentCardIndex, setCurrentCardIndex] = useState(0)
|
||||
const [score, setScore] = useState({ correct: 0, total: 0 })
|
||||
|
||||
const currentCard = SIMPLE_CARDS[currentCardIndex]
|
||||
|
||||
// 生成測驗選項 (包含正確答案和3個干擾項)
|
||||
const generateOptions = (correctWord: string) => {
|
||||
const allWords = SIMPLE_CARDS.map(card => card.word).filter(word => word !== correctWord)
|
||||
const shuffledWords = allWords.sort(() => Math.random() - 0.5)
|
||||
const distractors = shuffledWords.slice(0, 3)
|
||||
|
||||
const options = [correctWord, ...distractors]
|
||||
return options.sort(() => Math.random() - 0.5) // 隨機排列選項
|
||||
}
|
||||
|
||||
const handleAnswer = (confidence: number) => {
|
||||
const isCorrect = confidence >= 2
|
||||
setScore(prev => ({
|
||||
correct: prev.correct + (isCorrect ? 1 : 0),
|
||||
total: prev.total + 1
|
||||
}))
|
||||
|
||||
// 移動到下一張卡片
|
||||
setTimeout(() => {
|
||||
if (currentCardIndex < SIMPLE_CARDS.length - 1) {
|
||||
setCurrentCardIndex(prev => prev + 1)
|
||||
} else {
|
||||
alert(`測試完成!正確率: ${score.correct + (isCorrect ? 1 : 0)}/${score.total + 1}`)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleSkip = () => {
|
||||
setScore(prev => ({ ...prev, total: prev.total + 1 }))
|
||||
|
||||
if (currentCardIndex < SIMPLE_CARDS.length - 1) {
|
||||
setCurrentCardIndex(prev => prev + 1)
|
||||
} else {
|
||||
alert(`測試完成!正確率: ${score.correct}/${score.total + 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
setCurrentCardIndex(0)
|
||||
setScore({ correct: 0, total: 0 })
|
||||
}
|
||||
|
||||
if (!currentCard) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
<Navigation />
|
||||
<div className="py-8">
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">測試完成!</h1>
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
重新開始
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
<Navigation />
|
||||
|
||||
<div className="py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
{/* 進度顯示 */}
|
||||
<div className="mb-6 bg-white rounded-lg shadow p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-xl font-bold">詞彙選擇測驗</h1>
|
||||
<div className="text-sm text-gray-600">
|
||||
第 {currentCardIndex + 1} / {SIMPLE_CARDS.length} 題
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="text-sm text-gray-600 mb-1">
|
||||
得分: {score.correct} / {score.total}
|
||||
{score.total > 0 && ` (${Math.round(score.correct / score.total * 100)}%)`}
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${((currentCardIndex) / SIMPLE_CARDS.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 測驗組件 */}
|
||||
<VocabChoiceTest
|
||||
card={currentCard}
|
||||
options={generateOptions(currentCard.word)}
|
||||
onAnswer={handleAnswer}
|
||||
onSkip={handleSkip}
|
||||
/>
|
||||
|
||||
{/* 控制按鈕 */}
|
||||
<div className="mt-6 text-center">
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 mr-4"
|
||||
>
|
||||
重新開始
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import React, { useState, useCallback, useMemo, memo } from 'react'
|
||||
import { ChoiceTestProps } from '@/types/review'
|
||||
import {
|
||||
TestResultDisplay,
|
||||
ChoiceTestContainer,
|
||||
ChoiceGrid
|
||||
} from '@/components/review/shared'
|
||||
|
||||
interface VocabChoiceTestProps extends ChoiceTestProps {}
|
||||
|
||||
const VocabChoiceTestComponent: React.FC<VocabChoiceTestProps> = ({
|
||||
cardData,
|
||||
options,
|
||||
onAnswer,
|
||||
onReportError,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
|
||||
const [showResult, setShowResult] = useState(false)
|
||||
|
||||
|
||||
const handleAnswerSelect = useCallback((answer: string) => {
|
||||
if (disabled || showResult) return
|
||||
setSelectedAnswer(answer)
|
||||
setShowResult(true)
|
||||
onAnswer(answer)
|
||||
}, [disabled, showResult, onAnswer])
|
||||
|
||||
const isCorrect = useMemo(() =>
|
||||
selectedAnswer === cardData.word
|
||||
, [selectedAnswer, cardData.word])
|
||||
|
||||
// 問題顯示區域
|
||||
const questionArea = (
|
||||
<div className="text-center">
|
||||
<div className="bg-gray-50 rounded-lg p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-3 text-left">定義</h3>
|
||||
<p className="text-gray-700 text-left text-lg leading-relaxed">{cardData.definition}</p>
|
||||
</div>
|
||||
<p className="text-lg text-gray-700 mt-4 text-left">
|
||||
請選擇符合上述定義的英文詞彙:
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
// 選項區域
|
||||
const optionsArea = (
|
||||
<ChoiceGrid
|
||||
options={options}
|
||||
selectedOption={selectedAnswer}
|
||||
correctAnswer={cardData.word}
|
||||
showResult={showResult}
|
||||
onSelect={handleAnswerSelect}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
|
||||
// 結果顯示區域
|
||||
const resultArea = showResult ? (
|
||||
<TestResultDisplay
|
||||
isCorrect={isCorrect}
|
||||
correctAnswer={cardData.word}
|
||||
userAnswer={selectedAnswer || ''}
|
||||
word={cardData.word}
|
||||
pronunciation={cardData.pronunciation}
|
||||
example={cardData.example}
|
||||
exampleTranslation={cardData.exampleTranslation}
|
||||
showResult={showResult}
|
||||
/>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<ChoiceTestContainer
|
||||
cardData={cardData}
|
||||
testTitle="詞彙選擇"
|
||||
questionArea={questionArea}
|
||||
optionsArea={optionsArea}
|
||||
resultArea={resultArea}
|
||||
onAnswer={onAnswer}
|
||||
onReportError={onReportError}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const VocabChoiceTest = memo(VocabChoiceTestComponent)
|
||||
VocabChoiceTest.displayName = 'VocabChoiceTest'
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { CardState } from '../data'
|
||||
import { CardState } from '@/lib/data/reviewSimpleData'
|
||||
import { SimpleTestHeader } from './SimpleTestHeader'
|
||||
|
||||
interface SimpleFlipCardProps {
|
||||
|
|
@ -188,9 +188,9 @@ export function SimpleFlipCard({ card, onAnswer, onSkip }: SimpleFlipCardProps)
|
|||
</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ level: 1, label: '模糊', color: 'bg-orange-100 text-orange-700 border-orange-200 hover:bg-orange-200' },
|
||||
{ level: 2, label: '一般', color: 'bg-yellow-100 text-yellow-700 border-yellow-200 hover:bg-yellow-200' },
|
||||
{ level: 3, label: '熟悉', color: 'bg-green-100 text-green-700 border-green-200 hover:bg-green-200' }
|
||||
{ level: 0, label: '模糊', color: 'bg-orange-100 text-orange-700 border-orange-200 hover:bg-orange-200' },
|
||||
{ level: 1, label: '一般', color: 'bg-yellow-100 text-yellow-700 border-yellow-200 hover:bg-yellow-200' },
|
||||
{ level: 2, label: '熟悉', color: 'bg-green-100 text-green-700 border-green-200 hover:bg-green-200' }
|
||||
].map(({ level, label, color }) => {
|
||||
const isSelected = selectedConfidence === level
|
||||
return (
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { CardState } from '../data'
|
||||
import { CardState } from '@/lib/data/reviewSimpleData'
|
||||
|
||||
interface SimpleProgressProps {
|
||||
current: number
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { CardState } from '@/lib/data/reviewSimpleData'
|
||||
import { SimpleTestHeader } from './SimpleTestHeader'
|
||||
|
||||
interface VocabChoiceTestProps {
|
||||
card: CardState
|
||||
options: string[]
|
||||
onAnswer: (confidence: number) => void
|
||||
onSkip: () => void
|
||||
}
|
||||
|
||||
export function VocabChoiceTest({ card, options, onAnswer, onSkip }: VocabChoiceTestProps) {
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
|
||||
const [showResult, setShowResult] = useState(false)
|
||||
|
||||
const handleAnswerSelect = useCallback((answer: string) => {
|
||||
if (showResult) return
|
||||
|
||||
setSelectedAnswer(answer)
|
||||
setShowResult(true)
|
||||
|
||||
// 判斷答案是否正確,正確給3分,錯誤給1分
|
||||
const isCorrect = answer === card.word
|
||||
const confidence = isCorrect ? 2 : 0
|
||||
|
||||
// 延遲一點再調用回調,讓用戶看到選擇結果
|
||||
setTimeout(() => {
|
||||
onAnswer(confidence)
|
||||
// 重置狀態為下一題準備
|
||||
setSelectedAnswer(null)
|
||||
setShowResult(false)
|
||||
}, 1500)
|
||||
}, [showResult, card.word, onAnswer])
|
||||
|
||||
const handleSkipClick = useCallback(() => {
|
||||
onSkip()
|
||||
}, [onSkip])
|
||||
|
||||
const isCorrect = selectedAnswer === card.word
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg p-8">
|
||||
<SimpleTestHeader
|
||||
title="詞彙選擇"
|
||||
cefr={card.cefr}
|
||||
/>
|
||||
|
||||
{/* 問題區域 */}
|
||||
<div className="mb-8">
|
||||
<div className="bg-gray-50 rounded-lg p-6 mb-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-3 text-left">定義</h3>
|
||||
<p className="text-gray-700 text-left text-lg leading-relaxed">{card.definition}</p>
|
||||
</div>
|
||||
<p className="text-lg text-gray-700 text-left">
|
||||
請選擇符合上述定義的英文詞彙:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 選項區域 */}
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{options.map((option, index) => {
|
||||
const isSelected = selectedAnswer === option
|
||||
const isCorrectOption = option === card.word
|
||||
|
||||
let buttonClass = 'p-4 rounded-lg border-2 text-center font-medium transition-all duration-200 cursor-pointer active:scale-95'
|
||||
|
||||
if (showResult) {
|
||||
if (isSelected && isCorrectOption) {
|
||||
// 選中且正確
|
||||
buttonClass += ' bg-green-100 text-green-700 border-green-200 ring-2 ring-green-400'
|
||||
} else if (isSelected && !isCorrectOption) {
|
||||
// 選中但錯誤
|
||||
buttonClass += ' bg-red-100 text-red-700 border-red-200 ring-2 ring-red-400'
|
||||
} else if (!isSelected && isCorrectOption) {
|
||||
// 未選中但是正確答案
|
||||
buttonClass += ' bg-green-50 text-green-600 border-green-200'
|
||||
} else {
|
||||
// 未選中且非正確答案
|
||||
buttonClass += ' bg-gray-50 text-gray-500 border-gray-200'
|
||||
}
|
||||
} else {
|
||||
// 未答題狀態
|
||||
buttonClass += ' bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100'
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleAnswerSelect(option)}
|
||||
disabled={showResult}
|
||||
className={buttonClass}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 結果顯示區域 */}
|
||||
{showResult && (
|
||||
<div className="mb-6">
|
||||
<div className={`p-4 rounded-lg ${isCorrect ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
|
||||
<div className="flex items-center mb-3">
|
||||
<span className={`text-2xl mr-3 ${isCorrect ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{isCorrect ? '✅' : '❌'}
|
||||
</span>
|
||||
<h3 className={`text-lg font-semibold ${isCorrect ? 'text-green-800' : 'text-red-800'}`}>
|
||||
{isCorrect ? '答對了!' : '答錯了'}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-left">
|
||||
<p className="text-gray-700">
|
||||
<strong>正確答案:</strong> {card.word}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<strong>發音:</strong> {card.pronunciation}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<strong>例句:</strong> "{card.example}"
|
||||
</p>
|
||||
<p className="text-gray-600 text-sm">
|
||||
{card.exampleTranslation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 跳過按鈕 */}
|
||||
{!showResult && (
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={handleSkipClick}
|
||||
className="w-full border border-gray-300 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
跳過
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,84 +1,210 @@
|
|||
/**
|
||||
* 簡化的複習會話 Hook - Store 包裝器
|
||||
* 提供便捷的 Store 訪問方式,但所有邏輯都統一在 Store 中
|
||||
*/
|
||||
import { useReducer, useEffect, useMemo } from 'react'
|
||||
import { SIMPLE_CARDS, CardState, sortCardsByPriority } from '@/lib/data/reviewSimpleData'
|
||||
|
||||
import { useReviewSessionStore } from '@/store/review/useReviewSessionStore'
|
||||
import { flashcardsService } from '@/lib/services/flashcards'
|
||||
import type { ExtendedFlashcard, ReviewMode } from '@/lib/types/review'
|
||||
|
||||
interface UseReviewSessionReturn {
|
||||
// 狀態 (從 Store 直接取得)
|
||||
currentCard: ExtendedFlashcard | null
|
||||
dueCards: ExtendedFlashcard[]
|
||||
currentCardIndex: number
|
||||
isLoadingCard: boolean
|
||||
mode: ReviewMode
|
||||
isAutoSelecting: boolean
|
||||
showNoDueCards: boolean
|
||||
showComplete: boolean
|
||||
|
||||
// 操作 (Store 的包裝方法)
|
||||
loadNextCard: () => Promise<void>
|
||||
setCurrentCard: (card: ExtendedFlashcard | null) => void
|
||||
setCurrentCardIndex: (index: number) => void
|
||||
setMode: (mode: ReviewMode) => void
|
||||
setAutoSelecting: (auto: boolean) => void
|
||||
setShowNoDueCards: (show: boolean) => void
|
||||
setShowComplete: (show: boolean) => void
|
||||
resetSession: () => void
|
||||
interface ReviewState {
|
||||
cards: CardState[]
|
||||
score: { correct: number; total: number }
|
||||
isComplete: boolean
|
||||
}
|
||||
|
||||
export function useReviewSession(): UseReviewSessionReturn {
|
||||
// 從 Store 取得狀態和操作
|
||||
const store = useReviewSessionStore()
|
||||
type ReviewAction =
|
||||
| { type: 'LOAD_PROGRESS'; payload: ReviewState }
|
||||
| { type: 'ANSWER_CARD'; payload: { cardId: string; confidence: number } }
|
||||
| { type: 'SKIP_CARD'; payload: { cardId: string } }
|
||||
| { type: 'RESTART' }
|
||||
|
||||
// 載入卡片的業務邏輯 (唯一的 Hook 專有邏輯)
|
||||
const loadNextCard = async () => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
// 內部狀態更新函數
|
||||
const updateCardState = (
|
||||
cards: CardState[],
|
||||
cardIndex: number,
|
||||
updates: Partial<CardState>
|
||||
): CardState[] => {
|
||||
return cards.map((card, index) =>
|
||||
index === cardIndex
|
||||
? { ...card, ...updates }
|
||||
: card
|
||||
)
|
||||
}
|
||||
|
||||
const result = await flashcardsService.getDueFlashcards(50)
|
||||
const reviewReducer = (state: ReviewState, action: ReviewAction): ReviewState => {
|
||||
switch (action.type) {
|
||||
case 'LOAD_PROGRESS':
|
||||
return action.payload
|
||||
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
store.setDueCards(result.data)
|
||||
store.setCurrentCard(result.data[0])
|
||||
store.setCurrentCardIndex(0)
|
||||
store.setShowNoDueCards(false)
|
||||
} else {
|
||||
store.setShowNoDueCards(true)
|
||||
store.setCurrentCard(null)
|
||||
store.setDueCards([])
|
||||
case 'ANSWER_CARD': {
|
||||
const { cardId, confidence } = action.payload
|
||||
const isCorrect = confidence >= 2
|
||||
|
||||
// 使用 Map 優化查找性能
|
||||
const cardMap = new Map(state.cards.map((card, index) => [card.id, { card, index }]))
|
||||
const cardData = cardMap.get(cardId)
|
||||
|
||||
if (!cardData) return state
|
||||
|
||||
const { index: cardIndex } = cardData
|
||||
const currentCard = state.cards[cardIndex]
|
||||
|
||||
const updatedCards = updateCardState(state.cards, cardIndex, {
|
||||
isCompleted: isCorrect,
|
||||
wrongCount: isCorrect ? currentCard.wrongCount : currentCard.wrongCount + 1
|
||||
})
|
||||
|
||||
const newScore = {
|
||||
correct: state.score.correct + (isCorrect ? 1 : 0),
|
||||
total: state.score.total + 1
|
||||
}
|
||||
|
||||
const remainingCards = updatedCards.filter(card => !card.isCompleted)
|
||||
const isComplete = remainingCards.length === 0
|
||||
|
||||
return {
|
||||
cards: updatedCards,
|
||||
score: newScore,
|
||||
isComplete
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '載入卡片失敗'
|
||||
store.setError(errorMessage)
|
||||
store.setShowNoDueCards(true)
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
|
||||
case 'SKIP_CARD': {
|
||||
const { cardId } = action.payload
|
||||
|
||||
// 使用 Map 優化查找性能
|
||||
const cardMap = new Map(state.cards.map((card, index) => [card.id, { card, index }]))
|
||||
const cardData = cardMap.get(cardId)
|
||||
|
||||
if (!cardData) return state
|
||||
|
||||
const { index: cardIndex } = cardData
|
||||
const currentCard = state.cards[cardIndex]
|
||||
|
||||
const updatedCards = updateCardState(state.cards, cardIndex, {
|
||||
skipCount: currentCard.skipCount + 1
|
||||
})
|
||||
|
||||
const remainingCards = updatedCards.filter(card => !card.isCompleted)
|
||||
const isComplete = remainingCards.length === 0
|
||||
|
||||
return {
|
||||
cards: updatedCards,
|
||||
score: state.score,
|
||||
isComplete
|
||||
}
|
||||
}
|
||||
|
||||
case 'RESTART':
|
||||
return {
|
||||
cards: SIMPLE_CARDS,
|
||||
score: { correct: 0, total: 0 },
|
||||
isComplete: false
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export function useReviewSession() {
|
||||
// 使用 useReducer 統一狀態管理
|
||||
const [state, dispatch] = useReducer(reviewReducer, {
|
||||
cards: SIMPLE_CARDS,
|
||||
score: { correct: 0, total: 0 },
|
||||
isComplete: false
|
||||
})
|
||||
|
||||
const { cards, score, isComplete } = state
|
||||
|
||||
// 智能排序獲取當前卡片 - 使用 useMemo 優化性能
|
||||
const sortedCards = useMemo(() => sortCardsByPriority(cards), [cards])
|
||||
const incompleteCards = useMemo(() =>
|
||||
sortedCards.filter((card: CardState) => !card.isCompleted),
|
||||
[sortedCards]
|
||||
)
|
||||
const currentCard = incompleteCards[0] // 總是選擇優先級最高的未完成卡片
|
||||
|
||||
// localStorage進度保存和載入
|
||||
useEffect(() => {
|
||||
// 載入保存的進度
|
||||
const savedProgress = localStorage.getItem('review-progress')
|
||||
if (savedProgress) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedProgress)
|
||||
const saveTime = new Date(parsed.timestamp)
|
||||
const now = new Date()
|
||||
const isToday = saveTime.toDateString() === now.toDateString()
|
||||
|
||||
if (isToday && parsed.cards) {
|
||||
dispatch({
|
||||
type: 'LOAD_PROGRESS',
|
||||
payload: {
|
||||
cards: parsed.cards,
|
||||
score: parsed.score || { correct: 0, total: 0 },
|
||||
isComplete: parsed.isComplete || false
|
||||
}
|
||||
})
|
||||
console.log('📖 載入保存的複習進度')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('進度載入失敗:', error)
|
||||
localStorage.removeItem('review-progress')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 保存進度到localStorage
|
||||
const saveProgress = () => {
|
||||
const progress = {
|
||||
cards,
|
||||
score,
|
||||
isComplete,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
localStorage.setItem('review-progress', JSON.stringify(progress))
|
||||
console.log('💾 進度已保存')
|
||||
}
|
||||
|
||||
// 處理答題 - 使用 dispatch 統一管理
|
||||
const handleAnswer = (confidence: number) => {
|
||||
if (!currentCard) return
|
||||
|
||||
dispatch({
|
||||
type: 'ANSWER_CARD',
|
||||
payload: { cardId: currentCard.id, confidence }
|
||||
})
|
||||
|
||||
// 保存進度
|
||||
setTimeout(() => saveProgress(), 100) // 延遲一點確保狀態更新
|
||||
}
|
||||
|
||||
// 處理跳過 - 使用 dispatch 統一管理
|
||||
const handleSkip = () => {
|
||||
if (!currentCard) return
|
||||
|
||||
dispatch({
|
||||
type: 'SKIP_CARD',
|
||||
payload: { cardId: currentCard.id }
|
||||
})
|
||||
|
||||
// 保存進度
|
||||
setTimeout(() => saveProgress(), 100) // 延遲一點確保狀態更新
|
||||
}
|
||||
|
||||
// 重新開始 - 重置所有狀態
|
||||
const handleRestart = () => {
|
||||
dispatch({ type: 'RESTART' })
|
||||
localStorage.removeItem('review-progress') // 清除保存的進度
|
||||
console.log('🔄 複習進度已重置')
|
||||
}
|
||||
|
||||
return {
|
||||
// 狀態 (直接從 Store 映射)
|
||||
currentCard: store.currentCard,
|
||||
dueCards: store.dueCards,
|
||||
currentCardIndex: store.currentCardIndex,
|
||||
isLoadingCard: store.isLoading,
|
||||
mode: store.mode,
|
||||
isAutoSelecting: store.isAutoSelecting,
|
||||
showNoDueCards: store.showNoDueCards,
|
||||
showComplete: store.showComplete,
|
||||
// 狀態
|
||||
cards,
|
||||
score,
|
||||
isComplete,
|
||||
currentCard,
|
||||
sortedCards,
|
||||
|
||||
// 操作 (Store 方法的直接映射 + 業務邏輯)
|
||||
loadNextCard,
|
||||
setCurrentCard: store.setCurrentCard,
|
||||
setCurrentCardIndex: store.setCurrentCardIndex,
|
||||
setMode: store.setMode,
|
||||
setAutoSelecting: store.setAutoSelecting,
|
||||
setShowNoDueCards: store.setShowNoDueCards,
|
||||
setShowComplete: store.setShowComplete,
|
||||
resetSession: store.resetSession
|
||||
// 動作
|
||||
handleAnswer,
|
||||
handleSkip,
|
||||
handleRestart
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// 模擬真實API數據結構
|
||||
import apiSeeds from './components/api_seeds.json'
|
||||
import apiSeeds from './api_seeds.json'
|
||||
|
||||
// API響應接口 (匹配真實API結構 + 同義詞擴展)
|
||||
export interface ApiFlashcard {
|
||||
|
|
@ -6,10 +6,10 @@ import { ReviewRunner } from '@/components/review/core/ReviewRunner'
|
|||
import { ProgressTracker } from '@/components/review/ui/ProgressTracker'
|
||||
|
||||
// Store imports
|
||||
import { useReviewSessionStore } from '@/store/review/useReviewSessionStore'
|
||||
import { useTestQueueStore } from '@/store/review/useTestQueueStore'
|
||||
import { useTestResultStore } from '@/store/review/useTestResultStore'
|
||||
import { useReviewDataStore } from '@/store/review/useReviewDataStore'
|
||||
import { useReviewSessionStore } from '../../store/review/useReviewSessionStore'
|
||||
import { useTestQueueStore } from '@/archive/store/review/useTestQueueStore'
|
||||
import { useTestResultStore } from '@/archive/store/review/useTestResultStore'
|
||||
import { useReviewDataStore } from '@/archive/store/review/useReviewDataStore'
|
||||
import exampleData from './example-data.json'
|
||||
|
||||
// 動態測試資料集配置
|
||||
|
|
@ -13,12 +13,12 @@ import { LoadingStates } from '@/components/review/ui/LoadingStates'
|
|||
import { ReviewRunner } from '@/components/review/core/ReviewRunner'
|
||||
|
||||
// 狀態管理
|
||||
import { useReviewSessionStore } from '@/store/review/useReviewSessionStore'
|
||||
import { useTestQueueStore } from '@/store/review/useTestQueueStore'
|
||||
import { useTestResultStore } from '@/store/review/useTestResultStore'
|
||||
import { useReviewDataStore } from '@/store/review/useReviewDataStore'
|
||||
import { useReviewUIStore } from '@/store/review/useReviewUIStore'
|
||||
import { ReviewService } from '@/lib/services/review/reviewService'
|
||||
import { useReviewSessionStore } from '../../store/review/useReviewSessionStore'
|
||||
import { useTestQueueStore } from '@/archive/store/review/useTestQueueStore'
|
||||
import { useTestResultStore } from '@/archive/store/review/useTestResultStore'
|
||||
import { useReviewDataStore } from '@/archive/store/review/useReviewDataStore'
|
||||
import { useReviewUIStore } from '@/archive/store/review/useReviewUIStore'
|
||||
import { ReviewService } from '@/archive/lib/services/review/reviewService'
|
||||
|
||||
export default function LearnPage() {
|
||||
const router = useRouter()
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { memo, useCallback, useMemo } from 'react'
|
||||
import { useTestQueueStore } from '@/store/review/useTestQueueStore'
|
||||
import { useTestQueueStore } from '@/archive/store/review/useTestQueueStore'
|
||||
|
||||
/**
|
||||
* 智能導航控制器
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useReviewSessionStore } from '@/store/review/useReviewSessionStore'
|
||||
import { useTestQueueStore } from '@/store/review/useTestQueueStore'
|
||||
import { useTestResultStore } from '@/store/review/useTestResultStore'
|
||||
import { useReviewUIStore } from '@/store/review/useReviewUIStore'
|
||||
import { useReviewSessionStore } from '@/archive/store/review/useReviewSessionStore'
|
||||
import { useTestQueueStore } from '@/archive/store/review/useTestQueueStore'
|
||||
import { useTestResultStore } from '@/archive/store/review/useTestResultStore'
|
||||
import { useReviewUIStore } from '@/archive/store/review/useReviewUIStore'
|
||||
import { SmartNavigationController } from './NavigationController'
|
||||
import { ProgressBar } from '../ui/ProgressBar'
|
||||
import {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import { TestItem } from '@/store/review/useTestQueueStore'
|
||||
import { TestItem } from '@/archive/store/review/useTestQueueStore'
|
||||
import { TestStatusIndicator, TestStats, TestProgressBar, TestStatusList } from './TestStatusIndicator'
|
||||
|
||||
interface TaskListModalProps {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { memo } from 'react'
|
||||
import { TestItem } from '@/store/review/useTestQueueStore'
|
||||
import { TestItem } from '@/archive/store/review/useTestQueueStore'
|
||||
|
||||
/**
|
||||
* 測驗狀態指示器
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* 簡化的複習會話 Hook - Store 包裝器
|
||||
* 提供便捷的 Store 訪問方式,但所有邏輯都統一在 Store 中
|
||||
*/
|
||||
|
||||
import { useReviewSessionStore } from '../../../note/archive/store/review/useReviewSessionStore'
|
||||
import { flashcardsService } from '@/lib/services/flashcards'
|
||||
import type { ExtendedFlashcard, ReviewMode } from '@/archive/lib/types/review'
|
||||
|
||||
interface UseReviewSessionReturn {
|
||||
// 狀態 (從 Store 直接取得)
|
||||
currentCard: ExtendedFlashcard | null
|
||||
dueCards: ExtendedFlashcard[]
|
||||
currentCardIndex: number
|
||||
isLoadingCard: boolean
|
||||
mode: ReviewMode
|
||||
isAutoSelecting: boolean
|
||||
showNoDueCards: boolean
|
||||
showComplete: boolean
|
||||
|
||||
// 操作 (Store 的包裝方法)
|
||||
loadNextCard: () => Promise<void>
|
||||
setCurrentCard: (card: ExtendedFlashcard | null) => void
|
||||
setCurrentCardIndex: (index: number) => void
|
||||
setMode: (mode: ReviewMode) => void
|
||||
setAutoSelecting: (auto: boolean) => void
|
||||
setShowNoDueCards: (show: boolean) => void
|
||||
setShowComplete: (show: boolean) => void
|
||||
resetSession: () => void
|
||||
}
|
||||
|
||||
export function useReviewSession(): UseReviewSessionReturn {
|
||||
// 從 Store 取得狀態和操作
|
||||
const store = useReviewSessionStore()
|
||||
|
||||
// 載入卡片的業務邏輯 (唯一的 Hook 專有邏輯)
|
||||
const loadNextCard = async () => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
const result = await flashcardsService.getDueFlashcards(50)
|
||||
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
store.setDueCards(result.data)
|
||||
store.setCurrentCard(result.data[0])
|
||||
store.setCurrentCardIndex(0)
|
||||
store.setShowNoDueCards(false)
|
||||
} else {
|
||||
store.setShowNoDueCards(true)
|
||||
store.setCurrentCard(null)
|
||||
store.setDueCards([])
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '載入卡片失敗'
|
||||
store.setError(errorMessage)
|
||||
store.setShowNoDueCards(true)
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 狀態 (直接從 Store 映射)
|
||||
currentCard: store.currentCard,
|
||||
dueCards: store.dueCards,
|
||||
currentCardIndex: store.currentCardIndex,
|
||||
isLoadingCard: store.isLoading,
|
||||
mode: store.mode,
|
||||
isAutoSelecting: store.isAutoSelecting,
|
||||
showNoDueCards: store.showNoDueCards,
|
||||
showComplete: store.showComplete,
|
||||
|
||||
// 操作 (Store 方法的直接映射 + 業務邏輯)
|
||||
loadNextCard,
|
||||
setCurrentCard: store.setCurrentCard,
|
||||
setCurrentCardIndex: store.setCurrentCardIndex,
|
||||
setMode: store.setMode,
|
||||
setAutoSelecting: store.setAutoSelecting,
|
||||
setShowNoDueCards: store.setShowNoDueCards,
|
||||
setShowComplete: store.setShowComplete,
|
||||
resetSession: store.resetSession
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Mock 數據用於複習功能測試
|
||||
import { ExtendedFlashcard } from '@/lib/types/review'
|
||||
import { ExtendedFlashcard } from '@/archive/lib/types/review'
|
||||
|
||||
export const mockDueCards: ExtendedFlashcard[] = [
|
||||
{
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { flashcardsService } from '@/lib/services/flashcards'
|
||||
import { ExtendedFlashcard } from '@/lib/types/review'
|
||||
import { TestItem } from '@/store/review/useTestQueueStore'
|
||||
import { isTestMode, getMockCompletedTests } from '@/lib/mock/reviewMockData'
|
||||
import { ExtendedFlashcard } from '@/archive/lib/types/review'
|
||||
import { TestItem } from '@/archive/store/review/useTestQueueStore'
|
||||
import { isTestMode, getMockCompletedTests } from '@/archive/lib/mock/reviewMockData'
|
||||
|
||||
// 複習會話服務
|
||||
export class ReviewService {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { useReviewDataStore } from '../useReviewDataStore'
|
||||
import { mockDueCards } from '@/lib/mock/reviewMockData'
|
||||
import { mockDueCards } from '@/archive/lib/mock/reviewMockData'
|
||||
|
||||
// Mock flashcardsService
|
||||
vi.mock('@/lib/services/flashcards', () => ({
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { useTestQueueStore, TestItem, ReviewMode } from '../useTestQueueStore'
|
||||
import { mockDueCards } from '@/lib/mock/reviewMockData'
|
||||
import { mockDueCards } from '@/archive/lib/mock/reviewMockData'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/lib/utils/cefrUtils', () => ({
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { create } from 'zustand'
|
||||
import { subscribeWithSelector } from 'zustand/middleware'
|
||||
import { flashcardsService } from '@/lib/services/flashcards'
|
||||
import { ExtendedFlashcard } from '@/lib/types/review'
|
||||
import { isTestMode, getMockDueCards } from '@/lib/mock/reviewMockData'
|
||||
import { ReviewService } from '@/lib/services/review/reviewService'
|
||||
import { ExtendedFlashcard } from '@/archive/lib/types/review'
|
||||
import { isTestMode, getMockDueCards } from '@/archive/lib/mock/reviewMockData'
|
||||
import { ReviewService } from '@/archive/lib/services/review/reviewService'
|
||||
|
||||
// 數據狀態接口
|
||||
interface ReviewDataState {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { create } from 'zustand'
|
||||
import { subscribeWithSelector } from 'zustand/middleware'
|
||||
import type { ReviewSessionStore } from '@/lib/types/review'
|
||||
import type { ReviewSessionStore } from '@/archive/lib/types/review'
|
||||
|
||||
export const useReviewSessionStore = create<ReviewSessionStore>()(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { create } from 'zustand'
|
||||
import { subscribeWithSelector } from 'zustand/middleware'
|
||||
import { getReviewTypesByCEFR } from '@/lib/utils/cefrUtils'
|
||||
import { isTestMode, getTestModeReviewTypes } from '@/lib/mock/reviewMockData'
|
||||
import { isTestMode, getTestModeReviewTypes } from '@/archive/lib/mock/reviewMockData'
|
||||
|
||||
// 複習模式類型
|
||||
export type ReviewMode = 'flip-memory' | 'vocab-choice' | 'vocab-listening' | 'sentence-listening' | 'sentence-fill' | 'sentence-reorder' | 'sentence-speaking'
|
||||
|
|
@ -2,7 +2,7 @@ import { create } from 'zustand'
|
|||
import { subscribeWithSelector } from 'zustand/middleware'
|
||||
import { flashcardsService } from '@/lib/services/flashcards'
|
||||
import { ReviewMode } from './useTestQueueStore'
|
||||
import { isTestMode } from '@/lib/mock/reviewMockData'
|
||||
import { isTestMode } from '@/archive/lib/mock/reviewMockData'
|
||||
|
||||
// 測試結果狀態接口
|
||||
interface TestResultState {
|
||||
Loading…
Reference in New Issue