Files
sexy/app/(app)/sessions/[id]/page.tsx
T
valknarandClaude Sonnet 5 b58640beb2 Unify card title size to text-sm
Several CardTitle usages were text-base while others were already
text-sm, giving section headers inconsistent sizes across pages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuWtppPBizG2NQRa31FCRa
2026-09-05 11:29:41 +02:00

97 lines
4.3 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 { Badge } from "@/components/ui/badge";
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 { SessionDescriptionEditor } from "@/components/sessions/SessionDescriptionEditor";
import { SessionDeleteButton } from "@/components/sessions/SessionDeleteButton";
import { STATUS_VARIANT } from "@/components/sessions/session-status";
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 flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div className="min-w-0 flex-1">
<SessionTitleEditor sessionId={detail.session.id} initialName={detail.session.name} />
{(detail.session.kind === "replay" || detail.session.playCount > 0) && (
<p className="mt-1 text-sm text-muted-foreground">
{detail.session.kind === "replay" && detail.replayedFromName !== undefined && (
<>replayed from {detail.replayedFromName ?? `session #${detail.session.replayedSessionId}`}</>
)}
{detail.session.playCount > 0 && (
<>
{detail.session.kind === "replay" && detail.replayedFromName !== undefined && " · "}
replayed {detail.session.playCount}×
{detail.session.lastPlayedAt && ` (last ${new Date(detail.session.lastPlayedAt).toLocaleString()})`}
</>
)}
</p>
)}
<SessionDescriptionEditor sessionId={detail.session.id} initialDescription={detail.session.description} />
</div>
<div className="flex shrink-0 items-center gap-3 self-end sm:self-auto">
<Badge variant={STATUS_VARIANT[detail.session.status]}>{detail.session.status}</Badge>
<span className="bp-readout text-sm text-muted-foreground">{formatDuration(detail.session.durationMs)}</span>
{canReplay && (
<Button asChild>
<Link href={`/sessions/${detail.session.id}/replay`}>
<Play className="size-4" /> Replay
</Link>
</Button>
)}
<SessionDeleteButton sessionId={detail.session.id} sessionName={detail.session.name} />
</div>
</div>
<Card className="bp-glass">
<CardHeader>
<CardTitle className="text-sm">Intensity timeline</CardTitle>
</CardHeader>
<CardContent>
<SessionTimelineChart timeline={timeline} />
</CardContent>
</Card>
<Card className="bp-glass">
<CardHeader>
<CardTitle className="text-sm">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>
);
}