86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import { useState } from "react";
|
|
import { data, Link } from "react-router";
|
|
import type { Route } from "./+types/songs.$id";
|
|
import { TransposeBar } from "~/components/transpose-bar";
|
|
import { ChordChart } from "~/components/chord-chart";
|
|
import { EditSongSheet } from "~/components/edit-song-sheet";
|
|
import { DeleteSongDialog } from "~/components/delete-song-dialog";
|
|
import { transposeSong } from "~/lib/transpose";
|
|
import { getSong } from "~/lib/api";
|
|
import type { Song, SongSummary } from "~/lib/types";
|
|
|
|
export function meta({ data }: Route.MetaArgs) {
|
|
if (!data?.song) return [{ title: "PocketChords" }];
|
|
return [
|
|
{ title: `${data.song.meta.title} — PocketChords` },
|
|
{ name: "description", content: data.song.meta.artist },
|
|
];
|
|
}
|
|
|
|
export async function loader({ params }: Route.LoaderArgs) {
|
|
const id = params.id ?? "";
|
|
try {
|
|
const song = await getSong(id);
|
|
if (!song) throw data("Song not found", { status: 404 });
|
|
return { song, id };
|
|
} catch (err: unknown) {
|
|
if (err && typeof err === "object" && "status" in err && (err as { status: number }).status === 404) {
|
|
throw err;
|
|
}
|
|
return { song: null as unknown as Song, id };
|
|
}
|
|
}
|
|
|
|
export default function SongDetail({ loaderData }: Route.ComponentProps) {
|
|
const { song: initialSong, id } = loaderData;
|
|
const [song, setSong] = useState<Song | null>(initialSong ?? null);
|
|
const [offset, setOffset] = useState(0);
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
|
|
if (!song) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-full gap-4">
|
|
<p className="text-muted-foreground text-sm">Song not found or unavailable.</p>
|
|
<Link to="/" className="text-sm text-primary underline-offset-4 hover:underline">
|
|
← Back to library
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const displayed = transposeSong(song, offset);
|
|
|
|
function handleUpdated(summary: SongSummary) {
|
|
setSong((prev) => prev ? { ...prev, meta: summary.meta } : prev);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full max-w-lg mx-auto">
|
|
<TransposeBar
|
|
meta={song.meta}
|
|
offset={offset}
|
|
onOffsetChange={setOffset}
|
|
onEdit={() => setEditOpen(true)}
|
|
onDelete={() => setDeleteOpen(true)}
|
|
/>
|
|
<div className="flex-1 overflow-y-auto">
|
|
<ChordChart sections={displayed.sections} />
|
|
</div>
|
|
<EditSongSheet
|
|
id={id}
|
|
meta={song.meta}
|
|
open={editOpen}
|
|
onOpenChange={setEditOpen}
|
|
onUpdated={handleUpdated}
|
|
/>
|
|
<DeleteSongDialog
|
|
id={id}
|
|
title={song.meta.title}
|
|
open={deleteOpen}
|
|
onOpenChange={setDeleteOpen}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|