Files
sexy/app/(app)/sessions/[id]/page.tsx
T
valknarandClaude Sonnet 5 884ced7a47 Make the session title editable inline on the detail page
New SessionTitleEditor swaps the static <h1> for a heading-styled input
that autosaves on typing (600ms debounce, spinner-then-checkmark) - the
same click-in-place, no-Save-button pattern as the device display name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzyfRyA7h5rs5SCAahLAWd
2026-08-30 20:49:34 +02:00

89 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { notFound } from "next/navigation";
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";
export const dynamic = "force-dynamic";
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params;
const name = await getSessionName(Number(id));
return { title: name ?? `Session #${id}` };
}
function formatDuration(ms: number | null): string {
if (ms === null) return "-";
const totalSeconds = Math.round(ms / 1000);
return `${Math.floor(totalSeconds / 60)}:${(totalSeconds % 60).toString().padStart(2, "0")}`;
}
export default async function SessionDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const sessionId = Number(id);
const [detail, timeline] = await Promise.all([getSessionDetail(sessionId), getSessionTimeline(sessionId)]);
if (!detail) notFound();
const canReplay = detail.session.status === "completed" && detail.session.durationMs !== null;
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<SessionTitleEditor sessionId={detail.session.id} initialName={detail.session.name} />
<p className="mt-1 px-2 text-sm text-muted-foreground">
{detail.session.status} · {formatDuration(detail.session.durationMs)}
{detail.session.kind === "replay" && detail.replayedFromName !== undefined && (
<> · replayed from {detail.replayedFromName ?? `session #${detail.session.replayedSessionId}`}</>
)}
{detail.session.playCount > 0 && (
<>
{" "}
· replayed {detail.session.playCount}×
{detail.session.lastPlayedAt && ` (last ${new Date(detail.session.lastPlayedAt).toLocaleString()})`}
</>
)}
</p>
{detail.session.description && (
<p className="mt-1 px-2 text-sm text-muted-foreground">{detail.session.description}</p>
)}
</div>
{canReplay && (
<Button asChild>
<Link href={`/sessions/${detail.session.id}/replay`}>
<Play className="size-4" /> Replay
</Link>
</Button>
)}
</div>
<Card className="bp-glass">
<CardHeader>
<CardTitle className="text-base">Intensity timeline</CardTitle>
</CardHeader>
<CardContent>
<SessionTimelineChart timeline={timeline} />
</CardContent>
</Card>
<Card className="bp-glass">
<CardHeader>
<CardTitle className="text-base">Devices</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-1">
{detail.devices.map((d) => (
<div key={d.id} className="text-sm">
{d.slotLabel} <span className="text-muted-foreground">({d.deviceDisplayName ?? d.deviceBleName})</span>
</div>
))}
</CardContent>
</Card>
</div>
);
}