diff --git a/src/components/calculator/CaratEstimator.tsx b/src/components/calculator/CaratEstimator.tsx index 15ca5a6..b9271ca 100644 --- a/src/components/calculator/CaratEstimator.tsx +++ b/src/components/calculator/CaratEstimator.tsx @@ -37,9 +37,14 @@ export function CaratEstimator() { const [length, setLength] = useState(''); const [width, setWidth] = useState(''); const [depth, setDepth] = useState(''); - const [sg, setSg] = useState('3.52'); + const [sgSource, setSgSource] = useState<'preset' | 'custom'>('preset'); + const [sgPreset, setSgPreset] = useState('3.52'); + const [sgCustom, setSgCustom] = useState(''); const [shape, setShape] = useState('round-brilliant'); + // Get the effective SG value + const sg = sgSource === 'custom' ? sgCustom : sgPreset; + const result = useMemo(() => { const l = parseFloat(length); const w = parseFloat(width); @@ -140,24 +145,37 @@ export function CaratEstimator() { - setSg(e.target.value)} - placeholder="Custom SG" - className="w-full mt-2 px-3 py-2 rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-crystal-500 text-sm" - /> + {/* Custom SG input - only shown when "Custom" is selected */} + {sgSource === 'custom' && ( + setSgCustom(e.target.value)} + placeholder="Enter custom SG (e.g., 3.50)" + aria-label="Custom specific gravity value" + autoFocus + className="w-full mt-2 px-3 py-2 rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-crystal-500 text-sm" + /> + )} diff --git a/src/components/quiz/Exam.tsx b/src/components/quiz/Exam.tsx index 310a7ef..2f9679d 100644 --- a/src/components/quiz/Exam.tsx +++ b/src/components/quiz/Exam.tsx @@ -90,6 +90,16 @@ export function Exam({ const unansweredCount = questions.length - answeredCount; const isFlagged = isQuestionFlagged(currentQuestion.id); + // Calculate pacing indicator + const timeUsed = timeLimit - timeRemaining; + const suggestedTimePerQuestion = timeLimit / questions.length; + const expectedQuestionIndex = Math.floor(timeUsed / suggestedTimePerQuestion); + const actualQuestionIndex = state.currentIndex; + // Consider "on pace" if within 1 question of expected, or ahead + const isPacingOk = actualQuestionIndex >= expectedQuestionIndex - 1; + // Calculate how many questions ahead/behind + const pacingDiff = actualQuestionIndex - expectedQuestionIndex; + return (
{/* Header bar */} @@ -102,6 +112,31 @@ export function Exam({ isExpired={isTimeExpired} /> + {/* Pacing indicator - hidden on mobile, shows on sm+ */} +
= 0 + ? `${pacingDiff} question${pacingDiff !== 1 ? 's' : ''} ahead of suggested pace` + : `${Math.abs(pacingDiff)} question${Math.abs(pacingDiff) !== 1 ? 's' : ''} behind suggested pace` + } + > + {isPacingOk ? ( + + ) : ( + + )} + {isPacingOk ? 'On pace' : 'Behind pace'} +
+ {/* Quick stats */} ('all'); + const [reviewFilter, setReviewFilter] = useState<'all' | 'incorrect' | 'correct' | 'flagged'>('all'); const grade = getGrade(results.percentage); const feedback = getFeedback(results.percentage); @@ -42,10 +42,12 @@ export function ExamResults({ const filteredResults = results.results.filter(r => { if (reviewFilter === 'incorrect') return !r.isCorrect; if (reviewFilter === 'correct') return r.isCorrect; + if (reviewFilter === 'flagged') return r.wasFlagged; return true; }); const incorrectCount = results.results.filter(r => !r.isCorrect).length; + const flaggedCount = results.results.filter(r => r.wasFlagged).length; if (showReview) { return ( @@ -64,11 +66,12 @@ export function ExamResults({
{/* Filter tabs */} -
+
{[ { value: 'all' as const, label: `All (${results.results.length})` }, { value: 'incorrect' as const, label: `Incorrect (${incorrectCount})` }, { value: 'correct' as const, label: `Correct (${results.score})` }, + ...(flaggedCount > 0 ? [{ value: 'flagged' as const, label: `Flagged (${flaggedCount})` }] : []), ].map(tab => (