The -mx-2 trick pushed the hover/focus border noticeably left of the text, and flex-1 stretched the input across the whole header row. Tighten the offset to px-1.5/-ml-1.5 and size the input to its content via field-sizing:content instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzyfRyA7h5rs5SCAahLAWd
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
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-1.5 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-1.5 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>
|
||
);
|
||
}
|