From e9c2f7a5e0bc07397c0e8f56e2b6f56547148a90 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 9 Apr 2026 00:44:32 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20chord=20diagram=20=E2=80=94=20piano/gui?= =?UTF-8?q?tar=20diagrams=20in=20song=20detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/routes/songs.$id.tsx | 79 ++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/app/app/routes/songs.$id.tsx b/app/app/routes/songs.$id.tsx index 6ad0ec7..c3d44a9 100644 --- a/app/app/routes/songs.$id.tsx +++ b/app/app/routes/songs.$id.tsx @@ -1,11 +1,15 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef, useCallback } 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 { ChordGrid } from "~/components/chord-diagram/chord-grid"; +import { ChordDiagram } from "~/components/chord-diagram/chord-diagram"; +import type { Instrument } from "~/components/chord-diagram/chord-diagram"; import { EditSongSheet } from "~/components/edit-song-sheet"; import { DeleteSongDialog } from "~/components/delete-song-dialog"; import { transposeSong } from "~/lib/transpose"; +import { extractUniqueChords } from "~/lib/song-utils"; import { getSong } from "~/lib/api"; import type { Song, SongSummary } from "~/lib/types"; @@ -41,6 +45,14 @@ function initFontSize(): FontSize { return 'sm'; } +function initInstrument(): Instrument { + try { + const v = localStorage.getItem('chordDiagramInstrument'); + if (v === 'piano' || v === 'guitar') return v; + } catch { /* noop */ } + return 'piano'; +} + export default function SongDetail({ loaderData }: Route.ComponentProps) { const { song: initialSong, id } = loaderData; const [baseSong, setBaseSong] = useState(initialSong ?? null); @@ -62,6 +74,9 @@ export default function SongDetail({ loaderData }: Route.ComponentProps) { const [fontSize, setFontSize] = useState(initFontSize); const [editOpen, setEditOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); + const [activeChord, setActiveChord] = useState(null); + const [instrument, setInstrument] = useState(initInstrument); + const scrollRef = useRef(null); useEffect(() => { if (applyCapo && baseSong?.meta.capo) { @@ -81,6 +96,13 @@ export default function SongDetail({ loaderData }: Route.ComponentProps) { try { localStorage.setItem('fontSize', size); } catch { /* noop */ } } + function handleInstrumentChange(i: Instrument) { + setInstrument(i); + try { localStorage.setItem('chordDiagramInstrument', i); } catch { /* noop */ } + } + + const handleScroll = useCallback(() => setActiveChord(null), []); + if (!baseSong || !displayedSong) { return (
@@ -93,6 +115,8 @@ export default function SongDetail({ loaderData }: Route.ComponentProps) { } const displayed = transposeSong(displayedSong, offset); + const uniqueChords = extractUniqueChords(displayed.sections); + const handleChordClick = (chord: string) => setActiveChord(chord); function handleUpdated(summary: SongSummary) { setBaseSong((prev) => prev ? { ...prev, meta: summary.meta } : prev); @@ -100,7 +124,7 @@ export default function SongDetail({ loaderData }: Route.ComponentProps) { } return ( -
+
setApplyCapo((v) => !v)} /> -
- + + {/* Body: single column on mobile, two columns on desktop */} +
+ {/* Left / main column */} +
+
+ + + {/* Mobile bottom chord grid (hidden on desktop) */} +
+ +
+
+
+ + {/* Desktop side column (hidden on mobile) */} +
+ +
+ + {/* Mobile inline popup — fixed bottom, dismissed on scroll */} + {activeChord && ( +
+ + +
+ )} +