feat(app): add song detail page with chord chart and transposition
This commit is contained in:
6
app/app/routes.ts
Normal file
6
app/app/routes.ts
Normal 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;
|
||||||
70
app/app/routes/songs.$id.tsx
Normal file
70
app/app/routes/songs.$id.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user