From 9b6806915117bd71d8105d2b79587b62e573a08a Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 8 Apr 2026 02:32:50 +0200 Subject: [PATCH] feat(app): add song detail page with chord chart and transposition --- app/app/routes.ts | 6 ++++ app/app/routes/songs.$id.tsx | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 app/app/routes.ts create mode 100644 app/app/routes/songs.$id.tsx diff --git a/app/app/routes.ts b/app/app/routes.ts new file mode 100644 index 0000000..bfb9c80 --- /dev/null +++ b/app/app/routes.ts @@ -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; diff --git a/app/app/routes/songs.$id.tsx b/app/app/routes/songs.$id.tsx new file mode 100644 index 0000000..d00afa2 --- /dev/null +++ b/app/app/routes/songs.$id.tsx @@ -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(() => { + 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 ( +
+

Song not found.

+
+ ); + } + + const displayed = transposeSong(song, offset); + + return ( +
+ +
+ +
+
+ ); +}