From cb1d08141cbf0601fd8e83df1af8b6644586ac4d Mon Sep 17 00:00:00 2001
From: Thomas Orth
Date: Sat, 25 Apr 2026 12:01:23 -0400
Subject: [PATCH] refactor(frontend): split apollo route composition
---
.../assignments/AssignmentBuilderPage.tsx | 122 ++
.../features/assignments/AssignmentsPage.tsx | 25 +
.../src/features/assignments/mock-data.tsx | 14 +
frontend/apps/apollo/src/features/auth/api.ts | 36 +
.../src/features/auth/components/AuthCard.tsx | 29 +
.../auth/components/AuthErrorBanner.tsx | 17 +
.../src/features/auth/routes/LoginPage.tsx | 86 ++
.../src/features/auth/routes/SignupPage.tsx | 98 ++
.../apps/apollo/src/features/auth/session.ts | 122 ++
.../src/features/dashboard/DashboardPage.tsx | 116 ++
.../src/features/dashboard/mock-data.tsx | 63 +
.../src/features/gradebook/GradebookPage.tsx | 139 ++
.../src/features/gradebook/mock-data.ts | 70 +
.../src/features/settings/SettingsPage.tsx | 39 +
.../apollo/src/features/shell/AppSidebar.tsx | 106 ++
.../apollo/src/features/shell/AppTopbar.tsx | 50 +
.../apollo/src/features/shell/AuthLayout.tsx | 41 +
.../src/features/shell/ProtectedAppLayout.tsx | 13 +
.../src/features/students/StudentsPage.tsx | 18 +
.../features/submissions/SubmissionsPage.tsx | 18 +
frontend/apps/apollo/src/lib/api/client.ts | 35 +
frontend/apps/apollo/src/main.tsx | 1295 +----------------
.../apollo/src/routes/assignment-editor.tsx | 10 +
.../apps/apollo/src/routes/assignments.tsx | 10 +
frontend/apps/apollo/src/routes/auth.tsx | 41 +
frontend/apps/apollo/src/routes/dashboard.tsx | 10 +
frontend/apps/apollo/src/routes/gradebook.tsx | 10 +
frontend/apps/apollo/src/routes/protected.tsx | 14 +
frontend/apps/apollo/src/routes/root.tsx | 14 +
frontend/apps/apollo/src/routes/router.tsx | 53 +
frontend/apps/apollo/src/routes/settings.tsx | 10 +
frontend/apps/apollo/src/routes/students.tsx | 10 +
.../apps/apollo/src/routes/submissions.tsx | 10 +
33 files changed, 1454 insertions(+), 1290 deletions(-)
create mode 100644 frontend/apps/apollo/src/features/assignments/AssignmentBuilderPage.tsx
create mode 100644 frontend/apps/apollo/src/features/assignments/AssignmentsPage.tsx
create mode 100644 frontend/apps/apollo/src/features/assignments/mock-data.tsx
create mode 100644 frontend/apps/apollo/src/features/auth/api.ts
create mode 100644 frontend/apps/apollo/src/features/auth/components/AuthCard.tsx
create mode 100644 frontend/apps/apollo/src/features/auth/components/AuthErrorBanner.tsx
create mode 100644 frontend/apps/apollo/src/features/auth/routes/LoginPage.tsx
create mode 100644 frontend/apps/apollo/src/features/auth/routes/SignupPage.tsx
create mode 100644 frontend/apps/apollo/src/features/auth/session.ts
create mode 100644 frontend/apps/apollo/src/features/dashboard/DashboardPage.tsx
create mode 100644 frontend/apps/apollo/src/features/dashboard/mock-data.tsx
create mode 100644 frontend/apps/apollo/src/features/gradebook/GradebookPage.tsx
create mode 100644 frontend/apps/apollo/src/features/gradebook/mock-data.ts
create mode 100644 frontend/apps/apollo/src/features/settings/SettingsPage.tsx
create mode 100644 frontend/apps/apollo/src/features/shell/AppSidebar.tsx
create mode 100644 frontend/apps/apollo/src/features/shell/AppTopbar.tsx
create mode 100644 frontend/apps/apollo/src/features/shell/AuthLayout.tsx
create mode 100644 frontend/apps/apollo/src/features/shell/ProtectedAppLayout.tsx
create mode 100644 frontend/apps/apollo/src/features/students/StudentsPage.tsx
create mode 100644 frontend/apps/apollo/src/features/submissions/SubmissionsPage.tsx
create mode 100644 frontend/apps/apollo/src/lib/api/client.ts
create mode 100644 frontend/apps/apollo/src/routes/assignment-editor.tsx
create mode 100644 frontend/apps/apollo/src/routes/assignments.tsx
create mode 100644 frontend/apps/apollo/src/routes/auth.tsx
create mode 100644 frontend/apps/apollo/src/routes/dashboard.tsx
create mode 100644 frontend/apps/apollo/src/routes/gradebook.tsx
create mode 100644 frontend/apps/apollo/src/routes/protected.tsx
create mode 100644 frontend/apps/apollo/src/routes/root.tsx
create mode 100644 frontend/apps/apollo/src/routes/router.tsx
create mode 100644 frontend/apps/apollo/src/routes/settings.tsx
create mode 100644 frontend/apps/apollo/src/routes/students.tsx
create mode 100644 frontend/apps/apollo/src/routes/submissions.tsx
diff --git a/frontend/apps/apollo/src/features/assignments/AssignmentBuilderPage.tsx b/frontend/apps/apollo/src/features/assignments/AssignmentBuilderPage.tsx
new file mode 100644
index 0000000..27ef6d7
--- /dev/null
+++ b/frontend/apps/apollo/src/features/assignments/AssignmentBuilderPage.tsx
@@ -0,0 +1,122 @@
+import { FileCode2, FolderOpen, Sparkles, Upload } from "lucide-react";
+import {
+ BottomActionBar,
+ FormFieldScaffold,
+ PageHeader,
+ PageShell,
+ SectionHeading,
+ StatusPill,
+ SurfacePanel,
+} from "@optimark/calliope";
+
+import { starterFiles } from "./mock-data";
+
+export function AssignmentBuilderPage() {
+ return (
+
+
+
+
+
+
}
+ title="Description"
+ actions={
+
+
+ Write
+
+
+ Preview
+
+
+ }
+ />
+
+
+ {`## Instructions
+Implement a singly linked list with the following methods:
+- append(value)
+- prepend(value)
+- delete(value)
+- find(value)
+
+### Constraints
+- Time Complexity: O(n) for searching
+- Space Complexity: O(1) for deletions
+
+Ensure all edge cases are handled (empty list, single node list).`}
+
+
+
} title="Starter Files" />
+
+ {starterFiles.map((file) => (
+
+
+
{file.icon}
+
+
{file.name}
+
{file.meta}
+
+
+
+ ))}
+
+
+ Upload Additional Files
+
+
+
+
+
+
+
+
+ Oct 15, 2026
+
+
+ 100
+
+
+ Python 3.10
+
+
+
+
+
+
+
+
+
+ Visibility
+ Hidden
+
+
+ Category
+ Coding
+
+
+
+
+ Verify Environment
+
+
+
+
+
+ Current Status
+ Draft Saving...
+
+ }
+ actions={
+ <>
+ Save Draft
+ Publish
+ >
+ }
+ />
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/assignments/AssignmentsPage.tsx b/frontend/apps/apollo/src/features/assignments/AssignmentsPage.tsx
new file mode 100644
index 0000000..b2c0595
--- /dev/null
+++ b/frontend/apps/apollo/src/features/assignments/AssignmentsPage.tsx
@@ -0,0 +1,25 @@
+import { Link } from "@tanstack/react-router";
+import { FolderOpen } from "lucide-react";
+import { EmptyState, PageHeader, PageShell } from "@optimark/calliope";
+
+export function AssignmentsPage() {
+ return (
+
+
+ Open Editor
+
+ }
+ />
+ }
+ title="Reusable assignment patterns are ready"
+ description="Issue #8 can plug actual assignment data and actions into this shared shell without restyling the workspace."
+ />
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/assignments/mock-data.tsx b/frontend/apps/apollo/src/features/assignments/mock-data.tsx
new file mode 100644
index 0000000..539e41b
--- /dev/null
+++ b/frontend/apps/apollo/src/features/assignments/mock-data.tsx
@@ -0,0 +1,14 @@
+import { FileCode2, SquareTerminal } from "lucide-react";
+
+export const starterFiles = [
+ {
+ name: "linked_list.py",
+ meta: "Python Source • 2.4 KB",
+ icon: ,
+ },
+ {
+ name: "test_suite.py",
+ meta: "Python Test • 5.1 KB",
+ icon: ,
+ },
+] as const;
diff --git a/frontend/apps/apollo/src/features/auth/api.ts b/frontend/apps/apollo/src/features/auth/api.ts
new file mode 100644
index 0000000..fc225b6
--- /dev/null
+++ b/frontend/apps/apollo/src/features/auth/api.ts
@@ -0,0 +1,36 @@
+import { ApiError, requestJson } from "../../lib/api/client";
+import type { SessionResponse } from "./session";
+
+export type AuthPayload = {
+ email: string;
+ password: string;
+};
+
+export type SignupPayload = AuthPayload & {
+ display_name: string;
+};
+
+export async function loginRequest(payload: AuthPayload): Promise {
+ return requestJson("/api/v1/auth/login", {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+}
+
+export async function signupRequest(payload: SignupPayload): Promise {
+ return requestJson("/api/v1/auth/signup", {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+}
+
+export async function logoutRequest(): Promise {
+ const response = await fetch("/api/v1/auth/logout", {
+ method: "POST",
+ credentials: "include",
+ });
+
+ if (!response.ok) {
+ throw new ApiError(response.status, "Unable to end the current session.");
+ }
+}
diff --git a/frontend/apps/apollo/src/features/auth/components/AuthCard.tsx b/frontend/apps/apollo/src/features/auth/components/AuthCard.tsx
new file mode 100644
index 0000000..44ee6e1
--- /dev/null
+++ b/frontend/apps/apollo/src/features/auth/components/AuthCard.tsx
@@ -0,0 +1,29 @@
+import type { ReactNode } from "react";
+
+import { SurfacePanel } from "@optimark/calliope";
+
+export function AuthCard({
+ eyebrow,
+ title,
+ subtitle,
+ children,
+ footer,
+}: {
+ eyebrow: string;
+ title: string;
+ subtitle: string;
+ children: ReactNode;
+ footer?: ReactNode;
+}) {
+ return (
+
+
+
{eyebrow}
+
{title}
+
{subtitle}
+
+ {children}
+ {footer}
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/auth/components/AuthErrorBanner.tsx b/frontend/apps/apollo/src/features/auth/components/AuthErrorBanner.tsx
new file mode 100644
index 0000000..2bb6695
--- /dev/null
+++ b/frontend/apps/apollo/src/features/auth/components/AuthErrorBanner.tsx
@@ -0,0 +1,17 @@
+import { CircleAlert } from "lucide-react";
+
+import { ApiError } from "../../../lib/api/client";
+
+export function AuthErrorBanner({ error }: { error: unknown }) {
+ const detail =
+ error instanceof ApiError
+ ? error.message
+ : "Something went wrong. Please try again.";
+
+ return (
+
+
+ {detail}
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/auth/routes/LoginPage.tsx b/frontend/apps/apollo/src/features/auth/routes/LoginPage.tsx
new file mode 100644
index 0000000..db43c38
--- /dev/null
+++ b/frontend/apps/apollo/src/features/auth/routes/LoginPage.tsx
@@ -0,0 +1,86 @@
+import { useState } from "react";
+
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { Link, useNavigate, useRouterState } from "@tanstack/react-router";
+import { LoaderCircle } from "lucide-react";
+import { FormFieldScaffold } from "@optimark/calliope";
+
+import { loginRequest } from "../api";
+import {
+ sessionQueryKey,
+ type AuthSearch,
+} from "../session";
+import { AuthCard } from "../components/AuthCard";
+import { AuthErrorBanner } from "../components/AuthErrorBanner";
+
+export function LoginPage() {
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
+ const search = useRouterState({
+ select: (state) => (state.location.search as AuthSearch) ?? {},
+ });
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const login = useMutation({
+ mutationFn: loginRequest,
+ onSuccess: async (session) => {
+ queryClient.setQueryData(sessionQueryKey, session);
+ await navigate({ to: search.redirect || "/dashboard" });
+ },
+ });
+
+ async function onSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ login.mutate({ email, password });
+ }
+
+ return (
+
+ Need an account?{" "}
+
+ Create one
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/auth/routes/SignupPage.tsx b/frontend/apps/apollo/src/features/auth/routes/SignupPage.tsx
new file mode 100644
index 0000000..6c5282a
--- /dev/null
+++ b/frontend/apps/apollo/src/features/auth/routes/SignupPage.tsx
@@ -0,0 +1,98 @@
+import { useState } from "react";
+
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { Link, useNavigate, useRouterState } from "@tanstack/react-router";
+import { LoaderCircle } from "lucide-react";
+import { FormFieldScaffold } from "@optimark/calliope";
+
+import { signupRequest } from "../api";
+import {
+ sessionQueryKey,
+ type AuthSearch,
+} from "../session";
+import { AuthCard } from "../components/AuthCard";
+import { AuthErrorBanner } from "../components/AuthErrorBanner";
+
+export function SignupPage() {
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
+ const search = useRouterState({
+ select: (state) => (state.location.search as AuthSearch) ?? {},
+ });
+ const [displayName, setDisplayName] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const signup = useMutation({
+ mutationFn: signupRequest,
+ onSuccess: async (session) => {
+ queryClient.setQueryData(sessionQueryKey, session);
+ await navigate({ to: search.redirect || "/dashboard" });
+ },
+ });
+
+ async function onSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ signup.mutate({ display_name: displayName, email, password });
+ }
+
+ return (
+
+ Already have access?{" "}
+
+ Sign in
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/auth/session.ts b/frontend/apps/apollo/src/features/auth/session.ts
new file mode 100644
index 0000000..b8045b7
--- /dev/null
+++ b/frontend/apps/apollo/src/features/auth/session.ts
@@ -0,0 +1,122 @@
+import type { QueryClient } from "@tanstack/react-query";
+import { redirect } from "@tanstack/react-router";
+
+import { ApiError, requestJson } from "../../lib/api/client";
+
+export type AuthSearch = {
+ redirect?: string;
+};
+
+export type SessionUser = {
+ id: string;
+ email: string;
+ display_name: string;
+};
+
+export type SessionResponse = {
+ user: SessionUser;
+ provider: string;
+ expires_at: string;
+};
+
+export const sessionQueryKey = ["auth", "session"] as const;
+
+export async function fetchSession(): Promise {
+ try {
+ return await requestJson("/api/v1/auth/session", {
+ method: "GET",
+ });
+ } catch (error) {
+ if (error instanceof ApiError && error.status === 401) {
+ return null;
+ }
+
+ throw error;
+ }
+}
+
+export function sessionQueryOptions() {
+ return {
+ queryKey: sessionQueryKey,
+ queryFn: fetchSession,
+ staleTime: 60_000,
+ retry: false,
+ };
+}
+
+export function sanitizeRedirectPath(value: unknown): string | undefined {
+ if (typeof value !== "string") {
+ return undefined;
+ }
+
+ const trimmed = value.trim();
+
+ if (!trimmed) {
+ return undefined;
+ }
+
+ let decodedValue = trimmed;
+
+ try {
+ decodedValue = decodeURIComponent(trimmed);
+ } catch {
+ return undefined;
+ }
+
+ if (/[\u0000-\u001F\u007F]/.test(decodedValue)) {
+ return undefined;
+ }
+
+ if (!decodedValue.startsWith("/") || decodedValue.startsWith("//")) {
+ return undefined;
+ }
+
+ const firstNestedSlash = decodedValue.indexOf("/", 1);
+ const leadingSegment =
+ firstNestedSlash === -1 ? decodedValue.slice(1) : decodedValue.slice(1, firstNestedSlash);
+
+ if (leadingSegment.includes(":") || decodedValue.includes("://")) {
+ return undefined;
+ }
+
+ return decodedValue;
+}
+
+export function deriveInitials(displayName: string): string {
+ return displayName
+ .trim()
+ .split(/\s+/)
+ .filter(Boolean)
+ .map((part) => part[0] ?? "")
+ .join("")
+ .slice(0, 2)
+ .toUpperCase();
+}
+
+export async function ensureSession(queryClient: QueryClient): Promise {
+ return queryClient.ensureQueryData(sessionQueryOptions());
+}
+
+export async function requireAuthenticated(queryClient: QueryClient, redirectPath: string) {
+ const session = await ensureSession(queryClient);
+
+ if (!session) {
+ throw redirect({
+ to: "/login",
+ search: { redirect: redirectPath },
+ });
+ }
+
+ return session;
+}
+
+export async function redirectAuthenticated(
+ queryClient: QueryClient,
+ redirectPath?: string,
+) {
+ const session = await ensureSession(queryClient);
+
+ if (session) {
+ throw redirect({ to: redirectPath || "/dashboard" });
+ }
+}
diff --git a/frontend/apps/apollo/src/features/dashboard/DashboardPage.tsx b/frontend/apps/apollo/src/features/dashboard/DashboardPage.tsx
new file mode 100644
index 0000000..561f7c9
--- /dev/null
+++ b/frontend/apps/apollo/src/features/dashboard/DashboardPage.tsx
@@ -0,0 +1,116 @@
+import { BookOpen, ChevronDown, LayoutDashboard, Search, Sparkles, Upload } from "lucide-react";
+import {
+ BottomActionBar,
+ MetricCard,
+ PageHeader,
+ PageShell,
+ SectionHeading,
+ StatusPill,
+ SurfacePanel,
+ brand,
+} from "@optimark/calliope";
+
+import { activityFeed, assignmentRows } from "./mock-data";
+
+export function DashboardPage() {
+ return (
+
+
+
+
+ Open Gradebook
+
+
+
+ Publish All
+
+ >
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+ }
+ title="Active Coursework"
+ actions={
+
+ Filter
+
+
+ }
+ />
+
+
+
+
+ Name
+ Type
+ Status
+ Due Date
+ Submissions
+
+
+
+ {assignmentRows.map((row) => (
+
+ {row.name}
+ {row.type}
+
+ {row.status.label}
+
+ {row.due}
+ {row.submissions}
+
+ ))}
+
+
+
+
+
+ } title="Operational Feed" />
+
+ {activityFeed.map((item) => (
+
+ {item.icon}
+
+
{item.title}
+
{item.when}
+
{item.detail}
+
+
+ ))}
+
+
+
+
+
+ Quick Actions
+
+
+
+ >
+ }
+ actions={
+ <>
+ Regrade
+ Download CSV
+ >
+ }
+ />
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/dashboard/mock-data.tsx b/frontend/apps/apollo/src/features/dashboard/mock-data.tsx
new file mode 100644
index 0000000..f1ff942
--- /dev/null
+++ b/frontend/apps/apollo/src/features/dashboard/mock-data.tsx
@@ -0,0 +1,63 @@
+import { CircleAlert, FileCode2, SquareTerminal } from "lucide-react";
+
+export const assignmentRows = [
+ {
+ name: "Homework 4: Linked Lists",
+ type: "Practical",
+ status: { label: "Published", tone: "primary" as const },
+ due: "Oct 12, 23:59",
+ submissions: "42 / 45",
+ },
+ {
+ name: "Assignment 3: Binary Trees",
+ type: "Programming",
+ status: { label: "Reviewing", tone: "secondary" as const },
+ due: "Oct 05, 12:00",
+ submissions: "45 / 45",
+ },
+ {
+ name: "Midterm Quiz: Core Concepts",
+ type: "Exam",
+ status: { label: "Draft", tone: "default" as const },
+ due: "Oct 24, 09:00",
+ submissions: "0 / 45",
+ },
+ {
+ name: "Homework 5: Graph Theory",
+ type: "Practical",
+ status: { label: "Draft", tone: "default" as const },
+ due: "Nov 02, 23:59",
+ submissions: "0 / 45",
+ },
+] as const;
+
+export const activityFeed = [
+ {
+ title: "Student A submitted Homework 4",
+ when: "12 minutes ago",
+ detail: "New programming artifact is ready for queueing.",
+ tone: "primary" as const,
+ icon: ,
+ },
+ {
+ title: "Autograde completed for Assignment 3",
+ when: "45 minutes ago",
+ detail: "82% average • 4 error flags surfaced for review.",
+ tone: "secondary" as const,
+ icon: ,
+ },
+ {
+ title: "Manual review started on Midterm Essays",
+ when: "2 hours ago",
+ detail: "TA review batch is currently in progress.",
+ tone: "default" as const,
+ icon: ,
+ },
+ {
+ title: "System: Plagiarism detected in Homework 4",
+ when: "3 hours ago",
+ detail: "One critical alert needs instructor verification.",
+ tone: "danger" as const,
+ icon: ,
+ },
+] as const;
diff --git a/frontend/apps/apollo/src/features/gradebook/GradebookPage.tsx b/frontend/apps/apollo/src/features/gradebook/GradebookPage.tsx
new file mode 100644
index 0000000..912765d
--- /dev/null
+++ b/frontend/apps/apollo/src/features/gradebook/GradebookPage.tsx
@@ -0,0 +1,139 @@
+import { ChevronDown, Download, Filter, Settings } from "lucide-react";
+import {
+ BottomActionBar,
+ FormFieldScaffold,
+ PageShell,
+ SectionHeading,
+ StatusPill,
+ SurfacePanel,
+ brand,
+} from "@optimark/calliope";
+
+import { gradebookRows, gradeDistribution } from "./mock-data";
+
+export function GradebookPage() {
+ return (
+
+
+
+
+ Academic Overview
+ {brand.courseLabel} • {brand.courseTerm}
+
+
+ Average Grade
+ 84.2
+
+
+ Completion Rate
+ 92%
+
+
+ Next Deadline
+ Oct 12
+
+
+
+
+
+
+
+ All Students
+
+
+
+
+
+ All Statuses
+
+
+
+
+
+
+ Export CSV
+
+
+
+ View Settings
+
+
+
+
+
+
+
+
+ Student Name
+ HW 1
+ HW 2
+ HW 3
+ HW 4
+ Midterm
+ Overall
+
+
+
+ {gradebookRows.map((row) => (
+
+
+
+
+ {row.initials}
+
+
+
{row.student}
+
{row.email}
+
+
+
+ {row.scores.map((score, index) => (
+
+ {score.value}
+ {score.status}
+
+ ))}
+
+ {row.overall}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {gradeDistribution.map((bar) => (
+
+ ))}
+
+
+
+
+
+ 3 students selected
+ Batch actions ready
+
+ }
+ actions={
+ <>
+ Message
+ Batch Edit
+ Release Selected
+ >
+ }
+ />
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/gradebook/mock-data.ts b/frontend/apps/apollo/src/features/gradebook/mock-data.ts
new file mode 100644
index 0000000..f19cb7a
--- /dev/null
+++ b/frontend/apps/apollo/src/features/gradebook/mock-data.ts
@@ -0,0 +1,70 @@
+export const gradebookRows = [
+ {
+ initials: "AA",
+ student: "Alex Abramov",
+ email: "a.abramov@university.edu",
+ accent: "secondary" as const,
+ scores: [
+ { value: "95/100", status: "Released", tone: "primary" as const },
+ { value: "88/100", status: "Released", tone: "primary" as const },
+ { value: "--/100", status: "Pending", tone: "secondary" as const },
+ { value: "--/100", status: "Missing", tone: "default" as const },
+ { value: "142/150", status: "Released", tone: "primary" as const },
+ ],
+ overall: "91.4%",
+ overallTone: "primary" as const,
+ },
+ {
+ initials: "EL",
+ student: "Elena Laveau",
+ email: "e.laveau@university.edu",
+ accent: "secondary" as const,
+ scores: [
+ { value: "100/100", status: "Released", tone: "primary" as const },
+ { value: "94/100", status: "Released", tone: "primary" as const },
+ { value: "98/100", status: "Released", tone: "primary" as const },
+ { value: "92/100", status: "Released", tone: "primary" as const },
+ { value: "148/150", status: "Released", tone: "primary" as const },
+ ],
+ overall: "97.8%",
+ overallTone: "primary" as const,
+ },
+ {
+ initials: "JM",
+ student: "Jordan Miller",
+ email: "j.miller@university.edu",
+ accent: "danger" as const,
+ scores: [
+ { value: "64/100", status: "Released", tone: "primary" as const },
+ { value: "72/100", status: "Released", tone: "primary" as const },
+ { value: "--/100", status: "Pending", tone: "secondary" as const },
+ { value: "--/100", status: "Pending", tone: "secondary" as const },
+ { value: "110/150", status: "Released", tone: "primary" as const },
+ ],
+ overall: "68.2%",
+ overallTone: "danger" as const,
+ },
+ {
+ initials: "SW",
+ student: "Sarah Wu",
+ email: "s.wu@university.edu",
+ accent: "secondary" as const,
+ scores: [
+ { value: "92/100", status: "Released", tone: "primary" as const },
+ { value: "89/100", status: "Released", tone: "primary" as const },
+ { value: "91/100", status: "Released", tone: "primary" as const },
+ { value: "88/100", status: "Released", tone: "primary" as const },
+ { value: "138/150", status: "Released", tone: "primary" as const },
+ ],
+ overall: "90.5%",
+ overallTone: "primary" as const,
+ },
+] as const;
+
+export const gradeDistribution = [
+ { label: "F", height: "18%", accent: false },
+ { label: "D", height: "42%", accent: false },
+ { label: "C", height: "78%", accent: true },
+ { label: "B", height: "60%", accent: false },
+ { label: "A", height: "14%", accent: false },
+] as const;
diff --git a/frontend/apps/apollo/src/features/settings/SettingsPage.tsx b/frontend/apps/apollo/src/features/settings/SettingsPage.tsx
new file mode 100644
index 0000000..284b275
--- /dev/null
+++ b/frontend/apps/apollo/src/features/settings/SettingsPage.tsx
@@ -0,0 +1,39 @@
+import { Gauge, Sparkles } from "lucide-react";
+import {
+ FormFieldScaffold,
+ PageHeader,
+ PageShell,
+ SectionHeading,
+ SurfacePanel,
+} from "@optimark/calliope";
+
+export function SettingsPage() {
+ return (
+
+
+
+
+ } />
+
+ Manual review gate
+
+
+ Hidden until publish
+
+
+
+ } />
+
+ 2 retries
+
+
+ Full term archive
+
+
+
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/shell/AppSidebar.tsx b/frontend/apps/apollo/src/features/shell/AppSidebar.tsx
new file mode 100644
index 0000000..9b3111b
--- /dev/null
+++ b/frontend/apps/apollo/src/features/shell/AppSidebar.tsx
@@ -0,0 +1,106 @@
+import { useMemo } from "react";
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Link, useNavigate } from "@tanstack/react-router";
+import { Code2, LayoutDashboard, BookOpen, ListChecks, ChartColumn, Users, Settings, LoaderCircle, LogOut } from "lucide-react";
+import {
+ BrandLockup,
+ SidebarNavItem,
+ SidebarShell,
+ brand,
+ sidebarUtilityLinks,
+} from "@optimark/calliope";
+
+import { logoutRequest } from "../auth/api";
+import { deriveInitials, sessionQueryKey, sessionQueryOptions } from "../auth/session";
+
+const navItems = [
+ { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
+ { to: "/assignments", label: "Assignments", icon: BookOpen },
+ { to: "/submissions", label: "Submissions", icon: ListChecks },
+ { to: "/gradebook", label: "Gradebook", icon: ChartColumn },
+ { to: "/students", label: "Students", icon: Users },
+ { to: "/settings", label: "Settings", icon: Settings },
+] as const;
+
+function SidebarProfile({
+ displayName,
+ email,
+}: {
+ displayName: string;
+ email: string;
+}) {
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
+ const logout = useMutation({
+ mutationFn: logoutRequest,
+ onSuccess: async () => {
+ queryClient.setQueryData(sessionQueryKey, null);
+ await navigate({ to: "/login" });
+ },
+ });
+ const initials = useMemo(() => deriveInitials(displayName), [displayName]);
+
+ return (
+
+
+
{initials}
+
+
{displayName}
+
{email}
+
+
+
logout.mutate()}
+ disabled={logout.isPending}
+ >
+ {logout.isPending ? : }
+ Log Out
+
+ {logout.isError ? (
+
We could not end the current session. Try again.
+ ) : null}
+
+ );
+}
+
+export function AppSidebar() {
+ const { data: session } = useQuery(sessionQueryOptions());
+
+ return (
+ }
+ />
+ }
+ navigation={navItems.map(({ to, label, icon: Icon }) => (
+
+ {({ isActive }) => (
+ } label={label} />
+ )}
+
+ ))}
+ primaryAction={
+
+ New Assessment
+
+ }
+ utilityLinks={sidebarUtilityLinks.map((item) => (
+
+ {item.label}
+
+ ))}
+ profile={
+
+ }
+ />
+ );
+}
diff --git a/frontend/apps/apollo/src/features/shell/AppTopbar.tsx b/frontend/apps/apollo/src/features/shell/AppTopbar.tsx
new file mode 100644
index 0000000..1102436
--- /dev/null
+++ b/frontend/apps/apollo/src/features/shell/AppTopbar.tsx
@@ -0,0 +1,50 @@
+import { useMemo } from "react";
+
+import { useQuery } from "@tanstack/react-query";
+import { useRouterState } from "@tanstack/react-router";
+import { Bell, History, Search } from "lucide-react";
+import { Topbar, TopbarTab, brand, topTabs } from "@optimark/calliope";
+
+import { deriveInitials, sessionQueryOptions } from "../auth/session";
+
+export function AppTopbar() {
+ const pathname = useRouterState({
+ select: (state) => state.location.pathname,
+ });
+ const { data: session } = useQuery(sessionQueryOptions());
+ const activeTopTab = pathname === "/gradebook" ? "analytics" : "course-settings";
+ const initials = useMemo(
+ () => deriveInitials(session?.user.display_name ?? brand.instructorName),
+ [session?.user.display_name],
+ );
+
+ return (
+ (
+
+ {tab.label}
+
+ ))}
+ search={
+
+
+
+
+ }
+ tools={
+
+
+
+
+
+
+
+
+
{session?.user.display_name ?? brand.instructorName}
+
{initials}
+
+
+ }
+ />
+ );
+}
diff --git a/frontend/apps/apollo/src/features/shell/AuthLayout.tsx b/frontend/apps/apollo/src/features/shell/AuthLayout.tsx
new file mode 100644
index 0000000..84a411f
--- /dev/null
+++ b/frontend/apps/apollo/src/features/shell/AuthLayout.tsx
@@ -0,0 +1,41 @@
+import { Outlet } from "@tanstack/react-router";
+import { Code2 } from "lucide-react";
+import { BrandLockup, SurfacePanel, brand } from "@optimark/calliope";
+
+export function AuthLayout() {
+ return (
+
+
+
+
+ }
+ />
+
+
Hosted Access
+
The academic workspace with operational calm.
+
+ Sign in to restore your course session, review assessment activity,
+ and continue in the same curated shell used across the app.
+
+
+
+
+ Session restoration
+ Cookie-backed app access restores your workspace on reload.
+
+
+ Protected surfaces
+ Instructor and student routes stay gated until a valid session exists.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/shell/ProtectedAppLayout.tsx b/frontend/apps/apollo/src/features/shell/ProtectedAppLayout.tsx
new file mode 100644
index 0000000..3360e3b
--- /dev/null
+++ b/frontend/apps/apollo/src/features/shell/ProtectedAppLayout.tsx
@@ -0,0 +1,13 @@
+import { Outlet } from "@tanstack/react-router";
+import { AppFrame } from "@optimark/calliope";
+
+import { AppSidebar } from "./AppSidebar";
+import { AppTopbar } from "./AppTopbar";
+
+export function ProtectedAppLayout() {
+ return (
+ } topbar={ }>
+
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/students/StudentsPage.tsx b/frontend/apps/apollo/src/features/students/StudentsPage.tsx
new file mode 100644
index 0000000..196a080
--- /dev/null
+++ b/frontend/apps/apollo/src/features/students/StudentsPage.tsx
@@ -0,0 +1,18 @@
+import { GraduationCap } from "lucide-react";
+import { EmptyState, PageHeader, PageShell } from "@optimark/calliope";
+
+export function StudentsPage() {
+ return (
+
+
+ }
+ title="Roster primitives are now part of the system"
+ description="Issue-specific student views can reuse the gradebook table rhythm, metadata labels, and sidebar shell without forking styles."
+ />
+
+ );
+}
diff --git a/frontend/apps/apollo/src/features/submissions/SubmissionsPage.tsx b/frontend/apps/apollo/src/features/submissions/SubmissionsPage.tsx
new file mode 100644
index 0000000..307bb53
--- /dev/null
+++ b/frontend/apps/apollo/src/features/submissions/SubmissionsPage.tsx
@@ -0,0 +1,18 @@
+import { ListChecks } from "lucide-react";
+import { EmptyState, PageHeader, PageShell } from "@optimark/calliope";
+
+export function SubmissionsPage() {
+ return (
+
+
+ }
+ title="Submission workflows will plug in here"
+ description="The design system now supports high-density queue views and calm operational empty states."
+ />
+
+ );
+}
diff --git a/frontend/apps/apollo/src/lib/api/client.ts b/frontend/apps/apollo/src/lib/api/client.ts
new file mode 100644
index 0000000..b92d942
--- /dev/null
+++ b/frontend/apps/apollo/src/lib/api/client.ts
@@ -0,0 +1,35 @@
+export class ApiError extends Error {
+ status: number;
+
+ constructor(status: number, message: string) {
+ super(message);
+ this.name = "ApiError";
+ this.status = status;
+ }
+}
+
+export async function requestJson(path: string, init?: RequestInit): Promise {
+ const response = await fetch(path, {
+ ...init,
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ ...(init?.headers ?? {}),
+ },
+ });
+
+ if (!response.ok) {
+ let detail = "Request failed.";
+
+ try {
+ const body = (await response.json()) as { detail?: string };
+ detail = body.detail ?? detail;
+ } catch {
+ detail = response.statusText || detail;
+ }
+
+ throw new ApiError(response.status, detail);
+ }
+
+ return (await response.json()) as T;
+}
diff --git a/frontend/apps/apollo/src/main.tsx b/frontend/apps/apollo/src/main.tsx
index bf774f6..2f8fa87 100644
--- a/frontend/apps/apollo/src/main.tsx
+++ b/frontend/apps/apollo/src/main.tsx
@@ -1,1296 +1,11 @@
-import React, { useMemo, useState } from "react";
+import React from "react";
import ReactDOM from "react-dom/client";
-import {
- QueryClient,
- QueryClientProvider,
- useMutation,
- useQuery,
-} from "@tanstack/react-query";
-import {
- Link,
- Outlet,
- RouterProvider,
- createRootRouteWithContext,
- createRoute,
- createRouter,
- redirect,
- useNavigate,
- useRouterState,
-} from "@tanstack/react-router";
-import {
- Bell,
- BookOpen,
- ChartColumn,
- ChevronDown,
- CircleAlert,
- Code2,
- Download,
- FileCode2,
- Filter,
- FolderOpen,
- Gauge,
- GraduationCap,
- History,
- LayoutDashboard,
- ListChecks,
- LoaderCircle,
- LogOut,
- Search,
- Settings,
- Sparkles,
- SquareTerminal,
- Upload,
- Users,
-} from "lucide-react";
-import {
- AppFrame,
- BottomActionBar,
- BrandLockup,
- EmptyState,
- FormFieldScaffold,
- MetricCard,
- PageHeader,
- PageShell,
- SectionHeading,
- SidebarNavItem,
- SidebarShell,
- StatusPill,
- SurfacePanel,
- Topbar,
- TopbarTab,
- brand,
- sidebarUtilityLinks,
- topTabs,
-} from "@optimark/calliope";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { RouterProvider } from "@tanstack/react-router";
+
import "@optimark/calliope/system.css";
import "./styles.css";
-
-type AppContext = {
- queryClient: QueryClient;
-};
-
-type AuthSearch = {
- redirect?: string;
-};
-
-type SessionUser = {
- id: string;
- email: string;
- display_name: string;
-};
-
-type SessionResponse = {
- user: SessionUser;
- provider: string;
- expires_at: string;
-};
-
-type AuthPayload = {
- email: string;
- password: string;
-};
-
-type SignupPayload = AuthPayload & {
- display_name: string;
-};
-
-class ApiError extends Error {
- status: number;
-
- constructor(status: number, message: string) {
- super(message);
- this.name = "ApiError";
- this.status = status;
- }
-}
-
-const queryClient = new QueryClient();
-const sessionQueryKey = ["auth", "session"] as const;
-
-const navItems = [
- { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
- { to: "/assignments", label: "Assignments", icon: BookOpen },
- { to: "/submissions", label: "Submissions", icon: ListChecks },
- { to: "/gradebook", label: "Gradebook", icon: ChartColumn },
- { to: "/students", label: "Students", icon: Users },
- { to: "/settings", label: "Settings", icon: Settings },
-] as const;
-
-const assignmentRows = [
- {
- name: "Homework 4: Linked Lists",
- type: "Practical",
- status: { label: "Published", tone: "primary" as const },
- due: "Oct 12, 23:59",
- submissions: "42 / 45",
- },
- {
- name: "Assignment 3: Binary Trees",
- type: "Programming",
- status: { label: "Reviewing", tone: "secondary" as const },
- due: "Oct 05, 12:00",
- submissions: "45 / 45",
- },
- {
- name: "Midterm Quiz: Core Concepts",
- type: "Exam",
- status: { label: "Draft", tone: "default" as const },
- due: "Oct 24, 09:00",
- submissions: "0 / 45",
- },
- {
- name: "Homework 5: Graph Theory",
- type: "Practical",
- status: { label: "Draft", tone: "default" as const },
- due: "Nov 02, 23:59",
- submissions: "0 / 45",
- },
-] as const;
-
-const activityFeed = [
- {
- title: "Student A submitted Homework 4",
- when: "12 minutes ago",
- detail: "New programming artifact is ready for queueing.",
- tone: "primary" as const,
- },
- {
- title: "Autograde completed for Assignment 3",
- when: "45 minutes ago",
- detail: "82% average • 4 error flags surfaced for review.",
- tone: "secondary" as const,
- },
- {
- title: "Manual review started on Midterm Essays",
- when: "2 hours ago",
- detail: "TA review batch is currently in progress.",
- tone: "default" as const,
- },
- {
- title: "System: Plagiarism detected in Homework 4",
- when: "3 hours ago",
- detail: "One critical alert needs instructor verification.",
- tone: "danger" as const,
- },
-] as const;
-
-const starterFiles = [
- {
- name: "linked_list.py",
- meta: "Python Source • 2.4 KB",
- icon: ,
- },
- {
- name: "test_suite.py",
- meta: "Python Test • 5.1 KB",
- icon: ,
- },
-] as const;
-
-const gradebookRows = [
- {
- initials: "AA",
- student: "Alex Abramov",
- email: "a.abramov@university.edu",
- accent: "secondary" as const,
- scores: [
- { value: "95/100", status: "Released", tone: "primary" as const },
- { value: "88/100", status: "Released", tone: "primary" as const },
- { value: "--/100", status: "Pending", tone: "secondary" as const },
- { value: "--/100", status: "Missing", tone: "default" as const },
- { value: "142/150", status: "Released", tone: "primary" as const },
- ],
- overall: "91.4%",
- overallTone: "primary" as const,
- },
- {
- initials: "EL",
- student: "Elena Laveau",
- email: "e.laveau@university.edu",
- accent: "secondary" as const,
- scores: [
- { value: "100/100", status: "Released", tone: "primary" as const },
- { value: "94/100", status: "Released", tone: "primary" as const },
- { value: "98/100", status: "Released", tone: "primary" as const },
- { value: "92/100", status: "Released", tone: "primary" as const },
- { value: "148/150", status: "Released", tone: "primary" as const },
- ],
- overall: "97.8%",
- overallTone: "primary" as const,
- },
- {
- initials: "JM",
- student: "Jordan Miller",
- email: "j.miller@university.edu",
- accent: "danger" as const,
- scores: [
- { value: "64/100", status: "Released", tone: "primary" as const },
- { value: "72/100", status: "Released", tone: "primary" as const },
- { value: "--/100", status: "Pending", tone: "secondary" as const },
- { value: "--/100", status: "Pending", tone: "secondary" as const },
- { value: "110/150", status: "Released", tone: "primary" as const },
- ],
- overall: "68.2%",
- overallTone: "danger" as const,
- },
- {
- initials: "SW",
- student: "Sarah Wu",
- email: "s.wu@university.edu",
- accent: "secondary" as const,
- scores: [
- { value: "92/100", status: "Released", tone: "primary" as const },
- { value: "89/100", status: "Released", tone: "primary" as const },
- { value: "91/100", status: "Released", tone: "primary" as const },
- { value: "88/100", status: "Released", tone: "primary" as const },
- { value: "138/150", status: "Released", tone: "primary" as const },
- ],
- overall: "90.5%",
- overallTone: "primary" as const,
- },
-] as const;
-
-const gradeDistribution = [
- { label: "F", height: "18%", accent: false },
- { label: "D", height: "42%", accent: false },
- { label: "C", height: "78%", accent: true },
- { label: "B", height: "60%", accent: false },
- { label: "A", height: "14%", accent: false },
-] as const;
-
-async function requestJson(path: string, init?: RequestInit): Promise {
- const response = await fetch(path, {
- ...init,
- credentials: "include",
- headers: {
- "Content-Type": "application/json",
- ...(init?.headers ?? {}),
- },
- });
-
- if (!response.ok) {
- let detail = "Request failed.";
-
- try {
- const body = (await response.json()) as { detail?: string };
- detail = body.detail ?? detail;
- } catch {
- detail = response.statusText || detail;
- }
-
- throw new ApiError(response.status, detail);
- }
-
- return (await response.json()) as T;
-}
-
-async function fetchSession(): Promise {
- try {
- return await requestJson("/api/v1/auth/session", {
- method: "GET",
- });
- } catch (error) {
- if (error instanceof ApiError && error.status === 401) {
- return null;
- }
-
- throw error;
- }
-}
-
-async function loginRequest(payload: AuthPayload): Promise {
- return requestJson("/api/v1/auth/login", {
- method: "POST",
- body: JSON.stringify(payload),
- });
-}
-
-async function signupRequest(payload: SignupPayload): Promise {
- return requestJson("/api/v1/auth/signup", {
- method: "POST",
- body: JSON.stringify(payload),
- });
-}
-
-async function logoutRequest(): Promise {
- const response = await fetch("/api/v1/auth/logout", {
- method: "POST",
- credentials: "include",
- });
-
- if (!response.ok) {
- throw new ApiError(response.status, "Unable to end the current session.");
- }
-}
-
-function sessionQueryOptions() {
- return {
- queryKey: sessionQueryKey,
- queryFn: fetchSession,
- staleTime: 60_000,
- retry: false,
- };
-}
-
-async function ensureSession(queryClient: QueryClient): Promise {
- return queryClient.ensureQueryData(sessionQueryOptions());
-}
-
-async function requireAuthenticated(queryClient: QueryClient, redirectPath: string) {
- const session = await ensureSession(queryClient);
-
- if (!session) {
- throw redirect({
- to: "/login",
- search: { redirect: redirectPath },
- });
- }
-
- return session;
-}
-
-async function redirectAuthenticated(queryClient: QueryClient) {
- const session = await ensureSession(queryClient);
-
- if (session) {
- throw redirect({ to: "/dashboard" });
- }
-}
-
-const rootRoute = createRootRouteWithContext()({
- component: RootLayout,
-});
-
-const indexRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/",
- beforeLoad: async ({ context }) => {
- const session = await ensureSession(context.queryClient);
- throw redirect({ to: session ? "/dashboard" : "/login" });
- },
-});
-
-const loginRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/login",
- validateSearch: (search: Record): AuthSearch => ({
- redirect: typeof search.redirect === "string" ? search.redirect : undefined,
- }),
- beforeLoad: async ({ context }) => {
- await redirectAuthenticated(context.queryClient);
- },
- component: LoginPage,
-});
-
-const signupRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/signup",
- validateSearch: (search: Record): AuthSearch => ({
- redirect: typeof search.redirect === "string" ? search.redirect : undefined,
- }),
- beforeLoad: async ({ context }) => {
- await redirectAuthenticated(context.queryClient);
- },
- component: SignupPage,
-});
-
-const dashboardRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/dashboard",
- beforeLoad: async ({ context, location }) => {
- await requireAuthenticated(context.queryClient, location.pathname);
- },
- component: DashboardPage,
-});
-
-const assignmentsRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/assignments",
- beforeLoad: async ({ context, location }) => {
- await requireAuthenticated(context.queryClient, location.pathname);
- },
- component: AssignmentsPage,
-});
-
-const assignmentEditorRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/assignments/new",
- beforeLoad: async ({ context, location }) => {
- await requireAuthenticated(context.queryClient, location.pathname);
- },
- component: AssignmentBuilderPage,
-});
-
-const submissionsRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/submissions",
- beforeLoad: async ({ context, location }) => {
- await requireAuthenticated(context.queryClient, location.pathname);
- },
- component: SubmissionsPage,
-});
-
-const gradebookRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/gradebook",
- beforeLoad: async ({ context, location }) => {
- await requireAuthenticated(context.queryClient, location.pathname);
- },
- component: GradebookPage,
-});
-
-const studentsRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/students",
- beforeLoad: async ({ context, location }) => {
- await requireAuthenticated(context.queryClient, location.pathname);
- },
- component: StudentsPage,
-});
-
-const settingsRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/settings",
- beforeLoad: async ({ context, location }) => {
- await requireAuthenticated(context.queryClient, location.pathname);
- },
- component: SettingsPage,
-});
-
-const routeTree = rootRoute.addChildren([
- indexRoute,
- loginRoute,
- signupRoute,
- dashboardRoute,
- assignmentsRoute,
- assignmentEditorRoute,
- submissionsRoute,
- gradebookRoute,
- studentsRoute,
- settingsRoute,
-]);
-
-const router = createRouter({
- routeTree,
- context: {
- queryClient,
- },
- defaultPreload: "intent",
-});
-
-declare module "@tanstack/react-router" {
- interface Register {
- router: typeof router;
- }
-}
-
-function RootLayout() {
- const pathname = useRouterState({
- select: (state) => state.location.pathname,
- });
- const isAuthRoute = pathname === "/login" || pathname === "/signup";
-
- if (isAuthRoute) {
- return ;
- }
-
- return (
- } topbar={ }>
-
-
- );
-}
-
-function AuthViewport() {
- return (
-
-
-
-
- }
- />
-
-
Hosted Access
-
The academic workspace with operational calm.
-
- Sign in to restore your course session, review assessment activity,
- and continue in the same curated shell used across the app.
-
-
-
-
- Session restoration
- Cookie-backed app access restores your workspace on reload.
-
-
- Protected surfaces
- Instructor and student routes stay gated until a valid session exists.
-
-
-
-
-
-
-
-
- );
-}
-
-function AppSidebar() {
- const { data: session } = useQuery(sessionQueryOptions());
-
- return (
- }
- />
- }
- navigation={navItems.map(({ to, label, icon: Icon }) => (
-
- {({ isActive }) => (
- }
- label={label}
- />
- )}
-
- ))}
- primaryAction={
-
- New Assessment
-
- }
- utilityLinks={sidebarUtilityLinks.map((item) => (
-
- {item.label}
-
- ))}
- profile={
-
- }
- />
- );
-}
-
-function SidebarProfile({
- displayName,
- email,
-}: {
- displayName: string;
- email: string;
-}) {
- const navigate = useNavigate();
- const logout = useMutation({
- mutationFn: logoutRequest,
- onSuccess: async () => {
- queryClient.setQueryData(sessionQueryKey, null);
- await queryClient.invalidateQueries({ queryKey: sessionQueryKey });
- await navigate({ to: "/login" });
- },
- });
- const initials = useMemo(
- () =>
- displayName
- .split(" ")
- .map((part) => part[0])
- .join("")
- .slice(0, 2)
- .toUpperCase(),
- [displayName],
- );
-
- return (
-
-
-
{initials}
-
-
{displayName}
-
{email}
-
-
-
logout.mutate()}
- disabled={logout.isPending}
- >
- {logout.isPending ? : }
- Log Out
-
- {logout.isError ? (
-
We could not end the current session. Try again.
- ) : null}
-
- );
-}
-
-function AppTopbar() {
- const pathname = useRouterState({
- select: (state) => state.location.pathname,
- });
- const { data: session } = useQuery(sessionQueryOptions());
- const activeTopTab = pathname === "/gradebook" ? "analytics" : "course-settings";
- const initials = useMemo(
- () =>
- (session?.user.display_name ?? brand.instructorName)
- .split(" ")
- .map((part) => part[0])
- .join("")
- .slice(0, 2)
- .toUpperCase(),
- [session?.user.display_name],
- );
-
- return (
- (
-
- {tab.label}
-
- ))}
- search={
-
-
-
-
- }
- tools={
-
-
-
-
-
-
-
-
-
{session?.user.display_name ?? brand.instructorName}
-
{initials}
-
-
- }
- />
- );
-}
-
-function LoginPage() {
- const navigate = useNavigate({ from: "/login" });
- const search = loginRoute.useSearch();
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const login = useMutation({
- mutationFn: loginRequest,
- onSuccess: async (session) => {
- queryClient.setQueryData(sessionQueryKey, session);
- await navigate({ to: search.redirect || "/dashboard" });
- },
- });
-
- async function onSubmit(event: React.FormEvent) {
- event.preventDefault();
- login.mutate({ email, password });
- }
-
- return (
-
- Need an account?{" "}
-
- Create one
-
-
- }
- >
-
-
- );
-}
-
-function SignupPage() {
- const navigate = useNavigate({ from: "/signup" });
- const search = signupRoute.useSearch();
- const [displayName, setDisplayName] = useState("");
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const signup = useMutation({
- mutationFn: signupRequest,
- onSuccess: async (session) => {
- queryClient.setQueryData(sessionQueryKey, session);
- await navigate({ to: search.redirect || "/dashboard" });
- },
- });
-
- async function onSubmit(event: React.FormEvent) {
- event.preventDefault();
- signup.mutate({ display_name: displayName, email, password });
- }
-
- return (
-
- Already have access?{" "}
-
- Sign in
-
-
- }
- >
-
-
- );
-}
-
-function AuthCard({
- eyebrow,
- title,
- subtitle,
- children,
- footer,
-}: {
- eyebrow: string;
- title: string;
- subtitle: string;
- children: React.ReactNode;
- footer?: React.ReactNode;
-}) {
- return (
-
-
-
{eyebrow}
-
{title}
-
{subtitle}
-
- {children}
- {footer}
-
- );
-}
-
-function AuthErrorBanner({ error }: { error: unknown }) {
- const detail = error instanceof ApiError ? error.message : "Something went wrong. Please try again.";
-
- return (
-
-
- {detail}
-
- );
-}
-
-function DashboardPage() {
- return (
-
-
-
-
- Open Gradebook
-
-
-
- Publish All
-
- >
- }
- />
-
-
-
-
-
-
-
-
-
-
- }
- title="Active Coursework"
- actions={
-
- Filter
-
-
- }
- />
-
-
-
-
- Name
- Type
- Status
- Due Date
- Submissions
-
-
-
- {assignmentRows.map((row) => (
-
- {row.name}
- {row.type}
-
- {row.status.label}
-
- {row.due}
- {row.submissions}
-
- ))}
-
-
-
-
-
- } title="Operational Feed" />
-
- {activityFeed.map((item) => (
-
-
- {item.tone === "danger" ? : }
-
-
-
{item.title}
-
{item.when}
-
{item.detail}
-
-
- ))}
-
-
-
-
-
- Quick Actions
-
-
-
- >
- }
- actions={
- <>
- Regrade
- Download CSV
- >
- }
- />
-
- );
-}
-
-function AssignmentsPage() {
- return (
-
-
- Open Editor
-
- }
- />
- }
- title="Reusable assignment patterns are ready"
- description="Issue #8 can plug actual assignment data and actions into this shared shell without restyling the workspace."
- />
-
- );
-}
-
-function AssignmentBuilderPage() {
- return (
-
-
-
-
-
-
}
- title="Description"
- actions={
-
-
- Write
-
-
- Preview
-
-
- }
- />
-
-
- {`## Instructions
-Implement a singly linked list with the following methods:
-- append(value)
-- prepend(value)
-- delete(value)
-- find(value)
-
-### Constraints
-- Time Complexity: O(n) for searching
-- Space Complexity: O(1) for deletions
-
-Ensure all edge cases are handled (empty list, single node list).`}
-
-
-
} title="Starter Files" />
-
- {starterFiles.map((file) => (
-
-
-
{file.icon}
-
-
{file.name}
-
{file.meta}
-
-
-
- ))}
-
-
- Upload Additional Files
-
-
-
-
-
-
-
-
- Oct 15, 2026
-
-
- 100
-
-
- Python 3.10
-
-
-
-
-
-
-
-
-
- Visibility
- Hidden
-
-
- Category
- Coding
-
-
-
-
- Verify Environment
-
-
-
-
-
- Current Status
- Draft Saving...
-
- }
- actions={
- <>
- Save Draft
- Publish
- >
- }
- />
-
- );
-}
-
-function SubmissionsPage() {
- return (
-
-
- }
- title="Submission workflows will plug in here"
- description="The design system now supports high-density queue views and calm operational empty states."
- />
-
- );
-}
-
-function GradebookPage() {
- return (
-
-
-
-
- Academic Overview
- {brand.courseLabel} • {brand.courseTerm}
-
-
- Average Grade
- 84.2
-
-
- Completion Rate
- 92%
-
-
- Next Deadline
- Oct 12
-
-
-
-
-
-
-
- All Students
-
-
-
-
-
- All Statuses
-
-
-
-
-
-
- Export CSV
-
-
-
- View Settings
-
-
-
-
-
-
-
-
- Student Name
- HW 1
- HW 2
- HW 3
- HW 4
- Midterm
- Overall
-
-
-
- {gradebookRows.map((row) => (
-
-
-
-
- {row.initials}
-
-
-
{row.student}
-
{row.email}
-
-
-
- {row.scores.map((score, index) => (
-
- {score.value}
- {score.status}
-
- ))}
-
- {row.overall}
-
-
- ))}
-
-
-
-
-
-
-
-
- {gradeDistribution.map((bar) => (
-
- ))}
-
-
-
-
-
- 3 students selected
- Batch actions ready
-
- }
- actions={
- <>
- Message
- Batch Edit
- Release Selected
- >
- }
- />
-
- );
-}
-
-function StudentsPage() {
- return (
-
-
- }
- title="Roster primitives are now part of the system"
- description="Issue-specific student views can reuse the gradebook table rhythm, metadata labels, and sidebar shell without forking styles."
- />
-
- );
-}
-
-function SettingsPage() {
- return (
-
-
-
-
- } />
-
- Manual review gate
-
-
- Hidden until publish
-
-
-
- } />
-
- 2 retries
-
-
- Full term archive
-
-
-
-
- );
-}
+import { queryClient, router } from "./routes/router";
const root = ReactDOM.createRoot(document.getElementById("root")!);
diff --git a/frontend/apps/apollo/src/routes/assignment-editor.tsx b/frontend/apps/apollo/src/routes/assignment-editor.tsx
new file mode 100644
index 0000000..30aca4a
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/assignment-editor.tsx
@@ -0,0 +1,10 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { AssignmentBuilderPage } from "../features/assignments/AssignmentBuilderPage";
+import { protectedLayoutRoute } from "./protected";
+
+export const assignmentEditorRoute = createRoute({
+ getParentRoute: () => protectedLayoutRoute,
+ path: "/assignments/new",
+ component: AssignmentBuilderPage,
+});
diff --git a/frontend/apps/apollo/src/routes/assignments.tsx b/frontend/apps/apollo/src/routes/assignments.tsx
new file mode 100644
index 0000000..9c1014f
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/assignments.tsx
@@ -0,0 +1,10 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { AssignmentsPage } from "../features/assignments/AssignmentsPage";
+import { protectedLayoutRoute } from "./protected";
+
+export const assignmentsRoute = createRoute({
+ getParentRoute: () => protectedLayoutRoute,
+ path: "/assignments",
+ component: AssignmentsPage,
+});
diff --git a/frontend/apps/apollo/src/routes/auth.tsx b/frontend/apps/apollo/src/routes/auth.tsx
new file mode 100644
index 0000000..31d68d0
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/auth.tsx
@@ -0,0 +1,41 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { LoginPage } from "../features/auth/routes/LoginPage";
+import { SignupPage } from "../features/auth/routes/SignupPage";
+import {
+ redirectAuthenticated,
+ sanitizeRedirectPath,
+ type AuthSearch,
+} from "../features/auth/session";
+import { AuthLayout } from "../features/shell/AuthLayout";
+import { rootRoute } from "./root";
+
+export const authLayoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: "auth-layout",
+ component: AuthLayout,
+});
+
+export const loginRoute = createRoute({
+ getParentRoute: () => authLayoutRoute,
+ path: "/login",
+ validateSearch: (search: Record): AuthSearch => ({
+ redirect: sanitizeRedirectPath(search.redirect),
+ }),
+ beforeLoad: async ({ context, search }) => {
+ await redirectAuthenticated(context.queryClient, sanitizeRedirectPath(search.redirect));
+ },
+ component: LoginPage,
+});
+
+export const signupRoute = createRoute({
+ getParentRoute: () => authLayoutRoute,
+ path: "/signup",
+ validateSearch: (search: Record): AuthSearch => ({
+ redirect: sanitizeRedirectPath(search.redirect),
+ }),
+ beforeLoad: async ({ context, search }) => {
+ await redirectAuthenticated(context.queryClient, sanitizeRedirectPath(search.redirect));
+ },
+ component: SignupPage,
+});
diff --git a/frontend/apps/apollo/src/routes/dashboard.tsx b/frontend/apps/apollo/src/routes/dashboard.tsx
new file mode 100644
index 0000000..c9e8bab
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/dashboard.tsx
@@ -0,0 +1,10 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { DashboardPage } from "../features/dashboard/DashboardPage";
+import { protectedLayoutRoute } from "./protected";
+
+export const dashboardRoute = createRoute({
+ getParentRoute: () => protectedLayoutRoute,
+ path: "/dashboard",
+ component: DashboardPage,
+});
diff --git a/frontend/apps/apollo/src/routes/gradebook.tsx b/frontend/apps/apollo/src/routes/gradebook.tsx
new file mode 100644
index 0000000..b8cf307
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/gradebook.tsx
@@ -0,0 +1,10 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { GradebookPage } from "../features/gradebook/GradebookPage";
+import { protectedLayoutRoute } from "./protected";
+
+export const gradebookRoute = createRoute({
+ getParentRoute: () => protectedLayoutRoute,
+ path: "/gradebook",
+ component: GradebookPage,
+});
diff --git a/frontend/apps/apollo/src/routes/protected.tsx b/frontend/apps/apollo/src/routes/protected.tsx
new file mode 100644
index 0000000..3fa9fe9
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/protected.tsx
@@ -0,0 +1,14 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { requireAuthenticated } from "../features/auth/session";
+import { ProtectedAppLayout } from "../features/shell/ProtectedAppLayout";
+import { rootRoute } from "./root";
+
+export const protectedLayoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: "protected-layout",
+ beforeLoad: async ({ context, location }) => {
+ await requireAuthenticated(context.queryClient, location.pathname);
+ },
+ component: ProtectedAppLayout,
+});
diff --git a/frontend/apps/apollo/src/routes/root.tsx b/frontend/apps/apollo/src/routes/root.tsx
new file mode 100644
index 0000000..b434c13
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/root.tsx
@@ -0,0 +1,14 @@
+import type { QueryClient } from "@tanstack/react-query";
+import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
+
+export type AppContext = {
+ queryClient: QueryClient;
+};
+
+function RootRouteComponent() {
+ return ;
+}
+
+export const rootRoute = createRootRouteWithContext()({
+ component: RootRouteComponent,
+});
diff --git a/frontend/apps/apollo/src/routes/router.tsx b/frontend/apps/apollo/src/routes/router.tsx
new file mode 100644
index 0000000..d00e74b
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/router.tsx
@@ -0,0 +1,53 @@
+import { QueryClient } from "@tanstack/react-query";
+import { createRoute, createRouter, redirect } from "@tanstack/react-router";
+
+import { ensureSession } from "../features/auth/session";
+import { assignmentEditorRoute } from "./assignment-editor";
+import { assignmentsRoute } from "./assignments";
+import { authLayoutRoute, loginRoute, signupRoute } from "./auth";
+import { dashboardRoute } from "./dashboard";
+import { gradebookRoute } from "./gradebook";
+import { protectedLayoutRoute } from "./protected";
+import { rootRoute, type AppContext } from "./root";
+import { settingsRoute } from "./settings";
+import { studentsRoute } from "./students";
+import { submissionsRoute } from "./submissions";
+
+export const queryClient = new QueryClient();
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: "/",
+ beforeLoad: async ({ context }) => {
+ const session = await ensureSession(context.queryClient);
+ throw redirect({ to: session ? "/dashboard" : "/login" });
+ },
+});
+
+const routeTree = rootRoute.addChildren([
+ indexRoute,
+ authLayoutRoute.addChildren([loginRoute, signupRoute]),
+ protectedLayoutRoute.addChildren([
+ dashboardRoute,
+ assignmentsRoute,
+ assignmentEditorRoute,
+ submissionsRoute,
+ gradebookRoute,
+ studentsRoute,
+ settingsRoute,
+ ]),
+]);
+
+export const router = createRouter({
+ routeTree,
+ context: {
+ queryClient,
+ } satisfies AppContext,
+ defaultPreload: "intent",
+});
+
+declare module "@tanstack/react-router" {
+ interface Register {
+ router: typeof router;
+ }
+}
diff --git a/frontend/apps/apollo/src/routes/settings.tsx b/frontend/apps/apollo/src/routes/settings.tsx
new file mode 100644
index 0000000..b52eaeb
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/settings.tsx
@@ -0,0 +1,10 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { SettingsPage } from "../features/settings/SettingsPage";
+import { protectedLayoutRoute } from "./protected";
+
+export const settingsRoute = createRoute({
+ getParentRoute: () => protectedLayoutRoute,
+ path: "/settings",
+ component: SettingsPage,
+});
diff --git a/frontend/apps/apollo/src/routes/students.tsx b/frontend/apps/apollo/src/routes/students.tsx
new file mode 100644
index 0000000..fcceeb9
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/students.tsx
@@ -0,0 +1,10 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { StudentsPage } from "../features/students/StudentsPage";
+import { protectedLayoutRoute } from "./protected";
+
+export const studentsRoute = createRoute({
+ getParentRoute: () => protectedLayoutRoute,
+ path: "/students",
+ component: StudentsPage,
+});
diff --git a/frontend/apps/apollo/src/routes/submissions.tsx b/frontend/apps/apollo/src/routes/submissions.tsx
new file mode 100644
index 0000000..937623d
--- /dev/null
+++ b/frontend/apps/apollo/src/routes/submissions.tsx
@@ -0,0 +1,10 @@
+import { createRoute } from "@tanstack/react-router";
+
+import { SubmissionsPage } from "../features/submissions/SubmissionsPage";
+import { protectedLayoutRoute } from "./protected";
+
+export const submissionsRoute = createRoute({
+ getParentRoute: () => protectedLayoutRoute,
+ path: "/submissions",
+ component: SubmissionsPage,
+});