Fix blank /students/new and /students/$id (missing Outlet)

students.tsx was the list AND the parent of students.new / students.$id but
rendered no <Outlet/>, so child routes never showed. Make students.tsx an
Outlet layout and move the list to students.index.tsx. Add an errorComponent
to students.new as a safety net.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:42:45 -04:00
co-authored by Claude Opus 4.8
parent cee27a6189
commit 9d5b11bb10
4 changed files with 91 additions and 58 deletions
@@ -0,0 +1,59 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Plus, ChevronRight } from "lucide-react";
export const Route = createFileRoute("/_authenticated/students/")({
head: () => ({ meta: [{ title: "Students — School Portal" }] }),
component: StudentsIndexPage,
});
function StudentsIndexPage() {
const { roles } = useAuth();
const isAdmin = roles.includes("admin");
const { data: students } = useQuery({
queryKey: ["students"],
queryFn: async () => {
const { data, error } = await supabase
.from("students")
.select("id, first_name, last_name, dob, allergies, photo_release, class_id, classes(name)")
.order("last_name");
if (error) throw error;
return data ?? [];
},
});
return (
<div className="p-8 max-w-6xl">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-semibold">Students</h1>
<p className="text-muted-foreground text-sm">{students?.length ?? 0} students visible to you</p>
</div>
{isAdmin && (
<Link to="/students/new">
<Button><Plus className="h-4 w-4 mr-1" /> Add student</Button>
</Link>
)}
</div>
<div className="bg-card border rounded-lg divide-y">
{(students ?? []).map((s) => (
<Link to="/students/$id" params={{ id: s.id }} key={s.id} className="flex items-center justify-between px-4 py-3 hover:bg-muted/50">
<div>
<div className="font-medium">{s.last_name}, {s.first_name}</div>
<div className="text-xs text-muted-foreground">
{(s.classes as { name: string } | null)?.name ?? "No class"} · {s.allergies ? `Allergies: ${s.allergies}` : "No allergies"}
</div>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</Link>
))}
{students?.length === 0 && <div className="p-6 text-sm text-muted-foreground text-center">No students yet.</div>}
</div>
</div>
);
}
@@ -15,6 +15,12 @@ import { toast } from "sonner";
export const Route = createFileRoute("/_authenticated/students/new")({
head: () => ({ meta: [{ title: "New student — School Portal" }] }),
component: NewStudentPage,
errorComponent: ({ error }) => (
<div className="p-8 max-w-2xl">
<h1 className="text-lg font-semibold text-destructive">This page hit an error</h1>
<pre className="mt-3 whitespace-pre-wrap break-words text-xs bg-muted p-3 rounded border">{error?.message}{"\n\n"}{error?.stack}</pre>
</div>
),
});
type Guardian = {
+4 -56
View File
@@ -1,59 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Plus, ChevronRight } from "lucide-react";
import { createFileRoute, Outlet } from "@tanstack/react-router";
// Layout route for /students. The list lives in students.index.tsx; the child
// routes (students.new, students.$id) render here through the Outlet.
export const Route = createFileRoute("/_authenticated/students")({
head: () => ({ meta: [{ title: "Students — School Portal" }] }),
component: StudentsPage,
component: () => <Outlet />,
});
function StudentsPage() {
const { roles } = useAuth();
const isAdmin = roles.includes("admin");
const { data: students } = useQuery({
queryKey: ["students"],
queryFn: async () => {
const { data, error } = await supabase
.from("students")
.select("id, first_name, last_name, dob, allergies, photo_release, class_id, classes(name)")
.order("last_name");
if (error) throw error;
return data ?? [];
},
});
return (
<div className="p-8 max-w-6xl">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-semibold">Students</h1>
<p className="text-muted-foreground text-sm">{students?.length ?? 0} students visible to you</p>
</div>
{isAdmin && (
<Link to="/students/new">
<Button><Plus className="h-4 w-4 mr-1" /> Add student</Button>
</Link>
)}
</div>
<div className="bg-card border rounded-lg divide-y">
{(students ?? []).map((s) => (
<Link to="/students/$id" params={{ id: s.id }} key={s.id} className="flex items-center justify-between px-4 py-3 hover:bg-muted/50">
<div>
<div className="font-medium">{s.last_name}, {s.first_name}</div>
<div className="text-xs text-muted-foreground">
{(s.classes as { name: string } | null)?.name ?? "No class"} · {s.allergies ? `Allergies: ${s.allergies}` : "No allergies"}
</div>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</Link>
))}
{students?.length === 0 && <div className="p-6 text-sm text-muted-foreground text-center">No students yet.</div>}
</div>
</div>
);
}