// EsploraBeat — Beat catalog tab
// Owns: BeatFilterRail, BeatGrid, BeatCard, Waveform, StickyPlayer, BuyModal
// Sort options + active filter pills are owned by parent (Esplora.jsx); beat catalog
// just renders its rail + grid + active player.
const BeatIcons = window.SonaraIcons;
const { useState: useStateBeat, useMemo: useMemoBeat } = React;
// Reusable Geo filter for the rail (free-text location + quick chips + "near me" radius)
function BeatGeoFilter() {
const [mode, setMode] = useStateBeat("any"); // "any" | "near" | "country" | "region" | "custom"
const [query, setQuery] = useStateBeat("");
const [radius, setRadius] = useStateBeat(50);
const chips = [
{ id: "any", label: "Ovunque" },
{ id: "near", label: "Vicino a me", icon: true },
{ id: "country", label: "Italia" },
];
return (
{ setQuery(e.target.value); setMode("custom"); }}
/>
{query && (
)}
{chips.map(c => (
))}
{mode === "near" && (
)}
);
}
// ---------- mock data ----------
const BEAT_GENRES = ["Trap", "Drill", "Hip-Hop", "R&B", "Afro", "Pop", "House", "Reggaeton", "UK Garage"];
const BEAT_MOODS = ["Dark", "Hype", "Chill", "Emo", "Aggressive", "Romantic", "Dreamy", "Bouncy"];
const BEAT_KEYS = ["A min", "A maj", "B min", "C# min", "D min", "E min", "F maj", "F# min", "G min", "G# min"];
const BEAT_PRODS = ["Yoshi", "Marco V.", "DK Beats", "Riva", "Nemo", "Kyo", "Astra", "Frey", "Lume", "Tide"];
function beatRand(seed) { let s = seed; return () => (s = (s * 9301 + 49297) % 233280) / 233280; }
function makeBeats() {
const r = beatRand(7);
const titles = [
"Notte Lunga","Asfalto","Skyline","Vetro Rotto","Mare Nero","Polvere d'Oro","Riflesso","Ferro Caldo",
"Anima Lenta","Frequenza 88","Capo Bianco","Vento Sud","Eco","Tempesta","Velluto","Magma","Onda Lunga",
"Carbone","Vela","Solstizio","Treno Notturno","Luce Bassa","Pioggia 4am","Nebbia Soft"
];
return titles.map((title, i) => {
const bpm = 80 + Math.floor(r() * 80);
const exclusive = r() > 0.55;
const lease = [29, 39, 49, 59, 79][Math.floor(r() * 5)];
const exclusivePrice = lease * (8 + Math.floor(r() * 16));
return {
id: i + 1, title,
producer: BEAT_PRODS[i % BEAT_PRODS.length],
verified: r() > 0.45,
genre: BEAT_GENRES[i % BEAT_GENRES.length],
mood: BEAT_MOODS[i % BEAT_MOODS.length],
bpm, key: BEAT_KEYS[i % BEAT_KEYS.length],
duration: `${Math.floor(2 + r() * 2)}:${String(Math.floor(r() * 60)).padStart(2, "0")}`,
lease, exclusive, exclusivePrice,
plays: Math.floor(800 + r() * 12000),
art: i % 6,
stems: r() > 0.5,
peaks: Array.from({ length: 64 }, (_, k) => {
const base = 0.25 + 0.5 * Math.abs(Math.sin(i * 0.7 + k * 0.31));
return Math.min(1, base + (r() - 0.5) * 0.3);
}),
};
});
}
const ALL_BEATS = makeBeats();
// ---------- waveform ----------
function Waveform({ peaks, progress = 0, playing = false, compact = false }) {
const h = compact ? 28 : 44;
return (
{peaks.map((p, i) => (
))}
);
}
// ---------- filter rail (Beat-specific) ----------
function BeatFilterRail() {
const [genre, setGenre] = useStateBeat("Tutti");
const [mood, setMood] = useStateBeat("Tutti");
const [bpmMin, setBpmMin] = useStateBeat(60);
const [bpmMax, setBpmMax] = useStateBeat(180);
const [licenseType, setLicenseType] = useStateBeat("any");
const [stemsOnly, setStemsOnly] = useStateBeat(false);
return (
);
}
// ---------- beat card ----------
function BeatCard({ beat, isPlaying, onPlay, onLike, onBuy, hero, liked }) {
const arts = ["art-a","art-b","art-c","art-d","art-e","art-f"];
return (
{beat.duration}
{beat.exclusive &&
Esclusiva disp.}
{beat.bpm} BPM
{beat.producer.charAt(0)}
{beat.producer}
{beat.verified && }
{beat.genre}
{beat.mood}
{beat.key}
{beat.stems && + Stems}
€{beat.lease}licenza
{beat.exclusive && (
€{beat.exclusivePrice}esclusiva
)}
);
}
// ---------- BuyModal ----------
function BuyModal({ beat, onClose }) {
const [tier, setTier] = useStateBeat("lease");
const tiers = [
{ id: "lease", t: "Licenza non-esclusiva", p: beat.lease,
desc: "Stream illimitato, monetizzazione fino a 50k stream. MP3 + WAV.",
includes: ["MP3 + WAV", "Streaming + monetizzazione", "Crediti producer obbligatori"] },
{ id: "lease-plus", t: "Lease + Stems", p: beat.lease + 30,
desc: "Tutto della licenza, più gli stems separati per mix custom.",
includes: ["MP3 + WAV + Stems separati", "Streaming + monetizzazione", "Mixaggio personalizzabile"] },
{ id: "excl", t: "Esclusiva", p: beat.exclusivePrice,
desc: "Diritti esclusivi. Il beat viene rimosso dal catalogo entro 24h. Solo 18+.",
includes: ["Tutti i file + stems", "Diritti esclusivi a vita", "Contratto firmato digitalmente", "Rimozione catalogo entro 24h"],
flag: "Solo 18+" },
];
const sel = tiers.find(t => t.id === tier);
return (
e.stopPropagation()}>
Acquisto licenza
{beat.title}
di {beat.producer}
{beat.verified && }
{beat.bpm} BPM · {beat.key} · {beat.genre}
Scegli la licenza
{tiers.map(t => (
))}
Cosa ricevi
{sel.includes.map(it => (
- {it}
))}
{tier === "excl" && (
Contratto digitale richiesto
Allo step successivo firmerai il contratto di cessione esclusiva (SES). Verifica età 18+ richiesta.
)}
Metodo
);
}
// ---------- StickyPlayer ----------
function StickyPlayer({ beat, playing, onToggle, onClose, style = "expanded" }) {
if (!beat) return null;
const arts = ["art-a","art-b","art-c","art-d","art-e","art-f"];
return (
{beat.title}
{beat.producer} · {beat.bpm} BPM · {beat.key}
{style === "expanded" && (
1:14
{beat.duration}
)}
);
}
// ---------- BeatCatalog ----------
function BeatCatalog({ playerStyle = "expanded", cardStyle = "standard" }) {
const [playing, setPlaying] = useStateBeat({ id: null, on: false });
const [liked, setLiked] = useStateBeat(new Set([3, 7, 12]));
const [buying, setBuying] = useStateBeat(null);
const playBeat = (b) => setPlaying(p => p.id === b.id ? { id: b.id, on: !p.on } : { id: b.id, on: true });
const toggleLike = (id) => setLiked(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
const beats = ALL_BEATS;
const playingBeat = beats.find(b => b.id === playing.id);
return (
<>
{beats.map(b => (
playBeat(b)}
onLike={() => toggleLike(b.id)}
onBuy={() => setBuying(b)}
/>
))}
{playingBeat && (
setPlaying(p => ({ ...p, on: !p.on }))}
onClose={() => setPlaying({ id: null, on: false })}
style={playerStyle}
/>
)}
{buying && setBuying(null)}/>}
>
);
}
window.BeatCatalog = BeatCatalog;
window.ALL_BEATS = ALL_BEATS;
window.BEAT_COUNT = ALL_BEATS.length;