From af328deac12dcf3c39437ff218f3127bfb6120a5 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 11 Jul 2026 22:30:59 +0200 Subject: [PATCH] refactor: backend-only transpose, remove client-side transpose logic --- app/app/lib/api.ts | 13 +++++++--- app/app/lib/transpose.ts | 34 ------------------------- app/app/routes/songs.$id.tsx | 25 +++++++----------- crates/api-types/src/lib.rs | 1 + crates/presentation/src/routes/songs.rs | 7 +++++ 5 files changed, 27 insertions(+), 53 deletions(-) delete mode 100644 app/app/lib/transpose.ts diff --git a/app/app/lib/api.ts b/app/app/lib/api.ts index b86ac86..df89ccd 100644 --- a/app/app/lib/api.ts +++ b/app/app/lib/api.ts @@ -94,9 +94,16 @@ export async function listSongs(q = "", sort = "date", order = "desc"): Promise< return res.json(); } -export async function getSong(id: string, applyCapo = false): Promise { - const url = applyCapo - ? `${API_BASE}/songs/${id}?apply_capo=true` +export async function getSong( + id: string, + opts: { applyCapo?: boolean; transpose?: number } = {}, +): Promise { + const params = new URLSearchParams(); + if (opts.applyCapo) params.set("apply_capo", "true"); + if (opts.transpose && opts.transpose !== 0) + params.set("transpose", String(opts.transpose)); + const url = params.size + ? `${API_BASE}/songs/${id}?${params}` : `${API_BASE}/songs/${id}`; const res = await fetch(url); if (res.status === 404) return null; diff --git a/app/app/lib/transpose.ts b/app/app/lib/transpose.ts deleted file mode 100644 index da96576..0000000 --- a/app/app/lib/transpose.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Song } from "./types"; - -const NOTES_SHARP = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; -const NOTES_FLAT = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]; - -function transposeChord(chord: string, semitones: number): string { - const match = chord.match(/^([A-G][#b]?)(.*)/); - if (!match) return chord; - const [, root, descriptor] = match; - const idx = NOTES_SHARP.indexOf(root) !== -1 - ? NOTES_SHARP.indexOf(root) - : NOTES_FLAT.indexOf(root); - if (idx === -1) return chord; - const newIdx = ((idx + semitones) % 12 + 12) % 12; - const notes = semitones >= 0 ? NOTES_SHARP : NOTES_FLAT; - return notes[newIdx] + descriptor; -} - -export function transposeSong(song: Song, semitones: number): Song { - if (semitones === 0) return song; - return { - ...song, - sections: song.sections.map((section) => ({ - ...section, - lines: section.lines.map((line) => ({ - ...line, - chords: line.chords.map((cp) => ({ - ...cp, - chord: transposeChord(cp.chord, semitones), - })), - })), - })), - }; -} diff --git a/app/app/routes/songs.$id.tsx b/app/app/routes/songs.$id.tsx index da88d66..0c60dee 100644 --- a/app/app/routes/songs.$id.tsx +++ b/app/app/routes/songs.$id.tsx @@ -11,7 +11,6 @@ import { EditSongSheet } from "~/components/edit-song-sheet"; import { DeleteSongDialog } from "~/components/delete-song-dialog"; import { SectionNav } from "~/components/section-nav"; import { AutoScrollControls } from "~/components/auto-scroll-controls"; -import { transposeSong } from "~/lib/transpose"; import { extractUniqueChords } from "~/lib/song-utils"; import { getSong } from "~/lib/api"; import { useAuth } from "~/lib/auth"; @@ -82,23 +81,19 @@ export default function SongDetail() { useEffect(() => { setLoading(true); - getSong(id) + getSong(id, { transpose: initOffset }) .then((s) => { setBaseSong(s); setDisplayedSong(s); }) .finally(() => setLoading(false)); - }, [id]); + }, [id]); // eslint-disable-line useEffect(() => { - if (applyCapo && baseSong?.meta.capo) { - getSong(id, true).then((s) => { - if (s) setDisplayedSong(s); - }); - } else { - setDisplayedSong(baseSong); - } - }, [applyCapo]); // eslint-disable-line + getSong(id, { applyCapo, transpose: offset }).then((s) => { + if (s) setDisplayedSong(s); + }); + }, [id, applyCapo, offset]); function handleOffsetChange(newOffset: number) { setOffset(newOffset); @@ -158,9 +153,8 @@ export default function SongDetail() { ); } - const displayed = transposeSong(displayedSong, offset); - const uniqueChords = extractUniqueChords(displayed.sections); - const sectionItems = displayed.sections.map((s, i) => ({ + const uniqueChords = extractUniqueChords(displayedSong.sections); + const sectionItems = displayedSong.sections.map((s, i) => ({ label: s.label, index: i, })); @@ -200,7 +194,7 @@ export default function SongDetail() { >
@@ -224,7 +218,6 @@ export default function SongDetail() {
- {/* Bottom toolbar: section nav + auto-scroll */}
diff --git a/crates/api-types/src/lib.rs b/crates/api-types/src/lib.rs index c915395..8078ac8 100644 --- a/crates/api-types/src/lib.rs +++ b/crates/api-types/src/lib.rs @@ -29,6 +29,7 @@ pub struct UpdateSongRequest { #[derive(Deserialize, ToSchema, IntoParams)] pub struct GetSongQuery { pub apply_capo: Option, + pub transpose: Option, } #[derive(Deserialize, ToSchema)] diff --git a/crates/presentation/src/routes/songs.rs b/crates/presentation/src/routes/songs.rs index e5c7017..2931917 100644 --- a/crates/presentation/src/routes/songs.rs +++ b/crates/presentation/src/routes/songs.rs @@ -155,6 +155,13 @@ pub async fn get_song( song }; + let song = match params.transpose { + Some(semitones) if semitones != 0 => { + ChordTransposer.transpose_song(&song, semitones) + } + _ => song, + }; + Ok(Json(song)) }