mymusics

retro MySpace-style music player
Log | Files | Refs | README

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 }