feat(app): add song detail page with chord chart and transposition

This commit is contained in:
2026-04-08 02:32:50 +02:00
parent 3d18bb9e77
commit 9b68069151
2 changed files with 76 additions and 0 deletions

6
app/app/routes.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("songs/:id", "routes/songs.$id.tsx"),
] satisfies RouteConfig;

View File

@@ -0,0 +1,70 @@
import { useState } from "react";
import { data } from "react-router";
import type { Route } from "./+types/songs.$id";
import { TransposeBar } from "~/components/transpose-bar";
import { ChordChart } from "~/components/chord-chart";
import { transposeSong } from "~/lib/transpose";
import { getMockSong } from "~/lib/mock";
import type { Song } from "~/lib/types";
export function meta({ data }: Route.MetaArgs) {
if (!data) return [{ title: "Song not found" }];
return [
{ title: `${data.song.meta.title} — PocketChords` },
{ name: "description", content: `${data.song.meta.artist}` },
];
}
export function loader({ params }: Route.LoaderArgs) {
const id = params.id ?? "";
// Temporary songs are stored in sessionStorage on the client.
// During SSR, we can't access sessionStorage — return a placeholder
// that the client will hydrate from sessionStorage.
if (id.startsWith("new-")) {
return { song: null as unknown as Song, tempId: id };
}
const song = getMockSong(id);
if (!song) throw data("Song not found", { status: 404 });
return { song, tempId: null };
}
export default function SongDetail({ loaderData }: Route.ComponentProps) {
const { tempId } = loaderData;
// Resolve song — either from loader or from sessionStorage (temp songs)
const [song] = useState<Song>(() => {
if (loaderData.song) return loaderData.song;
if (tempId && typeof window !== "undefined") {
const stored = sessionStorage.getItem(tempId);
if (stored) return JSON.parse(stored) as Song;
}
return loaderData.song;
});
const [offset, setOffset] = useState(0);
if (!song) {
return (
<div className="flex items-center justify-center h-dvh">
<p className="text-muted-foreground">Song not found.</p>
</div>
);
}
const displayed = transposeSong(song, offset);
return (
<div className="flex flex-col h-dvh max-w-lg mx-auto">
<TransposeBar
meta={song.meta}
offset={offset}
onOffsetChange={setOffset}
/>
<div className="flex-1 overflow-y-auto">
<ChordChart sections={displayed.sections} />
</div>
</div>
);
}