week-5-module-2 / src /components /Calculator.tsx
iurbinah's picture
Update src/components/Calculator.tsx
4bbb3b1 verified
import React, { useState, useMemo, useRef } from 'react';
import { SlideData } from '../data/slides';
import { styles } from '../styles/appStyles';
// Number of decimals to show when revealing correct answers
const PRECISION = 4; // means usually need fewer decimals than proportions
const TOLERANCE = 1e-6;
/**
* Interactive worksheet for the *difference of two means* (large‑sample Z / small‑sample t).
*
* • Press ⏎ inside any input to jump to the next one (added July 2025).
*/
const InteractiveWorksheet = ({
defaults,
}: {
/* Expected keys in defaults:
x1, s1, n1, x2, s2, n2 — all strings that parse to numbers. */
defaults: NonNullable<SlideData['calculatorDefaults']>;
}) => {
/* --------------------- pull parameters from the slide object --------------------- */
const x1 = parseFloat((defaults as any).x1);
const s1 = parseFloat((defaults as any).s1);
const n1 = parseInt((defaults as any).n1, 10);
const x2 = parseFloat((defaults as any).x2);
const s2 = parseFloat((defaults as any).s2);
const n2 = parseInt((defaults as any).n2, 10);
/* --------------------------- worksheet state setup --------------------------- */
const initialAnswers = {
diff: '',
var1: '',
var2: '',
varDiff: '',
se: '',
moe: '',
ciLower: '',
ciUpper: '',
} as const;
const [userAnswers, setUserAnswers] = useState<typeof initialAnswers>(
initialAnswers
);
const [isSubmitted, setIsSubmitted] = useState(false);
/** Refs to each <input> so we can move focus */
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
/** Handle typing in any input cell */
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setUserAnswers((prev) => ({ ...prev, [name]: value }));
};
/** Handle Enter key: jump to next input */
const handleKeyDown = (index: number) => (
e: React.KeyboardEvent<HTMLInputElement>
) => {
if (e.key === 'Enter') {
e.preventDefault();
let nextIdx = index + 1;
while (nextIdx < inputRefs.current.length) {
const nextEl = inputRefs.current[nextIdx];
if (nextEl && !nextEl.disabled) {
nextEl.focus();
break;
}
nextIdx += 1;
}
}
};
/* ---------------------------- correct solutions ----------------------------- */
const correctAnswers = useMemo(() => {
const diff = x1 - x2;
const var1 = (s1 ** 2) / n1;
const var2 = (s2 ** 2) / n2;
const varDiff = var1 + var2;
const se = Math.sqrt(varDiff);
const moe = 1.96 * se; // 95 % Z‑critical, good for n > 30 each side
const ciLower = diff - moe;
const ciUpper = diff + moe;
return { diff, var1, var2, varDiff, se, moe, ciLower, ciUpper } as const;
}, [x1, s1, n1, x2, s2, n2]);
/* ------------------------------- utilities ---------------------------------- */
const checkSingleAnswer = (
userVal: string,
correctVal: number
): 'unchecked' | 'correct' | 'incorrect' => {
if (!isSubmitted) return 'unchecked';
const num = parseFloat(userVal.replace(',', '.'));
if (Number.isNaN(num)) return 'incorrect';
return Math.abs(num - correctVal) < TOLERANCE ? 'correct' : 'incorrect';
};
/* --------------------------- worksheet blueprint --------------------------- */
const calculationSteps: {
key: keyof typeof initialAnswers;
desc: string;
formula: string;
}[] = [
{ key: 'diff', desc: 'Difference', formula: 'x̄₁ − x̄₂' },
{
key: 'var1',
desc: 'Variance of Group 1',
formula: 's₁² / n₁',
},
{
key: 'var2',
desc: 'Variance of Group 2',
formula: 's₂² / n₂',
},
{
key: 'varDiff',
desc: 'Variance of Difference',
formula: 'Var(x̄₁) + Var(x̄₂)',
},
{ key: 'se', desc: 'Standard Error (SE)', formula: '√Var(Difference)' },
{
key: 'moe',
desc: 'Margin of Error (95 % CI)',
formula: '1.96 × SE',
},
{ key: 'ciLower', desc: 'CI Lower', formula: 'Difference − MoE' },
{ key: 'ciUpper', desc: 'CI Upper', formula: 'Difference + MoE' },
];
/* -------------------------------------------------------------------------- */
/* render */
/* -------------------------------------------------------------------------- */
return (
<div style={styles.worksheetContainer}>
{/* Problem header ---------------------------------------------------- */}
<div style={styles.worksheetHeader}>
<div style={styles.worksheetDataItem}>
<strong>Group 1:</strong>&nbsp; x̄₁ = {x1}, s₁ = {s1}, n₁ = {n1}
</div>
<div style={styles.worksheetDataItem}>
<strong>Group 2:</strong>&nbsp; x̄₂ = {x2}, s₂ = {s2}, n₂ = {n2}
</div>
</div>
{/* Column headings ---------------------------------------------------- */}
<div
style={{
...styles.worksheetRow,
borderBottom: '2px solid #003366',
paddingBottom: '0.5rem',
marginBottom: '0.5rem',
}}
>
<div style={styles.worksheetRowHeader}>Calculation Step</div>
<div style={{ ...styles.worksheetRowHeader, textAlign: 'center' }}>
Formula
</div>
<div style={{ ...styles.worksheetRowHeader, textAlign: 'center' }}>
Your Answer
</div>
<div style={styles.worksheetRowHeader}>Check</div>
</div>
{/* Dynamic rows ------------------------------------------------------- */}
{calculationSteps.map((step, idx) => {
const userAnswer = userAnswers[step.key];
const hasInput = userAnswer.trim() !== '';
const status = checkSingleAnswer(
userAnswer,
correctAnswers[step.key]
);
const showFeedback = isSubmitted && hasInput;
const isIncorrect = showFeedback && status === 'incorrect';
return (
<div key={step.key} style={styles.worksheetRow}>
<div style={styles.worksheetDescription}>{step.desc}</div>
<div style={styles.worksheetFormula}>{step.formula}</div>
{/* Input and feedback */}
<div style={{ display: 'flex', flexDirection: 'column' }}>
<input
type="number"
name={step.key}
value={userAnswer}
onChange={handleInputChange}
onKeyDown={handleKeyDown(idx)}
ref={(el) => {
inputRefs.current[idx] = el;
}}
style={styles.worksheetInput}
aria-label={step.desc}
disabled={isSubmitted}
/>
{isIncorrect && (
<span style={styles.correctAnswerText}>
Correct:&nbsp;
{correctAnswers[step.key].toFixed(PRECISION)}
</span>
)}
</div>
<div
style={{
...styles.feedbackIcon,
...(status === 'correct'
? styles.correctFeedback
: styles.incorrectFeedback),
}}
>
{showFeedback && (status === 'correct' ? '✓' : '✗')}
</div>
</div>
);
})}
{/* Action buttons ----------------------------------------------------- */}
<div
style={{
marginTop: '1.5rem',
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
gap: '1rem',
}}
>
<button
style={styles.button}
onClick={() => setIsSubmitted(true)}
disabled={isSubmitted}
>
Check My Work
</button>
<button
style={{ ...styles.button, backgroundColor: '#6c757d' }}
onClick={() => {
setUserAnswers(initialAnswers);
setIsSubmitted(false);
if (inputRefs.current[0]) inputRefs.current[0].focus();
}}
>
Reset
</button>
</div>
</div>
);
};
export default InteractiveWorksheet;