Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-06-30 20:44:37 +00:00
co-authored by renee-png
parent e094b56e1b
commit eeeae5181a
8 changed files with 1226 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Plus, ChevronRight } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createFileRoute("/_authenticated/students")({
head: () => ({ meta: [{ title: "Students — School Portal" }] }),
component: StudentsPage,
});
function StudentsPage() {
const { roles } = useAuth();
const isAdmin = roles.includes("admin");
const qc = useQueryClient();
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 ?? [];
},
});
const { data: classes } = useQuery({
queryKey: ["classes"],
queryFn: async () => (await supabase.from("classes").select("*").order("name")).data ?? [],
});
const [open, setOpen] = useState(false);
const [form, setForm] = useState({ first_name: "", last_name: "", dob: "", class_id: "", allergies: "" });
const createStudent = useMutation({
mutationFn: async () => {
const { error } = await supabase.from("students").insert({
first_name: form.first_name,
last_name: form.last_name,
dob: form.dob || null,
class_id: form.class_id || null,
allergies: form.allergies || null,
});
if (error) throw error;
},
onSuccess: () => {
toast.success("Student added");
setOpen(false);
setForm({ first_name: "", last_name: "", dob: "", class_id: "", allergies: "" });
qc.invalidateQueries({ queryKey: ["students"] });
},
onError: (e: Error) => toast.error(e.message),
});
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 && (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild><Button><Plus className="h-4 w-4 mr-1" /> Add student</Button></DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>New student</DialogTitle></DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div><Label>First name</Label><Input value={form.first_name} onChange={(e) => setForm({ ...form, first_name: e.target.value })} /></div>
<div><Label>Last name</Label><Input value={form.last_name} onChange={(e) => setForm({ ...form, last_name: e.target.value })} /></div>
</div>
<div><Label>Date of birth</Label><Input type="date" value={form.dob} onChange={(e) => setForm({ ...form, dob: e.target.value })} /></div>
<div>
<Label>Class</Label>
<Select value={form.class_id} onValueChange={(v) => setForm({ ...form, class_id: v })}>
<SelectTrigger><SelectValue placeholder="Choose class" /></SelectTrigger>
<SelectContent>{(classes ?? []).map((c) => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}</SelectContent>
</Select>
</div>
<div><Label>Allergies</Label><Input value={form.allergies} onChange={(e) => setForm({ ...form, allergies: e.target.value })} placeholder="None" /></div>
<Button className="w-full" disabled={!form.first_name || !form.last_name || createStudent.isPending} onClick={() => createStudent.mutate()}>Create student</Button>
</div>
</DialogContent>
</Dialog>
)}
</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>
);
}