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