123 lines
3.0 KiB
TypeScript
123 lines
3.0 KiB
TypeScript
import React, { useState, useCallback, ReactNode } from 'react'
|
|
import { ErrorReportButton, TestHeader } from '@/components/review/shared'
|
|
import { BaseReviewProps } from '@/types/review'
|
|
|
|
/**
|
|
* 基礎測驗元件 - 提供所有測驗元件的共用功能
|
|
* 包含:標題、錯誤回報、測驗狀態管理、統一布局結構
|
|
*/
|
|
|
|
export interface BaseTestComponentProps extends BaseReviewProps {
|
|
testTitle: string
|
|
instructions?: string
|
|
children: ReactNode
|
|
showResult?: boolean
|
|
resultContent?: ReactNode
|
|
className?: string
|
|
}
|
|
|
|
interface TestState {
|
|
hasAnswered: boolean
|
|
userAnswer: string | null
|
|
showResult: boolean
|
|
}
|
|
|
|
export const BaseTestComponent: React.FC<BaseTestComponentProps> = ({
|
|
cardData,
|
|
testTitle,
|
|
instructions,
|
|
children,
|
|
showResult = false,
|
|
resultContent,
|
|
onReportError,
|
|
disabled = false,
|
|
className = ''
|
|
}) => {
|
|
const [testState, setTestState] = useState<TestState>({
|
|
hasAnswered: false,
|
|
userAnswer: null,
|
|
showResult: false
|
|
})
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
{/* 錯誤回報按鈕 */}
|
|
<div className="flex justify-end mb-2">
|
|
<ErrorReportButton onClick={onReportError} />
|
|
</div>
|
|
|
|
{/* 主要測驗容器 */}
|
|
<div className="bg-white rounded-xl shadow-lg p-8">
|
|
{/* 測驗標題 */}
|
|
<TestHeader
|
|
title={testTitle}
|
|
difficultyLevel={cardData.cefr}
|
|
/>
|
|
|
|
{/* 說明文字 */}
|
|
{instructions && (
|
|
<p className="text-lg text-gray-700 mb-6 text-left">
|
|
{instructions}
|
|
</p>
|
|
)}
|
|
|
|
{/* 測驗內容區域 */}
|
|
<div className="test-content">
|
|
{children}
|
|
</div>
|
|
|
|
{/* 結果顯示區域 */}
|
|
{(showResult || testState.showResult) && resultContent && (
|
|
<div className="mt-6">
|
|
{resultContent}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Hook for managing test answer state
|
|
* 提供測驗答題狀態管理的標準化邏輯
|
|
*/
|
|
export const useTestAnswer = (onAnswer: (answer: string) => void) => {
|
|
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
|
|
const [showResult, setShowResult] = useState(false)
|
|
|
|
const handleAnswer = useCallback((answer: string) => {
|
|
if (showResult) return
|
|
|
|
setSelectedAnswer(answer)
|
|
setShowResult(true)
|
|
onAnswer(answer)
|
|
}, [showResult, onAnswer])
|
|
|
|
const resetAnswer = useCallback(() => {
|
|
setSelectedAnswer(null)
|
|
setShowResult(false)
|
|
}, [])
|
|
|
|
return {
|
|
selectedAnswer,
|
|
showResult,
|
|
handleAnswer,
|
|
resetAnswer
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigation integration types
|
|
* 為後續的導航系統整合做準備
|
|
*/
|
|
export interface TestNavigationState {
|
|
status: 'unanswered' | 'answered' | 'skipped'
|
|
canSkip: boolean
|
|
canContinue: boolean
|
|
}
|
|
|
|
export interface TestNavigationProps {
|
|
navigationState: TestNavigationState
|
|
onSkip?: () => void
|
|
onContinue?: () => void
|
|
} |