Home.tsx (6204B)
1 import { useCallback, useState } from "react"; 2 import { useSearchParams } from "react-router-dom"; 3 4 import { CozyAudioBar } from "../components/CozyAudioBar"; 5 import { EmbedSnippet } from "../components/EmbedSnippet"; 6 import { PlayerAttribution } from "../components/PlayerAttribution"; 7 import { PlayerStatus } from "../components/PlayerStatus"; 8 import { SiteHeader } from "../components/SiteHeader"; 9 import { TrackSearch } from "../components/TrackSearch"; 10 import { PUBLIC_SITE_URL } from "../config/siteUrl"; 11 import { useMyMusicsPlayback } from "../hooks/useMyMusicsPlayback"; 12 import { usePlayerKeyboard } from "../hooks/usePlayerKeyboard"; 13 import "../App.css"; 14 15 export default function Home() { 16 const [searchParams] = useSearchParams(); 17 const startTrackId = searchParams.get("track")?.trim() || null; 18 const [linkCopied, setLinkCopied] = useState(false); 19 20 const { 21 audioRef, 22 preloadAudioRef, 23 track, 24 status, 25 playbackPhase, 26 upNext, 27 history, 28 autoPlay, 29 setAutoPlay, 30 healthWarn, 31 queueBusy, 32 requestNextTrack, 33 loadTrackById, 34 handleAudioPlaying, 35 handleAudioError, 36 handleAudioPause, 37 onEnded, 38 showUpNextHint, 39 } = useMyMusicsPlayback({ 40 startTrackId, 41 autoplayOnMount: true, 42 }); 43 44 usePlayerKeyboard({ audioRef, enabled: true, onNext: requestNextTrack }); 45 46 const copyShareLink = useCallback(async () => { 47 if (!track) return; 48 const url = `${PUBLIC_SITE_URL}/?track=${encodeURIComponent(track.id)}`; 49 try { 50 await navigator.clipboard.writeText(url); 51 setLinkCopied(true); 52 window.setTimeout(() => setLinkCopied(false), 2000); 53 } catch { 54 setLinkCopied(false); 55 } 56 }, [track]); 57 58 return ( 59 <div className="page"> 60 {healthWarn ? ( 61 <div className="health-banner" role="alert"> 62 <strong>Server metadata</strong> 63 <p>{healthWarn}</p> 64 <p className="health-banner-hint"> 65 On the host, run <code>curl -sS http://127.0.0.1:38471/api/health</code> (adjust 66 port) and fix <code>METADATA_TSV</code> or run <code>npm run index-metadata</code>. 67 </p> 68 </div> 69 ) : null} 70 <SiteHeader nav="home" /> 71 72 <main className="main main-home"> 73 <div className="main-sidebar"> 74 <TrackSearch onSelect={(id) => void loadTrackById(id)} disabled={!!healthWarn} /> 75 76 <aside className="card history" aria-label="Recently played"> 77 <h2>History</h2> 78 <ol className="history-list"> 79 {history.map((t, idx) => ( 80 <li key={`${t.id}-${idx}-${t.title}`}> 81 <button 82 type="button" 83 className="history-hit" 84 onClick={() => void loadTrackById(t.id)} 85 > 86 <span className="h-artist">{t.artist}</span> 87 <span className="sep">—</span> 88 <span className="h-title">{t.title}</span> 89 </button> 90 </li> 91 ))} 92 </ol> 93 </aside> 94 </div> 95 96 <article className="card now-playing"> 97 <header className="card-head"> 98 <h2>Now playing</h2> 99 {track ? ( 100 <button type="button" className="btn btn-share" onClick={() => void copyShareLink()}> 101 {linkCopied ? "Link copied!" : "Copy link"} 102 </button> 103 ) : null} 104 </header> 105 {track ? ( 106 <div className="track-block"> 107 <p className="artist">{track.artist}</p> 108 <p className="title">{track.title}</p> 109 </div> 110 ) : ( 111 <p className="muted">{status || "No track loaded."}</p> 112 )} 113 114 <section className="up-next" aria-label="Up next"> 115 <h3 className="up-next-label">Up next</h3> 116 {upNext ? ( 117 <> 118 <p className="up-next-track"> 119 <span className="up-next-artist">{upNext.artist}</span> 120 <span className="up-next-sep"> — </span> 121 <span className="up-next-title">{upNext.title}</span> 122 </p> 123 {showUpNextHint ? ( 124 <p className="up-next-note muted">Only one track in the pool — it will repeat.</p> 125 ) : null} 126 </> 127 ) : queueBusy ? ( 128 <p className="up-next-empty muted">Queuing…</p> 129 ) : track ? ( 130 <p className="up-next-empty muted">—</p> 131 ) : ( 132 <p className="up-next-empty muted">Queuing…</p> 133 )} 134 </section> 135 136 <div className="player-nook"> 137 <audio 138 ref={audioRef} 139 className="audio-hidden" 140 preload="metadata" 141 tabIndex={-1} 142 aria-hidden="true" 143 onEnded={onEnded} 144 onPlaying={handleAudioPlaying} 145 onPause={handleAudioPause} 146 onError={handleAudioError} 147 /> 148 <audio ref={preloadAudioRef} className="audio-hidden" preload="auto" aria-hidden="true" /> 149 <CozyAudioBar audioRef={audioRef} disabled={!track} /> 150 <PlayerStatus phase={playbackPhase} status={status} hasTrack={!!track} /> 151 152 <div className="actions"> 153 <button type="button" className="btn primary" onClick={() => void requestNextTrack()}> 154 Next 155 </button> 156 <label className="check"> 157 <input 158 type="checkbox" 159 checked={autoPlay} 160 onChange={(e) => setAutoPlay(e.target.checked)} 161 /> 162 Auto-advance when track ends 163 </label> 164 </div> 165 <p className="player-keys-hint muted"> 166 Shortcuts: Space play/pause, N next, M mute 167 </p> 168 </div> 169 <PlayerAttribution /> 170 </article> 171 172 <EmbedSnippet className="main-embed" /> 173 </main> 174 175 <footer className="footer"> 176 <small className="muted">Developed by Pablo Murad — 2026</small> 177 </footer> 178 </div> 179 ); 180 }