-
+
Visual labs are open today in the browser, with no install.
diff --git a/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx b/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx
index ecc4e618..39f60990 100644
--- a/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx
+++ b/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx
@@ -1,42 +1,6 @@
-import type { Metadata } from "next";
-import { SITE_SHARE_IMAGE } from "@/lib/site";
-import { visualLabGroups } from "@/lib/visual-labs";
-import { AreaHero, AreaPage, AreaSection, CardGrid, InfoCard } from "@/components/areas/area-page";
-
-export const metadata: Metadata = {
- title: "Visual labs",
- description: "No code. Move parts, press Run, and watch every test case pass or fail on the board.",
- alternates: { canonical: "/labs/visual" },
- openGraph: { title: "Visual labs on Edorax", url: "/labs/visual", images: [SITE_SHARE_IMAGE] },
-};
+import { redirect } from "next/navigation";
+// Visual labs are a filter on /labs now. Old links (and group anchors such as #networking) land on it.
export default function VisualLabsPage() {
- const groups = visualLabGroups();
- return (
-
-
- {groups.map((group) => (
-
-
- {group.labs.map((lab) => (
-
- ))}
-
-
- ))}
-
- );
+ redirect("/labs?kind=visual");
}
diff --git a/frontend/src/components/areas/filter-chips.tsx b/frontend/src/components/areas/filter-chips.tsx
new file mode 100644
index 00000000..a8dc9240
--- /dev/null
+++ b/frontend/src/components/areas/filter-chips.tsx
@@ -0,0 +1,74 @@
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+
+export type FilterOption = { value: string | null; label: string; count?: number; soon?: boolean };
+
+/**
+ * A row of one-tap filters. Each chip is a plain link that sets one search parameter and keeps the
+ * others, so a filtered view can be shared, and it works before the page's scripts load.
+ */
+export function FilterChips({
+ label,
+ param,
+ options,
+ current,
+ basePath,
+ params,
+}: {
+ /** Read out to screen readers and shown on wide screens before the chips. */
+ label: string;
+ param: string;
+ options: FilterOption[];
+ current: string | null;
+ basePath: string;
+ /** The page's other filters, kept when this one changes. */
+ params: Record;
+}) {
+ function href(value: string | null) {
+ const query = new URLSearchParams();
+ for (const [key, val] of Object.entries({ ...params, [param]: value })) if (val) query.set(key, val);
+ const text = query.toString();
+ return text ? `${basePath}?${text}` : basePath;
+ }
+
+ return (
+
+ );
+}
+
+/** Reads one filter from search params, accepting only the listed values. */
+export function pickFilter(value: string | string[] | undefined, allowed: readonly string[]): string | null {
+ const one = Array.isArray(value) ? value[0] : value;
+ return one && allowed.includes(one) ? one : null;
+}
diff --git a/frontend/src/lib/learn-areas.ts b/frontend/src/lib/learn-areas.ts
index 84987176..d6803484 100644
--- a/frontend/src/lib/learn-areas.ts
+++ b/frontend/src/lib/learn-areas.ts
@@ -124,7 +124,7 @@ export const LAB_KINDS: readonly LabKind[] = [
{
id: "visual",
name: "Visual labs",
- href: "/labs/visual",
+ href: "/labs?kind=visual",
status: "live",
tagline: "No code. Move parts, press Run, watch the checks.",
summary:
@@ -150,7 +150,7 @@ export const LAB_KINDS: readonly LabKind[] = [
},
];
-/** Visual labs grouped for the catalogue. Slugs come from the lab registry; `id` is the anchor on /labs/visual. */
+/** Visual labs grouped for the catalogue. Slugs come from the lab registry; `id` is the topic filter on /labs (?topic=). */
export const VISUAL_LAB_GROUPS: readonly { id: string; title: string; description: string; prefix: (slug: string) => boolean }[] = [
{
id: "system-design-challenges",
From 88ec03113949765bacec582717e1ab81a0bc7728 Mon Sep 17 00:00:00 2001
From: Zeeshan Adil
Date: Fri, 6 Nov 2026 18:10:10 +0500
Subject: [PATCH 2/3] Practice, Puzzles and Tutors: one chip row and one panel
each
Each page had four or five stacked sections. Now a subject, puzzle kind
or tutor topic is one tap, and it shows one preview next to a short list
of how it works. The longer walkthroughs fold away.
---
.../app/(learn)/(catalog)/practice/page.tsx | 123 +++++++++++++-----
.../app/(learn)/(catalog)/puzzles/page.tsx | 90 +++++++++----
.../src/app/(learn)/(catalog)/voice/page.tsx | 118 ++++++++++++-----
3 files changed, 245 insertions(+), 86 deletions(-)
diff --git a/frontend/src/app/(learn)/(catalog)/practice/page.tsx b/frontend/src/app/(learn)/(catalog)/practice/page.tsx
index 5ad790b5..e50c20a8 100644
--- a/frontend/src/app/(learn)/(catalog)/practice/page.tsx
+++ b/frontend/src/app/(learn)/(catalog)/practice/page.tsx
@@ -1,18 +1,63 @@
import type { Metadata } from "next";
-import { BookOpen, MessageCircleQuestion, Repeat, Timer } from "lucide-react";
import { SITE_SHARE_IMAGE } from "@/lib/site";
import { areaById } from "@/lib/learn-areas";
-import { AreaHero, AreaPage, AreaSection, CardGrid, ComingSoonPanel, InfoCard, StepList } from "@/components/areas/area-page";
+import { AreaHero, AreaPage, ComingSoonPanel, StepList } from "@/components/areas/area-page";
+import { FilterChips, pickFilter } from "@/components/areas/filter-chips";
export const metadata: Metadata = {
title: "Five-minute practice",
- description: "Master one topic five minutes a day: today's question, quick quizzes, quick reads and a review queue.",
+ description: "Master one subject five minutes a day: today's question, quick quizzes, quick reads and a review queue.",
alternates: { canonical: "/practice" },
openGraph: { title: "Five-minute practice on Edorax", url: "/practice", images: [SITE_SHARE_IMAGE] },
};
-export default function PracticePage() {
+// One practice app per subject. System Design is built first; the rest follow the courses they practise.
+const SUBJECTS = [
+ {
+ id: "system-design",
+ label: "System Design",
+ note: "Built first.",
+ question: "A page loads fast in the morning and slowly at night. Which part do you check first?",
+ options: ["The database", "The cache hit rate", "The load balancer", "I am not sure yet"],
+ },
+ {
+ id: "ai-engineering",
+ label: "AI engineering interview",
+ note: "After System Design.",
+ question: "Your RAG answer cites the right document but quotes the wrong number. Where do you look first?",
+ options: ["How the document was chunked", "The retrieval ranking", "The prompt", "I am not sure yet"],
+ },
+ {
+ id: "networking",
+ label: "Networking",
+ note: "Planned.",
+ question: "A site opens when you type its IP address but not its name. What is broken?",
+ options: ["DNS", "The router", "The TLS certificate", "I am not sure yet"],
+ },
+ {
+ id: "databases",
+ label: "Databases",
+ note: "Planned.",
+ question: "Two people book the last seat at the same moment. What stops both of them getting it?",
+ options: ["A row lock in a transaction", "A cache", "An index", "I am not sure yet"],
+ },
+] as const;
+
+const FORMATS = [
+ ["Today's question", "One a day. Answer first, read after."],
+ ["Quick quizzes", "Three to five questions in about two minutes."],
+ ["Quick reads", "One idea on one screen, ending with a check."],
+ ["Review queue", "Misses come back a few days later."],
+] as const;
+
+type SearchParams = Promise>;
+
+export default async function PracticePage({ searchParams }: { searchParams: SearchParams }) {
const area = areaById("practice");
+ const query = await searchParams;
+ const id = pickFilter(query.subject, SUBJECTS.map((s) => s.id)) ?? SUBJECTS[0].id;
+ const subject = SUBJECTS.find((s) => s.id === id)!;
+
return (
-
-
- } title="Today's question" body="One question a day for your topic. Answer it first; read about it after." />
- } title="Quick quizzes" body="Three to five questions in about two minutes. No timer, no penalty for a wrong answer." />
- } title="Quick reads" body="One idea on one screen, with a diagram where it helps. Each ends with a question to check it stuck." />
- } title="Review queue" body="What you got wrong comes back a few days later, then less often as you get it right." />
-
-
-
-
-
+ ({ value: s.id, label: s.label }))}
/>
-
+
-
-
-
System Design · Day 12
-
A page loads fast in the morning and slowly at night. Which part do you check first?
- {["The database", "The cache hit rate", "The load balancer", "I am not sure yet"].map((option) => (
+ {subject.options.map((option) => (
{option}
))}
-
+
+ {FORMATS.map(([title, body]) => (
+
+
{title}
+
{body}
+
+ ))}
+
+
+
+
+
+ A session, start to finish
+
+
+
+
+
- System Design comes first. Until then, every course chapter ends with a short quiz you can retake.
+ Until then, every course chapter ends with a short quiz you can retake.
);
diff --git a/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx b/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx
index c66991ae..ce9f4025 100644
--- a/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx
+++ b/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
-import { CalendarDays, Gauge, Route, Trophy } from "lucide-react";
import { SITE_SHARE_IMAGE } from "@/lib/site";
import { areaById } from "@/lib/learn-areas";
-import { AreaHero, AreaPage, AreaSection, CardGrid, ComingSoonPanel, InfoCard } from "@/components/areas/area-page";
+import { AreaHero, AreaPage, ComingSoonPanel } from "@/components/areas/area-page";
+import { FilterChips, pickFilter } from "@/components/areas/filter-chips";
export const metadata: Metadata = {
title: "Puzzles",
@@ -11,8 +11,48 @@ export const metadata: Metadata = {
openGraph: { title: "Daily puzzles on Edorax", url: "/puzzles", images: [SITE_SHARE_IMAGE] },
};
-export default function PuzzlesPage() {
+// The kinds take turns through the week. Each has a one-line goal and an example of the day's prompt.
+const KINDS = [
+ {
+ id: "bottleneck",
+ label: "Find the bottleneck",
+ goal: "A small system is struggling. Tap the one part that gives out first.",
+ example: "Users → load balancer → two app servers → one database at 95% CPU. Checkout takes 9 seconds.",
+ },
+ {
+ id: "trace",
+ label: "Trace the request",
+ goal: "Put the steps of a request in order, from the tap on a phone to the answer on screen.",
+ example: "DNS lookup · TLS handshake · cache miss · database read · response rendered",
+ },
+ {
+ id: "fix",
+ label: "Fix the diagram",
+ goal: "One link or one part is wrong. Find it and fix it in as few moves as you can.",
+ example: "A queue sits between the browser and the load balancer. One move fixes it.",
+ },
+ {
+ id: "odd",
+ label: "Odd one out",
+ goal: "Four ideas, one does not belong. Say which, and the reason appears.",
+ example: "Read replica · write-ahead log · CDN edge cache · sticky session",
+ },
+] as const;
+
+const RULES = [
+ ["One a day", "New at midnight in your time zone. Yesterday's stays open to practise."],
+ ["A few tries", "Each wrong try shows a clue, so you learn something either way."],
+ ["Daily leaderboard", "Fewer tries and a faster solve rank higher. It resets every day; joining is your choice."],
+] as const;
+
+type SearchParams = Promise>;
+
+export default async function PuzzlesPage({ searchParams }: { searchParams: SearchParams }) {
const area = areaById("puzzles");
+ const query = await searchParams;
+ const id = pickFilter(query.kind, KINDS.map((k) => k.id)) ?? KINDS[0].id;
+ const kind = KINDS.find((k) => k.id === id)!;
+
return (
-
-
- } title="Find the bottleneck" body="A small system is struggling. Tap the one part that gives out first." />
- } title="Trace the request" body="Put the steps of a request in order, from the tap on a phone to the answer on screen." />
-
-
-
-
-
-
-
- } title="One a day" body="A new puzzle at midnight in your time zone. Missed one? Yesterday's stays open to practise." />
-
- } title="Daily leaderboard" body="Fewer tries and a faster solve put you higher on today's board. It resets every day, so everyone starts level. Joining it is your choice, like every leaderboard on Edorax." />
-
-
-
-
+
+ ({ value: k.id, label: k.label }))} />
+
+
+
+
+ {kind.label}
+
{kind.goal}
+
+ Example:
+ {kind.example}
+
+
+
+ {RULES.map(([title, body]) => (
+
+
{title}
+
{body}
+
+ ))}
+
+
+
+
The visual labs are the closest thing today: a design, a set of checks, and a reason for every result.
diff --git a/frontend/src/app/(learn)/(catalog)/voice/page.tsx b/frontend/src/app/(learn)/(catalog)/voice/page.tsx
index 581a7329..97c25a15 100644
--- a/frontend/src/app/(learn)/(catalog)/voice/page.tsx
+++ b/frontend/src/app/(learn)/(catalog)/voice/page.tsx
@@ -1,7 +1,8 @@
import type { Metadata } from "next";
import { SITE_SHARE_IMAGE } from "@/lib/site";
import { areaById } from "@/lib/learn-areas";
-import { AreaHero, AreaPage, AreaSection, ComingSoonPanel, StepList } from "@/components/areas/area-page";
+import { AreaHero, AreaPage, ComingSoonPanel } from "@/components/areas/area-page";
+import { FilterChips, pickFilter } from "@/components/areas/filter-chips";
export const metadata: Metadata = {
title: "On-call tutors",
@@ -10,16 +11,59 @@ export const metadata: Metadata = {
openGraph: { title: "On-call tutors on Edorax", url: "/voice", images: [SITE_SHARE_IMAGE] },
};
-const CALL: { who: "agent" | "you"; text: string }[] = [
- { who: "agent", text: "Hi, I'm your System Design tutor. What do you already know, and what feels hard?" },
- { who: "you", text: "Yes. I know what a server is, but not much more." },
- { who: "agent", text: "Then we'll start as a beginner. Module 2 is about why waiting suddenly gets long. Want a two-minute version first?" },
- { who: "you", text: "Wait, what is a load balancer for?" },
- { who: "agent", text: "It spreads requests over several servers, so one busy server does not slow everyone down. That is module 4. Shall we go there next?" },
+type Line = { who: "agent" | "you"; text: string };
+
+// One tutor per topic. The sample calls are written out; calls are not open yet.
+const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [
+ {
+ id: "system-design",
+ label: "System Design",
+ note: "The first tutor.",
+ call: [
+ { who: "agent", text: "Hi, I'm your System Design tutor. What do you already know, and what feels hard?" },
+ { who: "you", text: "I know what a server is, but not much more." },
+ { who: "agent", text: "Then we start as a beginner. Module 2 is about why waiting suddenly gets long. Want the two-minute version first?" },
+ { who: "you", text: "Wait, what is a load balancer for?" },
+ { who: "agent", text: "It spreads requests over several servers, so one busy server does not slow everyone down. That is module 4. Shall we go there next?" },
+ ],
+ },
+ {
+ id: "networking",
+ label: "Networking",
+ note: "Planned.",
+ call: [
+ { who: "agent", text: "Hi, I'm your Networking tutor. Where would you like to start?" },
+ { who: "you", text: "My laptop says connected, but nothing loads." },
+ { who: "agent", text: "Good place to start. Let's check three things in order: your address, the way out of your network, then names. Can you open a terminal?" },
+ ],
+ },
+ {
+ id: "ai-engineering",
+ label: "AI engineering",
+ note: "Planned.",
+ call: [
+ { who: "agent", text: "Hi, I'm your AI engineering tutor. Building something, or getting ready for an interview?" },
+ { who: "you", text: "Interview. I always get stuck on evals." },
+ { who: "agent", text: "Then let's practise one answer out loud: how would you know your RAG system got better after a change?" },
+ ],
+ },
];
-export default function VoicePage() {
+const HOW = [
+ ["Call the topic", "Press Call and say where you want to start."],
+ ["Say your level", "It checks with one or two questions instead of trusting a label."],
+ ["Ask anything", "Interrupt at any point. It answers briefly, then brings you back."],
+ ["Your place is kept", "The next call picks up where you stopped."],
+] as const;
+
+type SearchParams = Promise>;
+
+export default async function VoicePage({ searchParams }: { searchParams: SearchParams }) {
const area = areaById("voice");
+ const query = await searchParams;
+ const id = pickFilter(query.topic, TOPICS.map((t) => t.id)) ?? TOPICS[0]!.id;
+ const topic = TOPICS.find((t) => t.id === id)!;
+
return (
-
-
- {CALL.map((line, i) => (
-
+
-
- System Design gets the first tutor. Every chapter can already be heard word by word, at any speed from 0.75x to 4x.
+
+ System Design gets the first tutor. Until then, every narrated course is on the Audiobooks shelf.
);
From 09c5f8ece8ccb322a353c608d6740c88c3bcf05c Mon Sep 17 00:00:00 2001
From: Zeeshan Adil
Date: Fri, 6 Nov 2026 18:17:51 +0500
Subject: [PATCH 3/3] Tutors: on call, shown as a phone call
Each tutor is a contact you can call, marked on call. Picking one shows
an in-progress call screen: the tutor on the line, a call timer, the
sample call as captions, mute, speaker and end controls, and the Call
button (opens soon).
---
.../src/app/(learn)/(catalog)/voice/page.tsx | 105 ++++++++++++++----
1 file changed, 83 insertions(+), 22 deletions(-)
diff --git a/frontend/src/app/(learn)/(catalog)/voice/page.tsx b/frontend/src/app/(learn)/(catalog)/voice/page.tsx
index 97c25a15..58ab65ab 100644
--- a/frontend/src/app/(learn)/(catalog)/voice/page.tsx
+++ b/frontend/src/app/(learn)/(catalog)/voice/page.tsx
@@ -2,7 +2,10 @@ import type { Metadata } from "next";
import { SITE_SHARE_IMAGE } from "@/lib/site";
import { areaById } from "@/lib/learn-areas";
import { AreaHero, AreaPage, ComingSoonPanel } from "@/components/areas/area-page";
-import { FilterChips, pickFilter } from "@/components/areas/filter-chips";
+import Link from "next/link";
+import { MicOff, Phone, PhoneOff, Volume2 } from "lucide-react";
+import { pickFilter } from "@/components/areas/filter-chips";
+import { cn } from "@/lib/utils";
export const metadata: Metadata = {
title: "On-call tutors",
@@ -13,12 +16,12 @@ export const metadata: Metadata = {
type Line = { who: "agent" | "you"; text: string };
-// One tutor per topic. The sample calls are written out; calls are not open yet.
+// One tutor per topic, on call: you ring them, they pick up. The sample calls are written out; calls are not open yet.
const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [
{
id: "system-design",
label: "System Design",
- note: "The first tutor.",
+ note: "Opens first.",
call: [
{ who: "agent", text: "Hi, I'm your System Design tutor. What do you already know, and what feels hard?" },
{ who: "you", text: "I know what a server is, but not much more." },
@@ -30,7 +33,7 @@ const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [
{
id: "networking",
label: "Networking",
- note: "Planned.",
+ note: "Opens later.",
call: [
{ who: "agent", text: "Hi, I'm your Networking tutor. Where would you like to start?" },
{ who: "you", text: "My laptop says connected, but nothing loads." },
@@ -40,7 +43,7 @@ const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [
{
id: "ai-engineering",
label: "AI engineering",
- note: "Planned.",
+ note: "Opens later.",
call: [
{ who: "agent", text: "Hi, I'm your AI engineering tutor. Building something, or getting ready for an interview?" },
{ who: "you", text: "Interview. I always get stuck on evals." },
@@ -50,7 +53,7 @@ const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [
];
const HOW = [
- ["Call the topic", "Press Call and say where you want to start."],
+ ["Pick a tutor and call", "One tutor per topic. Press Call and say where you want to start."],
["Say your level", "It checks with one or two questions instead of trusting a label."],
["Ask anything", "Interrupt at any point. It answers briefly, then brings you back."],
["Your place is kept", "The next call picks up where you stopped."],
@@ -71,30 +74,88 @@ export default async function VoicePage({ searchParams }: { searchParams: Search
title={area.name}
status={area.status}
mood="happy"
- intro="A tutor for every topic, one call away. Say how much you already know and start talking. It answers in plain words and keeps your place."
+ intro="A tutor on call for every topic. Pick one, press Call and start talking. Ask anything that feels hard; it answers in plain words and keeps your place for the next call."
/>
-
- ({ value: t.id, label: t.label }))} />
-
+ {/* Tutors are on call: pick one, then call. The list reads like contacts, each with its line status. */}
+
-
-
- {topic.label} tutor. {topic.note} A sample call, written out.
-
-
+ {/* The call screen: what it looks like with the tutor on the line. Captions are the sample call. */}
+