import { useState, useEffect, useRef, useMemo } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; import toast from "react-hot-toast"; import { ChevronLeft, ChevronRight, Edit, Save, X, Music, Mic2, ZoomIn, ZoomOut, Home, ArrowUp, Check, ChevronDown, Plus, Trash2, } from "lucide-react"; import api from "@utils/api"; import { useTheme } from "@context/ThemeContext"; import { useSongs, useSong, useDataMutations } from "@hooks/useDataFetch"; import { ALL_KEYS, ALL_CHORDS, ROOT_NOTES, CHORD_QUALITIES, parseLyricsWithChords, transposeLyricsText, detectKeyFromLyrics, getSemitonesBetweenKeys, getDiatonicChords, getCommonProgressions, } from "@utils/chordSheetUtils"; import LyricsRichTextEditor from "@components/LyricsRichTextEditor"; export default function SongViewPage() { const { songId: id } = useParams(); const navigate = useNavigate(); const { isDark } = useTheme(); const contentRef = useRef(null); // Use cached data from the global store const { songs } = useSongs(); const { song: cachedSong, loading: songLoading } = useSong(id); const { updateSongInCache, invalidateSongDetail } = useDataMutations(); // Core state const [song, setSong] = useState(null); const [loading, setLoading] = useState(true); // Key transposition state const [originalKey, setOriginalKey] = useState("C"); const [selectedKey, setSelectedKey] = useState("C"); const [showKeySelector, setShowKeySelector] = useState(false); // UI state const [isEditing, setIsEditing] = useState(false); const [editForm, setEditForm] = useState({}); const [fontSize, setFontSize] = useState(18); const [showNav, setShowNav] = useState(false); const [saving, setSaving] = useState(false); const [showChords, setShowChords] = useState(true); // Toggle chord visibility const [showDeleteModal, setShowDeleteModal] = useState(false); // Click-to-place chord editor state const [chordEditMode, setChordEditMode] = useState(false); const [selectedChordToPlace, setSelectedChordToPlace] = useState(null); const [showTextEditor, setShowTextEditor] = useState(false); const [showRichTextEditor, setShowRichTextEditor] = useState(false); const [editableLyrics, setEditableLyrics] = useState(""); // Extract original key from song data const extractOriginalKey = (songData) => { if (!songData) return "C"; if (songData.key_chord) return songData.key_chord.replace(/\s+/g, "").split(/[,\/]/)[0]; if (songData.chords) return songData.chords.replace(/\s+/g, "").split(/[,\/\s]/)[0]; if (songData.original_key) return songData.original_key; if (songData.key) return songData.key; // Try to detect from lyrics return detectKeyFromLyrics(songData.lyrics); }; // Update song state when cached data changes useEffect(() => { if (cachedSong) { setSong(cachedSong); setEditForm(cachedSong); // Extract and set the original key const key = extractOriginalKey(cachedSong); setOriginalKey(key); setSelectedKey(key); setLoading(false); } }, [cachedSong]); // Scroll listener for floating nav useEffect(() => { const handleScroll = () => { setShowNav(window.scrollY > 150); }; window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, []); // Navigation between songs const currentIndex = songs.findIndex((s) => s.id === id); const prevSong = currentIndex > 0 ? songs[currentIndex - 1] : null; const nextSong = currentIndex < songs.length - 1 ? songs[currentIndex + 1] : null; // Compute transposed lyrics const transposedLyrics = useMemo(() => { if (!song?.lyrics) return ""; return transposeLyricsText(song.lyrics, originalKey, selectedKey); }, [song?.lyrics, originalKey, selectedKey]); // Parse lyrics with chords for display const parsedLyrics = useMemo(() => { return parseLyricsWithChords(transposedLyrics); }, [transposedLyrics]); // Get diatonic chords for current key const diatonicChords = useMemo(() => { return getDiatonicChords(selectedKey); }, [selectedKey]); // Calculate semitone difference for display const semitonesDiff = useMemo(() => { return getSemitonesBetweenKeys(originalKey, selectedKey); }, [originalKey, selectedKey]); // Apply chords progression above lyrics // This takes the diatonic chord progression and applies it over the lyrics const lyricsWithAppliedChords = useMemo(() => { if (!song?.lyrics || !showChords) return []; const lyrics = transposedLyrics || song.lyrics; // Use the parser to handle both [Chord] embedded format AND chords-above-lyrics format const parsedLines = parseLyricsWithChords(lyrics); return parsedLines.map((segments, lineIdx) => { // Check if this is an empty line if ( segments.length === 1 && !segments[0].chord && !segments[0].text.trim() ) { return { isHeader: false, isEmpty: true, text: "", chordLine: null, }; } // Check if this is a section header const lineText = segments.map((s) => s.text).join(""); const trimmedLine = lineText.trim(); const isHeader = /^[\[\(]?(verse|chorus|bridge|pre-?chorus|intro|outro|interlude|hook|tag|ending|v\d|c\d|ch\d)[\d\s]*[\]\)]?:?$/i.test( trimmedLine, ); if (isHeader) { return { isHeader: true, isEmpty: false, text: lineText, chordLine: null, }; } // Check if this line has any chords const hasChords = segments.some((seg) => seg.chord !== null); if (hasChords) { // Line has chords - return segments for rendering return { isHeader: false, isEmpty: false, text: segments.map((s) => s.text).join(""), segments: segments, hasChords: true, }; } // Plain text line (no chords) return { isHeader: false, isEmpty: false, text: lineText, chordLine: null, hasChords: false, }; }); }, [song?.lyrics, transposedLyrics, diatonicChords, showChords]); // Handle key change const handleKeyChange = (newKey) => { setSelectedKey(newKey); setShowKeySelector(false); }; // Quick transpose up/down by semitone const transposeUp = () => { const majorKeys = ALL_KEYS.filter((k) => k.type === "major"); const currentIdx = majorKeys.findIndex((k) => k.value === selectedKey); if (currentIdx >= 0) { const nextIdx = (currentIdx + 1) % majorKeys.length; setSelectedKey(majorKeys[nextIdx].value); } else { // For minor keys const minorKeys = ALL_KEYS.filter((k) => k.type === "minor"); const minorIdx = minorKeys.findIndex((k) => k.value === selectedKey); const nextIdx = (minorIdx + 1) % minorKeys.length; setSelectedKey(minorKeys[nextIdx].value); } }; const transposeDown = () => { const majorKeys = ALL_KEYS.filter((k) => k.type === "major"); const currentIdx = majorKeys.findIndex((k) => k.value === selectedKey); if (currentIdx >= 0) { const prevIdx = (currentIdx - 1 + majorKeys.length) % majorKeys.length; setSelectedKey(majorKeys[prevIdx].value); } else { const minorKeys = ALL_KEYS.filter((k) => k.type === "minor"); const minorIdx = minorKeys.findIndex((k) => k.value === selectedKey); const prevIdx = (minorIdx - 1 + minorKeys.length) % minorKeys.length; setSelectedKey(minorKeys[prevIdx].value); } }; // Apply chords to lyrics and create embedded chord format const applyChordsTolLyrics = () => { if (!song?.lyrics) return song?.lyrics || ""; const lines = (song.lyrics || "").split("\n"); const chords = diatonicChords; let chordIndex = 0; const result = lines.map((line) => { const trimmedLine = line.trim(); // Skip empty lines or section headers if ( !trimmedLine || /^[\[\(]?(verse|chorus|bridge|pre-?chorus|intro|outro|interlude|hook|tag|ending|v\d|c\d|ch\d)[\d\s]*[\]\)]?:?$/i.test( trimmedLine, ) ) { return line; } // Skip if already has chords if (/\[[A-Ga-g][#b]?/.test(line)) { return line; } // Apply chords at word boundaries const words = line.split(/\s+/); const wordCount = words.length; const chordsPerLine = Math.min(4, Math.max(1, Math.floor(wordCount / 3))); const interval = Math.max(1, Math.floor(wordCount / chordsPerLine)); let chordedLine = ""; words.forEach((word, idx) => { if (idx % interval === 0 && idx < wordCount - 1) { chordedLine += `[${chords[chordIndex % chords.length]}]${word} `; chordIndex++; } else { chordedLine += word + " "; } }); return chordedLine.trim(); }); return result.join("\n"); }; // Convert pasted chord-over-lyrics format to embedded [Chord] format const convertChordsOverLyrics = (text) => { if (!text) return { lyrics: "", detectedChords: [] }; const lines = text.split("\n"); const result = []; const allChords = new Set(); // Chord pattern: line with mostly chords and spaces const chordPattern = /^[\sA-G#b/()m\d]*[A-G][#b]?(?:m|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*[\sA-G#b/()m\d]*$/; console.log("šŸŽµ Converting chords-over-lyrics format..."); for (let i = 0; i < lines.length; i++) { const currentLine = lines[i]; const nextLine = lines[i + 1]; // Check if current line is a chord line (has chords, next line is lyrics) const isChordLine = currentLine && currentLine.trim().length > 0 && chordPattern.test(currentLine.trim()) && nextLine !== undefined; if (isChordLine && nextLine) { // Extract chords with their positions const chords = []; let match; const chordRegex = /([A-G][#b]?(?:m|maj|min|dim|aug|sus|add)?[0-9]*)/g; while ((match = chordRegex.exec(currentLine)) !== null) { chords.push({ chord: match[1], position: match.index, }); allChords.add(match[1]); } // Build lyric line with embedded chords let embeddedLine = ""; let lastPos = 0; chords.forEach(({ chord, position }) => { // Get text from lyrics line up to chord position const textUpToChord = nextLine.substring(lastPos, position); embeddedLine += textUpToChord; // Add chord in brackets embeddedLine += `[${chord}]`; lastPos = position; }); // Add remaining lyrics embeddedLine += nextLine.substring(lastPos); result.push(embeddedLine); // Skip the lyrics line since we processed it i++; } else if (!isChordLine) { // Regular line (not a chord line) result.push(currentLine); } // Skip standalone chord lines that don't have lyrics below } console.log("āœ… Chord conversion complete!"); console.log("Detected chords:", Array.from(allChords)); return { lyrics: result.join("\n"), detectedChords: Array.from(allChords), }; }; // Save applied chords to database const handleApplyAndSaveChords = async () => { setSaving(true); try { // Get current lyrics - could be from editForm or song let lyricsToWork = isEditing ? editForm.lyrics || "" : song?.lyrics || ""; // Strip HTML if it's from Rich Text Editor const tempDiv = document.createElement("div"); tempDiv.innerHTML = lyricsToWork; lyricsToWork = tempDiv.textContent || tempDiv.innerText || lyricsToWork; // Check if lyrics already have chords const hasEmbeddedChords = /\[[A-Ga-g][#b]?/.test(lyricsToWork); const chordOnlyPattern = /^[\sA-G#b/()m\d]*[A-G][#b]?(?:m|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*[\sA-G#b/()m\d]*$/; const lines = lyricsToWork.split("\n"); const hasChordAboveLyrics = lines.some( (line, i) => line.trim().length > 0 && chordOnlyPattern.test(line.trim()) && lines[i + 1] !== undefined, ); let lyricsToSave = lyricsToWork; let appliedChords = []; // If lyrics don't have chords, apply the diatonic chord progression if (!hasEmbeddedChords && !hasChordAboveLyrics) { console.log( "šŸ“ Applying diatonic chord progression to plain lyrics...", ); lyricsToSave = applyChordsTolLyrics(); appliedChords = diatonicChords; toast.success(`Applied ${selectedKey} chord progression to lyrics!`); } else { // Lyrics have chords - transpose them if key changed console.log("šŸŽµ Lyrics have chords, checking if key changed..."); if (originalKey !== selectedKey) { console.log(`šŸŽ¹ Transposing from ${originalKey} to ${selectedKey}`); lyricsToSave = transposeLyricsText( lyricsToWork, originalKey, selectedKey, ); toast.success(`Transposed from ${originalKey} to ${selectedKey}!`); } else { lyricsToSave = lyricsToWork; } // Extract chords from the (possibly transposed) lyrics const chordPattern = /\[?([A-G][#b]?(?:m|maj|min|dim|aug|sus|add)?[0-9]*)\]?/g; const detectedChords = new Set(); let match; while ((match = chordPattern.exec(lyricsToSave)) !== null) { const chord = match[1]; if (chord.length <= 5) { detectedChords.add(chord); } } appliedChords = Array.from(detectedChords); } // Use the selected key const songKey = selectedKey; // Build update data const updateData = { ...song, ...(isEditing ? editForm : {}), lyrics: lyricsToSave, chords: appliedChords.length > 0 ? appliedChords.join(", ") : diatonicChords.join(", "), key_chord: songKey, }; console.log("šŸ’¾ Saving song with:", updateData); const res = await api.put(`/songs/${id}`, updateData, { timeout: 30000, // Increase timeout to 30 seconds }); if (res.data.success) { // Update song with new data const updatedSong = { ...updateData, id: id, }; // Update all state setSong(updatedSong); setOriginalKey(songKey); setSelectedKey(songKey); if (isEditing) { setEditForm(updatedSong); } toast.success( `Saved! Key: ${songKey}, ${appliedChords.length} chords applied`, ); } } catch (err) { console.error("Failed to save chords:", err); toast.error( "Failed to save changes: " + (err.response?.data?.message || err.message), ); } finally { setSaving(false); } }; // Save song edits const handleSave = async () => { setSaving(true); try { // Also update the key_chord field with selected key const updateData = { ...editForm, chords: diatonicChords.join(", "), key_chord: selectedKey, }; const res = await api.put(`/songs/${id}`, updateData); if (res.data.success) { setSong({ ...editForm, key_chord: selectedKey, chords: diatonicChords.join(", "), }); setOriginalKey(selectedKey); setIsEditing(false); toast.success("Song saved successfully!"); } } catch (err) { console.error("Failed to save:", err); toast.error("Failed to save song"); } finally { setSaving(false); } }; // Delete song const handleDelete = async () => { setShowDeleteModal(false); try { const res = await api.delete(`/songs/${id}`); if (res.data.success) { toast.success("Song deleted successfully!"); // Navigate to database page after successful deletion setTimeout(() => navigate("/database"), 1000); } else { toast.error( "Failed to delete song: " + (res.data.message || "Unknown error"), ); } } catch (err) { console.error("Failed to delete song:", err); toast.error( "Failed to delete song: " + (err.response?.data?.message || err.message), ); } }; // Click-to-place chord on a word const handleWordClick = (lineIdx, wordIdx, word) => { if (!chordEditMode || !selectedChordToPlace) return; // Get current lyrics const currentLyrics = editableLyrics || song?.lyrics || ""; const lines = currentLyrics.split("\n"); if (lineIdx >= lines.length) return; const line = lines[lineIdx]; const words = line.split(/(\s+)/); // Find the actual word position (accounting for spaces) let actualWordIdx = 0; let targetIdx = -1; for (let i = 0; i < words.length; i++) { if (words[i].trim()) { if (actualWordIdx === wordIdx) { targetIdx = i; break; } actualWordIdx++; } } if (targetIdx === -1) return; // Remove any existing chord from this word const cleanWord = words[targetIdx].replace(/^\[[^\]]+\]/, ""); // Add the new chord words[targetIdx] = `[${selectedChordToPlace}]${cleanWord}`; // Rebuild the line lines[lineIdx] = words.join(""); // Update state const newLyrics = lines.join("\n"); setEditableLyrics(newLyrics); setEditForm({ ...editForm, lyrics: newLyrics }); }; // Remove chord from a word const handleRemoveChord = (lineIdx, wordIdx) => { const currentLyrics = editableLyrics || song?.lyrics || ""; const lines = currentLyrics.split("\n"); if (lineIdx >= lines.length) return; const line = lines[lineIdx]; const words = line.split(/(\s+)/); let actualWordIdx = 0; let targetIdx = -1; for (let i = 0; i < words.length; i++) { if (words[i].trim()) { if (actualWordIdx === wordIdx) { targetIdx = i; break; } actualWordIdx++; } } if (targetIdx === -1) return; // Remove chord from this word words[targetIdx] = words[targetIdx].replace(/^\[[^\]]+\]/, ""); lines[lineIdx] = words.join(""); const newLyrics = lines.join("\n"); setEditableLyrics(newLyrics); setEditForm({ ...editForm, lyrics: newLyrics }); }; // Initialize editable lyrics when entering edit mode const enterEditMode = () => { setIsEditing(true); setEditableLyrics(song?.lyrics || ""); setEditForm({ ...song }); setChordEditMode(false); setShowTextEditor(true); // Default to text editor setShowRichTextEditor(false); }; // Section header detection const isSectionHeader = (text) => { const trimmed = text.trim().toLowerCase(); return /^[\[\(]?(verse|chorus|bridge|pre-?chorus|intro|outro|interlude|hook|tag|ending|v\d|c\d|ch\d)[\d\s]*[\]\)]?:?$/i.test( trimmed, ); }; // Parse lyrics for click-to-place editor const parsedEditableLyrics = useMemo(() => { const lyrics = editableLyrics || song?.lyrics || ""; return lyrics.split("\n").map((line, lineIdx) => { const words = []; const regex = /(\[[^\]]+\])?(\S+)/g; let match; let wordIdx = 0; while ((match = regex.exec(line)) !== null) { const chord = match[1] ? match[1].slice(1, -1) : null; const word = match[2]; words.push({ chord, word, wordIdx: wordIdx++ }); } return { lineIdx, words, text: line, isHeader: isSectionHeader(line), isEmpty: !line.trim(), }; }); }, [editableLyrics, song?.lyrics]); // Theme classes const textPrimary = isDark ? "text-white" : "text-gray-900"; const textSecondary = isDark ? "text-white/70" : "text-gray-600"; const textMuted = isDark ? "text-white/50" : "text-gray-500"; const bgCard = isDark ? "bg-slate-800/80" : "bg-white"; const borderColor = isDark ? "border-white/10" : "border-gray-200"; if (loading) { return (

Loading song...

); } if (!song) { return (

Song not found

); } return (
{/* Floating Navigation */} {showNav && (
{song.title}
)}
{/* Header */}

{song.title}

{(song.artist || song.singer) && ( {song.artist || song.singer} )} {song.band && ( • {song.band} )}
{/* Actions */}
{isEditing ? ( <> ) : ( <> )}
{/* Key Selector & Controls */}
{/* Key Selection */}
Key: {/* Original Key Display */}
Original: {originalKey}
{/* Key Selector Dropdown - ALL KEYS */}
{showKeySelector && ( {/* Major Keys Section */}
Major Keys
{ALL_KEYS.filter((k) => k.type === "major").map((key) => ( ))} {/* Minor Keys Section */}
Minor Keys
{ALL_KEYS.filter((k) => k.type === "minor").map((key) => ( ))}
)}
{/* Quick Transpose Buttons */}
{/* Semitones indicator */} {semitonesDiff !== 0 && ( {semitonesDiff > 0 ? `+${semitonesDiff}` : semitonesDiff}{" "} semitones )}
{/* Show Chords Toggle & Font Size Controls */}
{/* Show/Hide Chords Button */} {/* Apply & Save Chords Button */} {/* Font Size Controls */}
{fontSize}
{/* Diatonic Chord Progression for Selected Key */}
Chords in {selectedKey}:
{diatonicChords.map((chord, idx) => ( {chord} ))}
{/* Lyrics Display with Chords ABOVE */}
{isEditing ? ( /* Edit Mode with Chord Palette */
setEditForm({ ...editForm, title: e.target.value }) } className={`w-full px-4 py-2 rounded-lg border ${isDark ? "bg-white/5 border-white/10 text-white" : "bg-white border-gray-200 text-gray-900"}`} />
setEditForm({ ...editForm, artist: e.target.value }) } className={`w-full px-4 py-2 rounded-lg border ${isDark ? "bg-white/5 border-white/10 text-white" : "bg-white border-gray-200 text-gray-900"}`} />
{/* Chord Palette - Click to insert at cursor */}
Click chord to insert at cursor position:
{/* Diatonic Chords for Current Key */}
Key of {selectedKey} (Diatonic):
{diatonicChords.map((chord, idx) => ( ))}
{/* Common Chord Extensions */}
Extensions (add to root):
{[ "7", "maj7", "m7", "dim", "aug", "sus2", "sus4", "add9", "9", "11", "13", ].map((ext) => ( ))}
{/* All Root Notes */}
All Roots:
{[ "C", "C#", "Db", "D", "D#", "Eb", "E", "F", "F#", "Gb", "G", "G#", "Ab", "A", "A#", "Bb", "B", ].map((root) => ( ))} {/* Minor versions */} {["Cm", "Dm", "Em", "Fm", "Gm", "Am", "Bm"].map((root) => ( ))}
{/* Rich Text Editor Only - Same as Create Page */} setEditForm({ ...editForm, lyrics })} placeholder="Paste lyrics with chords on line above - they'll be auto-detected!" />
) : ( /* Display Mode - Chords ABOVE lyrics */
{lyricsWithAppliedChords.map((line, lineIdx) => { // Section header if (line.isHeader) { return (
{line.text}
); } // Empty line if (line.isEmpty) { return
; } return (
{/* CHORD LINE - displayed ABOVE lyrics when showChords is ON */} {showChords && line.chordLine && (
{line.chordLine}
)} {/* If line has embedded chord segments, render those */} {showChords && line.segments && (
{line.segments.map((seg, i) => ( {seg.chord || ""} ))}
)} {/* LYRICS LINE - always shown */}
{line.segments ? line.segments.map((seg, i) => ( {seg.text} )) : line.text}
); })}
)}
{/* Song Navigation Footer */}
{currentIndex + 1} of {songs.length}
{/* Click outside to close key selector */} {showKeySelector && (
setShowKeySelector(false)} /> )} {/* Delete Confirmation Modal */} {showDeleteModal && (

Delete Song

This action cannot be undone

Are you sure you want to delete "{song?.title}" ?

)}
); }