diff --git a/app/(app)/sessions/[id]/page.tsx b/app/(app)/sessions/[id]/page.tsx index a6ef620..aa39946 100644 --- a/app/(app)/sessions/[id]/page.tsx +++ b/app/(app)/sessions/[id]/page.tsx @@ -4,6 +4,7 @@ import type { Metadata } from "next"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { SessionTimelineChart } from "@/components/sessions/SessionTimelineChart"; +import { SessionTitleEditor } from "@/components/sessions/SessionTitleEditor"; import { getSessionDetail, getSessionName } from "@/lib/db/queries/sessions"; import { getSessionTimeline } from "@/lib/db/queries/stats"; import { Play } from "lucide-react"; @@ -33,9 +34,9 @@ export default async function SessionDetailPage({ params }: { params: Promise<{ return (
-
-

{detail.session.name ?? `Session #${detail.session.id}`}

-

+

+ +

{detail.session.status} · {formatDuration(detail.session.durationMs)} {detail.session.kind === "replay" && detail.replayedFromName !== undefined && ( <> · replayed from {detail.replayedFromName ?? `session #${detail.session.replayedSessionId}`} @@ -49,7 +50,7 @@ export default async function SessionDetailPage({ params }: { params: Promise<{ )}

{detail.session.description && ( -

{detail.session.description}

+

{detail.session.description}

)}
{canReplay && ( diff --git a/components/sessions/SessionTitleEditor.tsx b/components/sessions/SessionTitleEditor.tsx new file mode 100644 index 0000000..7830876 --- /dev/null +++ b/components/sessions/SessionTitleEditor.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Check, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + +const AUTOSAVE_DELAY_MS = 600; + +export function SessionTitleEditor({ sessionId, initialName }: { sessionId: number; initialName: string | null }) { + const router = useRouter(); + const initial = initialName ?? ""; + const [value, setValue] = useState(initial); + const [status, setStatus] = useState<"idle" | "saving" | "saved">("idle"); + const lastSaved = useRef(initial); + + useEffect(() => { + const trimmed = value.trim(); + if (trimmed.length === 0 || trimmed === lastSaved.current) return; + + setStatus("saving"); + const timer = setTimeout(async () => { + const res = await fetch(`/api/sessions/${sessionId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: trimmed }), + }); + if (res.ok) { + lastSaved.current = trimmed; + setStatus("saved"); + router.refresh(); + } else { + setStatus("idle"); + toast.error("Could not rename session"); + } + }, AUTOSAVE_DELAY_MS); + + return () => clearTimeout(timer); + }, [value, sessionId, router]); + + useEffect(() => { + if (status !== "saved") return; + const timer = setTimeout(() => setStatus("idle"), 1500); + return () => clearTimeout(timer); + }, [status]); + + return ( +
+ setValue(e.target.value)} + placeholder={`Session #${sessionId}`} + aria-label="Session name" + className="min-w-0 flex-1 rounded-md border border-transparent bg-transparent px-2 py-0.5 font-heading text-2xl font-semibold text-foreground outline-none transition-colors placeholder:text-muted-foreground/70 hover:border-border focus:border-border focus:bg-muted/30" + /> +
+ + +
+
+ ); +}