Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 82 additions & 28 deletions components/GameApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ const GameApp: React.FC<GameAppProps> = ({ mode, assignment }) => {
const [feedback, setFeedback] = useState<Feedback | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [attemptsUsed, setAttemptsUsed] = useState(0);
const [hasRevealed, setHasRevealed] = useState(false);
const maxAttempts = useMemo(() => assignment?.options?.attempts === 'two-then-reveal' ? 2 : Infinity, [assignment]);

const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const maxAttempts = assignment?.options.attemptsPerItem;
const attemptsLimit = typeof maxAttempts === 'number' ? maxAttempts : null;
const outOfAttempts = mode === 'homework' && attemptsLimit !== null && attemptsUsed >= attemptsLimit;

// --- Effects for Initialization and Mode Switching ---
useEffect(() => {
Expand All @@ -55,6 +55,7 @@ const GameApp: React.FC<GameAppProps> = ({ mode, assignment }) => {
setStudentName(savedName);
const storageKey = `ss::${assignment.id}::${savedName}`;
const savedProgress = loadProgress(storageKey);
setAttemptsUsed(savedProgress?.attemptsUsed ?? 0);
if (savedProgress && savedProgress.results.length > 0) {
setProgress(savedProgress);
setShowResumePrompt(true);
Expand Down Expand Up @@ -156,10 +157,14 @@ const GameApp: React.FC<GameAppProps> = ({ mode, assignment }) => {
assignmentId: assignment.id,
version: assignment.version,
student: { name: studentName },
summary: { total: assignment.sentences.length, solvedWithinMax: 0, firstTry: 0, reveals: 0, avgAttempts: 0 },
summary: { correct: 0, total: assignment.sentences.length, reveals: 0 },
attemptsUsed: 0,
results: []
};
setProgress(initialProgress);
const storageKey = `ss::${assignment.id}::${studentName}`;
saveProgress(storageKey, initialProgress);
setAttemptsUsed(0);
setCurrentSentenceIndex(0);
setupNewSentence(0);
setShowResumePrompt(false);
Expand Down Expand Up @@ -293,14 +298,14 @@ const GameApp: React.FC<GameAppProps> = ({ mode, assignment }) => {
...progress,
results: [...progress.results, result],
summary: {
total: progress.summary.total,
solvedWithinMax: progress.summary.solvedWithinMax + solvedInc,
firstTry: progress.summary.firstTry + firstTryInc,
reveals: progress.summary.reveals + (result.revealed ? 1 : 0),
avgAttempts: newAvg
}
...progress.summary,
correct: progress.summary.correct + (result.ok ? 1 : 0),
reveals: progress.summary.reveals + (result.revealed ? 1 : 0)
},
attemptsUsed: 0
};
setProgress(newProgress);
setAttemptsUsed(0);
const storageKey = `ss::${assignment.id}::${studentName}`;
saveProgress(storageKey, newProgress);
};
Expand All @@ -309,6 +314,7 @@ const GameApp: React.FC<GameAppProps> = ({ mode, assignment }) => {
const attempts = attemptsUsed + 1;
setAttemptsUsed(attempts);
let isCorrect = false;
let shouldShowFeedback = true;

if (isChunkMode && currentChunks) {
const userChunks = userSentence.map(w => w.text.trim().toLowerCase());
Expand All @@ -320,7 +326,37 @@ const GameApp: React.FC<GameAppProps> = ({ mode, assignment }) => {
}

if (mode === 'homework') {
updateProgress({ index: currentSentenceIndex, ok: isCorrect, revealed: false, attempts: 1 });
if (isCorrect) {
updateProgress({ index: currentSentenceIndex, ok: true, revealed: false });
} else {
const newAttempts = attemptsUsed + 1;
setAttemptsUsed(newAttempts);
if (progress && assignment) {
const storageKey = `ss::${assignment.id}::${studentName}`;
const newProgress = { ...progress, attemptsUsed: newAttempts };
setProgress(newProgress);
saveProgress(storageKey, newProgress);
}
if (attemptsLimit !== null && newAttempts >= attemptsLimit) {
if (assignment?.options.revealAnswerAfterMaxAttempts) {
handleReveal();
return;
} else {
updateProgress({ index: currentSentenceIndex, ok: false, revealed: false });
}
} else {
shouldShowFeedback = false;
}
}
}

if (shouldShowFeedback) {
setFeedback({
type: isCorrect ? 'success' : 'error',
message: isCorrect ? 'Correct! Well done!' : `Not quite. The correct answer is: "${correctSentenceText}"`,
});
} else {
setFeedback(null);
}
};

Expand Down Expand Up @@ -354,23 +390,41 @@ const GameApp: React.FC<GameAppProps> = ({ mode, assignment }) => {
setStudentName(newName);
};

const renderGameContent = () => {
const isFinalStep = !!feedback && (feedback.type === 'success' || hasRevealed || attemptsUsed >= maxAttempts);
return (
<>
{isLoading ? (
<div className="flex-grow flex items-center justify-center">
<SpinnerIcon />
</div>
) : (
<div className="flex flex-col gap-6 flex-grow">
<DropZone id="available-words" words={availableWords} title="Available Words" onDrop={(wordId, sourceZoneId) => handleDrop(wordId, sourceZoneId, 'available-words')} onWordClick={(wordId) => handleWordClick(wordId, 'available-words')} isDragging={isDragging} setIsDragging={setIsDragging} />
<DropZone id="user-sentence" words={userSentence} title="Your Sentence" onDrop={(wordId, sourceZoneId, index) => handleDrop(wordId, sourceZoneId, 'user-sentence', index)} onWordClick={(wordId) => handleWordClick(wordId, 'user-sentence')} isDragging={isDragging} setIsDragging={setIsDragging} isSentenceZone={true} />

{feedback && (
<div className={`mt-4 p-4 rounded-lg text-center font-semibold text-white ${feedback.type === 'success' ? 'bg-green-500' : 'bg-red-500'}`}>
{feedback.message}
</div>
const renderGameContent = () => (
<>
{isLoading ? (
<div className="flex-grow flex items-center justify-center">
<SpinnerIcon />
</div>
) : (
<div className="flex flex-col gap-6 flex-grow">
<DropZone id="available-words" words={availableWords} title="Available Words" onDrop={(wordId, sourceZoneId) => handleDrop(wordId, sourceZoneId, 'available-words')} onWordClick={(wordId) => handleWordClick(wordId, 'available-words')} isDragging={isDragging} setIsDragging={setIsDragging} />
<DropZone id="user-sentence" words={userSentence} title="Your Sentence" onDrop={(wordId, sourceZoneId, index) => handleDrop(wordId, sourceZoneId, 'user-sentence', index)} onWordClick={(wordId) => handleWordClick(wordId, 'user-sentence')} isDragging={isDragging} setIsDragging={setIsDragging} isSentenceZone={true} />

{feedback && (
<div className={`mt-4 p-4 rounded-lg text-center font-semibold text-white ${feedback.type === 'success' ? 'bg-green-500' : 'bg-red-500'}`}>
{feedback.message}
</div>
)}

<div className="mt-auto pt-6 flex flex-col sm:flex-row gap-4 justify-center items-center flex-wrap">
{!feedback ? (
<>
{mode === 'homework' && (
<>
<button type="button" onClick={handleUndo} className="w-full sm:w-auto px-6 py-3 bg-gray-500 text-white font-bold rounded-lg shadow-md hover:bg-gray-600 transition-colors">Undo</button>
<button type="button" onClick={() => setupNewSentence()} className="w-full sm:w-auto px-6 py-3 bg-yellow-500 text-white font-bold rounded-lg shadow-md hover:bg-yellow-600 transition-colors">Reset</button>
</>
)}
<button type="button" onClick={handleCheckAnswer} disabled={userSentence.length === 0 || outOfAttempts} className="w-full sm:w-auto px-8 py-3 bg-blue-600 text-white font-bold rounded-lg shadow-md hover:bg-blue-700 disabled:bg-gray-400 transition-all transform hover:scale-105">Check Answer</button>
{mode === 'homework' && (
<button type="button" onClick={handleReveal} className="w-full sm:w-auto px-6 py-3 bg-red-600 text-white font-bold rounded-lg shadow-md hover:bg-red-700 transition-colors">Reveal</button>
)}
</>
) : (
<button type="button" onClick={handleNext} className="w-full sm:w-auto px-8 py-3 bg-indigo-600 text-white font-bold rounded-lg shadow-md hover:bg-indigo-700 transition-all transform hover:scale-105">
{currentSentenceIndex < sentences.length - 1 ? 'Next Sentence' : (mode === 'homework' ? 'Finish & See Results' : 'Next Sentence')}
</button>
)}

<div className="mt-auto pt-6 flex flex-col sm:flex-row gap-4 justify-center items-center flex-wrap">
Expand Down
8 changes: 7 additions & 1 deletion types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export interface StudentProgress {
assignmentId: string;
version: number;
student: { name: string };
summary: Summary;
summary: {
correct: number;
total: number;
reveals: number;
};
attemptsUsed: number;

results: Result[];
}
6 changes: 4 additions & 2 deletions utils/__tests__/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
assignmentId: 'a1',
version: 1,
student: { name: 'Alice' },
summary: { total: 1, solvedWithinMax: 0, firstTry: 0, reveals: 0, avgAttempts: 0 },
summary: { correct: 0, total: 1, reveals: 0 },
attemptsUsed: 0,
results: []
};

Expand Down Expand Up @@ -47,7 +48,8 @@
assignmentId: 'a1',
version: 1,
student: { name: 'Alice' },
summary: { total: 1, solvedWithinMax: 0, firstTry: 0, reveals: 0, avgAttempts: 0 },
summary: { correct: 0, total: 1, reveals: 0 },
attemptsUsed: 0,
results: []
};

Expand Down Expand Up @@ -80,7 +82,7 @@

const result = loadProgress(key);

expect(result?.summary).toEqual({

Check failure on line 85 in utils/__tests__/storage.test.ts

View workflow job for this annotation

GitHub Actions / test

utils/__tests__/storage.test.ts > loadProgress > defaults missing summary fields for legacy data

AssertionError: expected { correct: 1, total: 2, reveals: +0 } to deeply equal { total: 2, solvedWithinMax: 1, …(3) } - Expected + Received { - "avgAttempts": 0, - "firstTry": 0, + "correct": 1, "reveals": 0, - "solvedWithinMax": 1, "total": 2, } ❯ utils/__tests__/storage.test.ts:85:29

Check failure on line 85 in utils/__tests__/storage.test.ts

View workflow job for this annotation

GitHub Actions / test

utils/__tests__/storage.test.ts > loadProgress > defaults missing summary fields for legacy data

AssertionError: expected { correct: 1, total: 2, reveals: +0 } to deeply equal { total: 2, solvedWithinMax: 1, …(3) } - Expected + Received { - "avgAttempts": 0, - "firstTry": 0, + "correct": 1, "reveals": 0, - "solvedWithinMax": 1, "total": 2, } ❯ utils/__tests__/storage.test.ts:85:29
total: 2,
solvedWithinMax: 1,
firstTry: 0,
Expand Down
18 changes: 4 additions & 14 deletions utils/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,11 @@ export const loadProgress = (key: string): StudentProgress | null => {
try {
const data = localStorage.getItem(key);
if (!data) return null;

const parsed = JSON.parse(data) as StudentProgress & { summary?: any };

if (parsed?.summary) {
const s = parsed.summary as any;
parsed.summary = {
total: s.total ?? 0,
solvedWithinMax: s.solvedWithinMax ?? s.correct ?? 0,
firstTry: s.firstTry ?? 0,
reveals: s.reveals ?? 0,
avgAttempts: s.avgAttempts ?? 0,
};
const parsed = JSON.parse(data);
if (typeof parsed.attemptsUsed !== 'number') {
parsed.attemptsUsed = 0;
}

return parsed;
return parsed as StudentProgress;
} catch (error) {
console.error("Failed to load progress from localStorage", error);
return null;
Expand Down
Loading