Files
sexy/app/(app)/sessions/[id]/page.tsx
T
valknarandClaude Sonnet 5 39539b6f94 Fix title/description overflow by restoring stretch on mobile
items-start on the stacked mobile header removed the default stretch,
so the min-w-0/flex-1 title column sized to its own content instead of
the row's width - leaving max-w-full on the field-sizing:content
inputs with no definite containing-block width to clamp against, so
they (and the whole row) could overflow horizontally. Dropping
items-start restores the default stretch; the right-hand badge/
duration/replay group already had its own self-end override, so it
still sits at the right edge.

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

95 lines
4.1 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 { STATUS_VARIANT } from "@/components/sessions/SessionsTable";
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>
)}
</div>
</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>
);
}