Skip to content

Commit c05a4bb

Browse files
committed
v2.2: Fix 15 bugs, add staged difficulty, consistent UI across all pages
Nav: UserMenu + ThemeToggle on landing page, responsive chat input Dark mode: StreakBadge theme-aware gradient, Sun/Moon toggle on feed Bubble Pop: fixed spawn race condition (useRef for nextId, single interval) Tracing: 65% accuracy threshold + Try Again on fail Color & Sound: Try Again on wrong answer (max 2 attempts per round) Alphabet Pattern: 3-stage difficulty (1 blank → 2 blanks → word completion) Sequence Memory: enhanced glow animation + Watch counter during playback Speech: 3-stage progression (word → phrase → sentence) Video capture: debug status bar, startingRef reset on camera failure Progress: de-duplicate display entries within 2s window Feed: anonymous/named post toggle with checkbox All 25 pages: consistent Sun/Moon ThemeToggle icons (no text toggles)
1 parent dcfa216 commit c05a4bb

25 files changed

Lines changed: 367 additions & 285 deletions

File tree

app/components/StreakBadge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function StreakBadge({ currentStreak, longestStreak }: StreakBadg
2222
padding: "16px 22px",
2323
borderRadius: "var(--r-lg)",
2424
background: currentStreak > 0
25-
? "linear-gradient(135deg, #fff7ed, #fef3c7)"
25+
? "linear-gradient(135deg, var(--feature-peach), var(--feature-green))"
2626
: "var(--bg-secondary)",
2727
border: "1px solid var(--border)",
2828
}}

app/dashboard/child/[id]/page.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import Link from "next/link";
44
import { useState, useEffect, useCallback, use } from "react";
55
import NavLogo from "../../../components/NavLogo";
6+
import ThemeToggle from "../../../components/ThemeToggle";
67
import { getProfile } from "../../../lib/db/childProfile.repository";
78
import { listSessions } from "../../../lib/db/session.repository";
89
import { aggregateBiomarkers } from "../../../lib/db/biomarker.repository";
@@ -126,14 +127,7 @@ export default function ChildDetailPage({
126127
<nav className="nav">
127128
<NavLogo />
128129
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
129-
<button
130-
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
131-
className="btn btn-outline"
132-
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem", gap: 6 }}
133-
aria-label="Toggle theme"
134-
>
135-
{theme === "light" ? "Dark" : "Light"}
136-
</button>
130+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
137131
</div>
138132
</nav>
139133

app/feed/page.tsx

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { getCurrentUserId } from "../lib/identity/identity";
77
import { useAuthGuard } from "../hooks/useAuthGuard";
88
import type { FeedPost } from "../types/feedPost";
99
import NavLogo from "../components/NavLogo";
10+
import ThemeToggle from "../components/ThemeToggle";
11+
import UserMenu from "../components/UserMenu";
1012
import { Plus, X, Send, Trash2 } from "lucide-react";
1113

1214
type Category = "all" | FeedPost["category"];
@@ -43,6 +45,7 @@ export default function FeedPage() {
4345
const [loading, setLoading] = useState(true);
4446
const [userId, setUserId] = useState("");
4547
const [showCompose, setShowCompose] = useState(false);
48+
const [anonymous, setAnonymous] = useState(true);
4649
const [userReactions, setUserReactions] = useState<Set<string>>(new Set());
4750

4851
useEffect(() => {
@@ -87,7 +90,7 @@ export default function FeedPage() {
8790
if (!content.trim() || posting) return;
8891
setPosting(true);
8992
try {
90-
await createPost(content.trim(), category, true);
93+
await createPost(content.trim(), category, anonymous);
9194
setContent("");
9295
setShowCompose(false);
9396
await loadPosts();
@@ -143,22 +146,16 @@ export default function FeedPage() {
143146
{/* Nav */}
144147
<nav className="nav">
145148
<NavLogo />
146-
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
147-
<button
148-
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
149-
className="btn btn-outline"
150-
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem" }}
151-
aria-label="Toggle theme"
152-
>
153-
{theme === "light" ? "Dark" : "Light"}
154-
</button>
149+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
150+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
155151
<Link
156152
href="/kid-dashboard"
157153
className="btn btn-outline"
158154
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem" }}
159155
>
160156
Dashboard
161157
</Link>
158+
<UserMenu />
162159
</div>
163160
</nav>
164161

@@ -252,22 +249,37 @@ export default function FeedPage() {
252249
))}
253250
</div>
254251

255-
<button
256-
onClick={handlePost}
257-
disabled={!content.trim() || posting}
258-
className="btn btn-primary"
259-
style={{
260-
minHeight: 44,
261-
padding: "10px 24px",
262-
display: "flex",
263-
alignItems: "center",
264-
gap: 8,
265-
fontSize: "0.88rem",
266-
}}
267-
>
268-
<Send size={16} />
269-
{posting ? "Posting..." : "Post Anonymously"}
270-
</button>
252+
<div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
253+
<label style={{
254+
display: "flex", alignItems: "center", gap: 8, cursor: "pointer",
255+
fontSize: "0.85rem", fontWeight: 600, color: "var(--text-secondary)",
256+
}}>
257+
<input
258+
type="checkbox"
259+
checked={anonymous}
260+
onChange={(e) => setAnonymous(e.target.checked)}
261+
style={{ width: 18, height: 18, accentColor: "var(--sage-500)" }}
262+
/>
263+
Post Anonymously
264+
</label>
265+
266+
<button
267+
onClick={handlePost}
268+
disabled={!content.trim() || posting}
269+
className="btn btn-primary"
270+
style={{
271+
minHeight: 44,
272+
padding: "10px 24px",
273+
display: "flex",
274+
alignItems: "center",
275+
gap: 8,
276+
fontSize: "0.88rem",
277+
}}
278+
>
279+
<Send size={16} />
280+
{posting ? "Posting..." : anonymous ? "Post Anonymously" : "Post as Me"}
281+
</button>
282+
</div>
271283
</div>
272284
)}
273285

@@ -369,7 +381,7 @@ export default function FeedPage() {
369381
color: "var(--text-primary)",
370382
}}
371383
>
372-
{post.anonymous ? "Anonymous" : "User"}
384+
{post.anonymous ? "Anonymous" : "Community Member"}
373385
</span>
374386
<span
375387
style={{

app/games/breathing/page.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Link from "next/link";
44
import { useState, useEffect, useRef, useCallback } from "react";
55
import { saveDifficulty } from "../../lib/games/difficultyEngine";
66
import NavLogo from "../../components/NavLogo";
7+
import ThemeToggle from "../../components/ThemeToggle";
78

89
type Screen = "start" | "play" | "result";
910
type BreathPhase = "inhale" | "hold" | "exhale" | "rest";
@@ -135,14 +136,7 @@ export default function BreathingGamePage() {
135136
<div className="page">
136137
<nav className="nav">
137138
<NavLogo />
138-
<button
139-
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
140-
className="btn btn-outline"
141-
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem" }}
142-
aria-label="Toggle theme"
143-
>
144-
{theme === "light" ? "Dark" : "Light"}
145-
</button>
139+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
146140
</nav>
147141

148142
<div className="main fade fade-1" style={{ maxWidth: 500, padding: "40px 28px 80px" }}>

app/games/color-sound/page.tsx

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useState, useEffect, useCallback } from "react";
55
import { getDifficulty, saveDifficulty } from "../../lib/games/difficultyEngine";
66
import { speakText } from "../../lib/audio/ttsHelper";
77
import NavLogo from "../../components/NavLogo";
8+
import ThemeToggle from "../../components/ThemeToggle";
89

910
type Screen = "start" | "play" | "result";
1011

@@ -75,6 +76,7 @@ export default function ColorSoundPage() {
7576
const [elapsed, setElapsed] = useState(0);
7677
const [hintText, setHintText] = useState("");
7778
const [isPlaying, setIsPlaying] = useState(false);
79+
const [attemptsThisRound, setAttemptsThisRound] = useState(0);
7880

7981
useEffect(() => {
8082
const saved =
@@ -99,6 +101,7 @@ export default function ColorSoundPage() {
99101
setTargetColor(target);
100102
setSelectedIndex(null);
101103
setFeedback(null);
104+
setAttemptsThisRound(0);
102105
setHintText(`Tap the ${target.name} color`);
103106

104107
// Play tone + voice cue after a short delay
@@ -131,29 +134,48 @@ export default function ColorSoundPage() {
131134
return () => clearInterval(iv);
132135
}, [screen, startTime]);
133136

137+
const advanceRound = useCallback((wasCorrect: boolean) => {
138+
const nextRound = round + 1;
139+
if (nextRound > maxRounds) {
140+
const score = Math.round(((correct + (wasCorrect ? 1 : 0)) / maxRounds) * 100);
141+
saveDifficulty("color-sound", "default", score);
142+
setScreen("result");
143+
} else {
144+
setRound(nextRound);
145+
const config = getDifficulty("color-sound", "default");
146+
generateRound(config.level);
147+
}
148+
}, [round, maxRounds, correct, generateRound]);
149+
134150
const handleSelect = (index: number) => {
135151
if (feedback !== null || !targetColor) return;
136152

137153
setSelectedIndex(index);
138154
const isCorrect = displayColors[index].name === targetColor.name;
139-
setFeedback(isCorrect ? "correct" : "wrong");
140-
if (isCorrect) setCorrect((c) => c + 1);
155+
const attempt = attemptsThisRound + 1;
156+
setAttemptsThisRound(attempt);
141157

142158
// Play the selected color's tone
143159
playTone(displayColors[index].frequency, 300, setIsPlaying);
144160

145-
setTimeout(() => {
146-
const nextRound = round + 1;
147-
if (nextRound > maxRounds) {
148-
const score = Math.round(((correct + (isCorrect ? 1 : 0)) / maxRounds) * 100);
149-
saveDifficulty("color-sound", "default", score);
150-
setScreen("result");
151-
} else {
152-
setRound(nextRound);
153-
const config = getDifficulty("color-sound", "default");
154-
generateRound(config.level);
161+
if (isCorrect) {
162+
setCorrect((c) => c + 1);
163+
setFeedback("correct");
164+
setTimeout(() => advanceRound(true), 800);
165+
} else {
166+
setFeedback("wrong");
167+
if (attempt >= 2) {
168+
// Second wrong — show answer and auto-advance
169+
setTimeout(() => advanceRound(false), 1200);
155170
}
156-
}, 800);
171+
// First wrong — wait for user to click "Try Again" (no auto-advance)
172+
}
173+
};
174+
175+
const handleRetry = () => {
176+
setFeedback(null);
177+
setSelectedIndex(null);
178+
replaySound();
157179
};
158180

159181
const replaySound = () => {
@@ -172,14 +194,7 @@ export default function ColorSoundPage() {
172194
<div className="page">
173195
<nav className="nav">
174196
<NavLogo />
175-
<button
176-
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
177-
className="btn btn-outline"
178-
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem" }}
179-
aria-label="Toggle theme"
180-
>
181-
{theme === "light" ? "Dark" : "Light"}
182-
</button>
197+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
183198
</nav>
184199

185200
<div className="main fade fade-1" style={{ maxWidth: 500, padding: "40px 28px 80px" }}>
@@ -324,15 +339,28 @@ export default function ColorSoundPage() {
324339
</div>
325340

326341
{feedback && (
327-
<div
328-
style={{
329-
marginTop: 20,
330-
fontSize: "1rem",
331-
fontWeight: 700,
332-
color: feedback === "correct" ? "var(--sage-500)" : "var(--peach-300)",
333-
}}
334-
>
335-
{feedback === "correct" ? "Correct!" : `That was ${displayColors[selectedIndex!]?.name}. The answer was ${targetColor?.name}.`}
342+
<div style={{ marginTop: 20, textAlign: "center" }}>
343+
<div
344+
style={{
345+
fontSize: "1rem",
346+
fontWeight: 700,
347+
color: feedback === "correct" ? "var(--sage-500)" : "var(--peach-300)",
348+
marginBottom: 8,
349+
}}
350+
>
351+
{feedback === "correct"
352+
? "Correct!"
353+
: `That was ${displayColors[selectedIndex!]?.name}. The answer was ${targetColor?.name}.`}
354+
</div>
355+
{feedback === "wrong" && attemptsThisRound < 2 && (
356+
<button
357+
onClick={handleRetry}
358+
className="btn btn-primary"
359+
style={{ minHeight: 44, padding: "8px 24px", fontSize: "0.95rem" }}
360+
>
361+
Try Again
362+
</button>
363+
)}
336364
</div>
337365
)}
338366
</div>

app/games/emotion-match/page.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getDifficulty, saveDifficulty } from "../../lib/games/difficultyEngine"
66
import { addGameActivity } from "../../lib/db/gameActivity.repository";
77
import { updateStreak } from "../../lib/db/streak.repository";
88
import NavLogo from "../../components/NavLogo";
9+
import ThemeToggle from "../../components/ThemeToggle";
910

1011
const fredoka = "'Fredoka',sans-serif";
1112

@@ -188,14 +189,7 @@ export default function EmotionQuizPage() {
188189
<div className="page">
189190
<nav className="nav">
190191
<NavLogo />
191-
<button
192-
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
193-
className="btn btn-outline"
194-
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem" }}
195-
aria-label="Toggle theme"
196-
>
197-
{theme === "light" ? "Dark" : "Light"}
198-
</button>
192+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
199193
</nav>
200194

201195
<div className="main fade fade-1" style={{ maxWidth: 600, padding: "40px 28px 80px" }}>

app/games/page.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Link from "next/link";
44
import { useState, useEffect } from "react";
55
import { useAuthGuard } from "../hooks/useAuthGuard";
66
import NavLogo from "../components/NavLogo";
7+
import ThemeToggle from "../components/ThemeToggle";
78

89
const games = [
910
{
@@ -86,14 +87,7 @@ export default function GamesHubPage() {
8687
<nav className="nav">
8788
<NavLogo />
8889
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
89-
<button
90-
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
91-
className="btn btn-outline"
92-
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem", gap: 6 }}
93-
aria-label="Toggle theme"
94-
>
95-
{theme === "light" ? "Dark" : "Light"}
96-
</button>
90+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
9791
<Link
9892
href="/kid-dashboard"
9993
className="btn btn-outline"

app/games/pattern-match/page.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Link from "next/link";
44
import { useState, useEffect, useCallback, useRef } from "react";
55
import { getDifficulty, saveDifficulty } from "../../lib/games/difficultyEngine";
66
import NavLogo from "../../components/NavLogo";
7+
import ThemeToggle from "../../components/ThemeToggle";
78

89
type Screen = "start" | "play" | "result";
910

@@ -179,14 +180,7 @@ export default function PatternMatchPage() {
179180
<div className="page">
180181
<nav className="nav">
181182
<NavLogo />
182-
<button
183-
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
184-
className="btn btn-outline"
185-
style={{ minHeight: 40, padding: "8px 16px", fontSize: "0.9rem" }}
186-
aria-label="Toggle theme"
187-
>
188-
{theme === "light" ? "Dark" : "Light"}
189-
</button>
183+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
190184
</nav>
191185

192186
<div className="main fade fade-1" style={{ maxWidth: 500, padding: "40px 28px 80px" }}>

0 commit comments

Comments
 (0)