feat(app): add types, mock data, and transpose utility

This commit is contained in:
2026-04-08 02:27:13 +02:00
parent eeb1ea9615
commit 16fd6ba9ad
3 changed files with 215 additions and 0 deletions

144
app/app/lib/mock.ts Normal file
View File

@@ -0,0 +1,144 @@
import type { Song, SongSummary } from "./types";
const OCEAN: Song = {
meta: {
title: "A Drop In The Ocean",
artist: "Ron Pope",
capo: null,
original_key: "Em",
tuning: null,
tempo: null,
},
sections: [
{
kind: "chorus",
label: "Chorus",
lines: [
{
text: "A drop in the ocean,",
chords: [{ offset: 0, chord: "Em" }, { offset: 12, chord: "C" }],
},
{
text: "A change in the weather,",
chords: [{ offset: 2, chord: "G" }, { offset: 16, chord: "D" }],
},
{
text: "I was praying that you and me might end up together.",
chords: [
{ offset: 6, chord: "Em" },
{ offset: 17, chord: "C" },
{ offset: 33, chord: "G" },
{ offset: 44, chord: "D" },
],
},
],
},
{
kind: "verse",
label: "Verse",
lines: [
{
text: "I don't wanna waste the weekend,",
chords: [{ offset: 0, chord: "C" }, { offset: 15, chord: "G" }],
},
{
text: "If you don't love me, pretend",
chords: [{ offset: 3, chord: "D" }, { offset: 21, chord: "Em" }],
},
{
text: "A few more hours, then it's time to go.",
chords: [{ offset: 2, chord: "C" }, { offset: 18, chord: "G" }, { offset: 30, chord: "D" }],
},
],
},
{
kind: "bridge",
label: "Bridge",
lines: [
{
text: "Still I can't let you be,",
chords: [{ offset: 0, chord: "Am" }, { offset: 11, chord: "G" }, { offset: 13, chord: "D" }],
},
],
},
],
};
const NAKED: Song = {
meta: {
title: "Naked",
artist: "James Arthur",
capo: null,
original_key: "G",
tuning: null,
tempo: null,
},
sections: [
{
kind: "chorus",
label: "Chorus",
lines: [
{
text: "I'm not going to wait until you're done",
chords: [{ offset: 0, chord: "G" }, { offset: 18, chord: "Bm" }],
},
{
text: "Pretending you don't need anyone",
chords: [{ offset: 16, chord: "Em" }, { offset: 27, chord: "C" }],
},
{
text: "I'm standing here naked",
chords: [{ offset: 19, chord: "G" }],
},
],
},
{
kind: "verse",
label: "Verse",
lines: [
{
text: "I lay awake thinking of all I wasted",
chords: [{ offset: 0, chord: "G" }, { offset: 22, chord: "Bm" }],
},
{
text: "All of the time we had I took for granted",
chords: [{ offset: 11, chord: "Em" }, { offset: 32, chord: "C" }],
},
],
},
],
};
const SONGS_MAP: Record<string, Song> = {
"song-ocean": OCEAN,
"song-naked": NAKED,
};
function previewChords(song: Song): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const section of song.sections) {
for (const line of section.lines) {
for (const cp of line.chords) {
if (!seen.has(cp.chord)) {
seen.add(cp.chord);
result.push(cp.chord);
}
}
}
if (result.length >= 5) break;
}
return result.slice(0, 5);
}
export const MOCK_SONGS: SongSummary[] = Object.entries(SONGS_MAP).map(
([id, song]) => ({
id,
meta: song.meta,
preview_chords: previewChords(song),
})
);
export function getMockSong(id: string): Song | null {
return SONGS_MAP[id] ?? null;
}

34
app/app/lib/transpose.ts Normal file
View File

@@ -0,0 +1,34 @@
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),
})),
})),
})),
};
}

37
app/app/lib/types.ts Normal file
View File

@@ -0,0 +1,37 @@
export interface ChordPosition {
offset: number;
chord: string;
}
export interface LyricLine {
text: string;
chords: ChordPosition[];
}
export interface Section {
kind: string;
label: string | null;
lines: LyricLine[];
}
export interface SongMeta {
title: string;
artist: string;
capo: number | null;
original_key: string | null;
tuning: string | null;
tempo: number | null;
}
export interface Song {
meta: SongMeta;
sections: Section[];
}
// Trimmed version used in the library grid
export interface SongSummary {
id: string;
meta: SongMeta;
// First 5 unique chord names from the song, in order of appearance
preview_chords: string[];
}