Spaces:
Sleeping
Sleeping
| import React, { useMemo } from 'react' | |
| const KEY_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] | |
| export function transposeChord(chord, semitones) { | |
| if (!chord || chord === 'N.C.' || semitones === 0) return chord | |
| let root, suffix | |
| if (chord.length >= 2 && chord[1] === '#') { | |
| root = chord.slice(0, 2) | |
| suffix = chord.slice(2) | |
| } else { | |
| root = chord[0] | |
| suffix = chord.slice(1) | |
| } | |
| const idx = KEY_NAMES.indexOf(root) | |
| if (idx === -1) return chord | |
| return KEY_NAMES[(idx + semitones + 120) % 12] + suffix | |
| } | |
| function ChordDisplay({ chordsData, semitones = 0 }) { | |
| const chords = chordsData?.chords || [] | |
| // Build unique chord list | |
| const uniqueChords = useMemo(() => { | |
| const seen = new Set() | |
| return chords | |
| .filter(c => c.chord !== 'N.C.') | |
| .map(c => transposeChord(c.chord, semitones)) | |
| .filter(c => { | |
| if (seen.has(c)) return false | |
| seen.add(c) | |
| return true | |
| }) | |
| }, [chords, semitones]) | |
| if (!chordsData || uniqueChords.length === 0) return null | |
| return ( | |
| <div className="card rounded-xl p-5 animate-fade-in"> | |
| <div className="flex items-center justify-between mb-3"> | |
| <h2 className="h-display text-xl text-white tracking-wider">Chords in this Song</h2> | |
| <span className="text-[10px] text-gray-400 uppercase tracking-wider"> | |
| {chordsData.chord_source === 'midi' ? 'MIDI' : 'Audio'} | |
| </span> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {uniqueChords.map((chord, i) => ( | |
| <span | |
| key={i} | |
| className="px-3 py-1.5 rounded-lg bg-surface-elevated border border-white/[0.06] text-sm font-semibold text-gray-300" | |
| > | |
| {chord} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default React.memo(ChordDisplay) | |