Skip to content

Commit d5113ef

Browse files
committed
fix(staffml): propagate track filter to all components and URL state
Track selection (TinyML, Edge, etc.) now transforms Topic objects at the source in filteredAreas, so TopicCard, TopicDetail, and AreaOverview all show track-accurate question counts and histograms. Drill links carry track param to practice page. Hero stats update dynamically. URL persists filter via ?track= for bookmarking.
1 parent c4177a7 commit d5113ef

2 files changed

Lines changed: 64 additions & 10 deletions

File tree

interviews/staffml/src/app/page.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use client";
22

3-
import { useState, useEffect, useMemo } from "react";
3+
import { Suspense, useState, useEffect, useMemo } from "react";
44
import { Search, X, Target, Crosshair, Flame, Calendar } from "lucide-react";
55
import { motion, AnimatePresence } from "framer-motion";
66
import Link from "next/link";
7+
import { useSearchParams, useRouter } from "next/navigation";
78
import {
89
getAreas, getVaultStats, getAreaStyle, getAreaForTopic, searchTopics,
910
type Topic,
@@ -17,13 +18,44 @@ import { isDailyCompleted } from "@/lib/daily";
1718
import manifest from "@/data/vault-manifest.json";
1819
import { ECOSYSTEM_BASE } from "@/lib/env";
1920

20-
export default function HomePage() {
21+
function formatTrackLabel(t: string) {
22+
return t === "tinyml" ? "TinyML" : t.charAt(0).toUpperCase() + t.slice(1);
23+
}
24+
25+
export default function HomePageWrapper() {
26+
return (
27+
<Suspense fallback={
28+
<div className="flex-1 flex items-center justify-center">
29+
<Search className="w-6 h-6 text-textTertiary animate-pulse" />
30+
</div>
31+
}>
32+
<HomePage />
33+
</Suspense>
34+
);
35+
}
36+
37+
function HomePage() {
38+
const searchParams = useSearchParams();
39+
const router = useRouter();
2140
const [mounted, setMounted] = useState(false);
2241
const [query, setQuery] = useState("");
2342
const [selectedTopic, setSelectedTopic] = useState<Topic | null>(null);
2443
const [selectedAreas, setSelectedAreas] = useState<Set<string>>(new Set());
2544
const [showAllAreas, setShowAllAreas] = useState(false);
26-
const [selectedTrack, setSelectedTrack] = useState<string | null>(null);
45+
46+
// Initialize track from URL param, sync changes back to URL
47+
const validTracks = getTracks();
48+
const initialTrack = searchParams.get("track");
49+
const [selectedTrack, setSelectedTrackState] = useState<string | null>(
50+
initialTrack && validTracks.includes(initialTrack) ? initialTrack : null
51+
);
52+
const setSelectedTrack = (track: string | null) => {
53+
setSelectedTrackState(track);
54+
const params = new URLSearchParams(searchParams.toString());
55+
if (track) { params.set("track", track); } else { params.delete("track"); }
56+
const qs = params.toString();
57+
router.replace(qs ? `/?${qs}` : "/", { scroll: false });
58+
};
2759
const [isReturning, setIsReturning] = useState(false);
2860
const [streakCount, setStreakCount] = useState(0);
2961
const [attemptCount, setAttemptCount] = useState(0);
@@ -154,7 +186,15 @@ export default function HomePage() {
154186
</h1>
155187
<p className="text-[13px] text-textSecondary mb-3">
156188
Free, open-source interview prep for ML systems engineers.{" "}
157-
{stats.totalQuestions.toLocaleString()} questions across compute, memory, latency, and more.
189+
{selectedTrack ? (
190+
<>
191+
<span className="font-semibold">{formatTrackLabel(selectedTrack)}</span> track:{" "}
192+
{filteredAreas.reduce((s, a) => s + a.questionCount, 0).toLocaleString()} of{" "}
193+
{stats.totalQuestions.toLocaleString()} questions.
194+
</>
195+
) : (
196+
<>{stats.totalQuestions.toLocaleString()} questions across compute, memory, latency, and more.</>
197+
)}
158198
</p>
159199

160200
{/* Welcome guide */}
@@ -384,6 +424,7 @@ export default function HomePage() {
384424
>
385425
<TopicDetail topic={selectedTopic}
386426
areaName={selectedArea?.name || ""} style={selectedStyle}
427+
selectedTrack={selectedTrack}
387428
onClose={() => setSelectedTopic(null)} />
388429
</motion.div>
389430
)}
@@ -416,6 +457,7 @@ export default function HomePage() {
416457
</div>
417458
<TopicDetail topic={selectedTopic}
418459
areaName={selectedArea?.name || ""} style={selectedStyle}
460+
selectedTrack={selectedTrack}
419461
onClose={() => setSelectedTopic(null)} />
420462
</motion.div>
421463
</>

interviews/staffml/src/components/vault/TopicDetail.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@ import SectionDivider from "./SectionDivider";
1212

1313
const LEVEL_IDS = LEVEL_DEFS.map(l => l.id);
1414

15-
export default function TopicDetail({ topic, areaName, style, onClose }: {
16-
topic: Topic; areaName: string; style: AreaStyle; onClose: () => void;
15+
function formatTrackName(t: string) {
16+
return t === "tinyml" ? "TinyML" : t.charAt(0).toUpperCase() + t.slice(1);
17+
}
18+
19+
export default function TopicDetail({ topic, areaName, style, onClose, selectedTrack }: {
20+
topic: Topic; areaName: string; style: AreaStyle; onClose: () => void; selectedTrack?: string | null;
1721
}) {
1822
const [drillLevel, setDrillLevel] = useState<string | null>(null);
1923
const Icon = style.icon;
2024

2125
const [prevId, setPrevId] = useState(topic.id);
2226
if (topic.id !== prevId) { setPrevId(topic.id); setDrillLevel(null); }
2327

28+
// Build track-aware query params for drill links
29+
const trackParam = selectedTrack ? `&track=${selectedTrack}` : "";
30+
2431
const levelQs = drillLevel ? topic.questionsByLevel[drillLevel] || [] : [];
2532

2633
return (
@@ -66,7 +73,7 @@ export default function TopicDetail({ topic, areaName, style, onClose }: {
6673
{levelQs.length} question{levelQs.length !== 1 ? "s" : ""}
6774
</p>
6875
</div>
69-
<Link href={`/practice?topic=${topic.id}&level=${drillLevel}`}
76+
<Link href={`/practice?topic=${topic.id}&level=${drillLevel}${trackParam}`}
7077
className="flex items-center gap-1.5 px-4 py-2 text-[13px] font-bold rounded-lg transition-all"
7178
style={{ backgroundColor: style.primary, color: "#101014" }}>
7279
<Target className="w-4 h-4" /> Drill
@@ -151,7 +158,7 @@ export default function TopicDetail({ topic, areaName, style, onClose }: {
151158
)}
152159

153160
{/* Drill all CTA */}
154-
<Link href={`/practice?topic=${topic.id}`}
161+
<Link href={`/practice?topic=${topic.id}${trackParam}`}
155162
className="flex items-center justify-center gap-2 w-full py-3.5 font-bold rounded-xl text-[14px] transition-all hover:opacity-90"
156163
style={{ backgroundColor: style.primary, color: "#101014" }}>
157164
<Target className="w-4 h-4" />
@@ -163,8 +170,13 @@ export default function TopicDetail({ topic, areaName, style, onClose }: {
163170
<SectionDivider label="Available In" />
164171
<div className="flex gap-2 flex-wrap mt-3">
165172
{topic.tracks.map((t) => (
166-
<span key={t} className="px-3 py-1.5 rounded-lg bg-surface border border-borderSubtle text-[13px] text-textPrimary font-medium">
167-
{t === "tinyml" ? "TinyML" : t.charAt(0).toUpperCase() + t.slice(1)}
173+
<span key={t} className={clsx(
174+
"px-3 py-1.5 rounded-lg text-[13px] font-medium transition-colors",
175+
selectedTrack === t
176+
? "bg-accentBlue/15 border-2 border-accentBlue text-accentBlue font-bold"
177+
: "bg-surface border border-borderSubtle text-textPrimary"
178+
)}>
179+
{formatTrackName(t)}
168180
</span>
169181
))}
170182
</div>

0 commit comments

Comments
 (0)