import { useState, useEffect } from "react";
// ─── Design Tokens ───────────────────────────────────────────────────────────
// Palette: near-black base, warm parchment text, gold accent (vinyl warmth),
// with a deep green Spotify nod used sparingly
const T = {
bg: "#0A0A0A",
surface: "#141414",
surfaceHover: "#1C1C1C",
border: "#242424",
borderActive: "#C8A951",
accent: "#C8A951",
accentDim: "#8A7234",
accentGlow: "rgba(200,169,81,0.15)",
spotifyGreen: "#1DB954",
textPrimary: "#F0EDE6",
textMuted: "#666",
textDim: "#444",
error: "#E05252",
success: "#4DAF7C",
};
const STEPS = [
{ id: 1, label: "Track", icon: "♬" },
{ id: 2, label: "Sound", icon: "◈" },
{ id: 3, label: "Playlists", icon: "☰" },
{ id: 4, label: "Pitch", icon: "✎" },
{ id: 5, label: "Submit", icon: "✦" },
];
const GENRES = ["Hip-Hop","R&B","Afrobeats","Pop","Electronic","Indie","Soul","Reggae","Gospel","Jazz","Lo-Fi","Dancehall","Amapiano","Drill","Alternative"];
const MOODS = ["Energetic","Chill","Emotional","Hype","Late Night","Focus","Romantic","Melancholic","Uplifting","Dark","Party","Spiritual"];
const PLAYLISTS = [
{ id: 1, curator: "Fresh Vibes Daily", genre: "Afrobeats · R&B", followers: "48.2K", slots: 3, img: "🎵" },
{ id: 2, curator: "Night Drive Collective", genre: "Lo-Fi · Electronic", followers: "122K", slots: 2, img: "🌙" },
{ id: 3, curator: "The Heat Check", genre: "Hip-Hop · Drill", followers: "89.4K", slots: 5, img: "🔥" },
{ id: 4, curator: "Soul Kitchen", genre: "Soul · R&B · Jazz", followers: "34.7K", slots: 4, img: "🎷" },
{ id: 5, curator: "Gospel Waves", genre: "Gospel · Worship", followers: "61K", slots: 2, img: "🙌" },
{ id: 6, curator: "Amapiano District", genre: "Amapiano · House", followers: "97.8K", slots: 6, img: "💿" },
];
// ─── Helpers ─────────────────────────────────────────────────────────────────
function isValidSpotifyUrl(url) {
return /open\.spotify\.com\/(track|embed\/track)\/[A-Za-z0-9]+/.test(url);
}
// ─── Sub-components ──────────────────────────────────────────────────────────
function ProgressBar({ step }) {
return (
{STEPS.map((s, i) => (
s.id ? T.accent : step === s.id ? T.accentGlow : "transparent",
border: `2px solid ${step >= s.id ? T.accent : T.border}`,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: step > s.id ? 14 : 16,
color: step > s.id ? "#000" : step === s.id ? T.accent : T.textDim,
fontWeight: 700,
transition: "all 0.3s ease",
boxShadow: step === s.id ? `0 0 16px ${T.accentGlow}` : "none",
}}>
{step > s.id ? "✓" : s.icon}
= s.id ? T.accent : T.textDim,
fontFamily: "monospace", fontWeight: step === s.id ? 700 : 400,
}}>{s.label}
{i < STEPS.length - 1 && (
s.id
? `linear-gradient(90deg, ${T.accent}, ${T.accentDim})`
: T.border,
transition: "background 0.4s ease",
}} />
)}
))}
);
}
function StepLabel({ number, title, subtitle }) {
return (
STEP {number} / {STEPS.length}
{title}
{subtitle &&
{subtitle}
}
);
}
function Input({ label, placeholder, value, onChange, type = "text", error }) {
const [focused, setFocused] = useState(false);
return (
{label}
onChange(e.target.value)}
placeholder={placeholder}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
style={{
width: "100%", padding: "13px 16px", boxSizing: "border-box",
background: T.surface,
border: `1px solid ${error ? T.error : focused ? T.accent : T.border}`,
borderRadius: 8, color: T.textPrimary, fontSize: 15,
outline: "none", fontFamily: "inherit",
boxShadow: focused ? `0 0 0 3px ${T.accentGlow}` : "none",
transition: "all 0.2s ease",
}}
/>
{error &&
{error}
}
);
}
function Textarea({ label, placeholder, value, onChange, maxLength = 500 }) {
const [focused, setFocused] = useState(false);
return (
);
}
function TagPicker({ label, options, selected, onToggle, max = 3 }) {
return (
{label} — pick up to {max}
{options.map(opt => {
const active = selected.includes(opt);
const disabled = !active && selected.length >= max;
return (
!disabled && onToggle(opt)}
style={{
padding: "7px 14px", borderRadius: 20,
border: `1px solid ${active ? T.accent : T.border}`,
background: active ? T.accentGlow : "transparent",
color: active ? T.accent : disabled ? T.textDim : T.textMuted,
fontSize: 13, cursor: disabled ? "not-allowed" : "pointer",
fontFamily: "inherit", transition: "all 0.15s ease",
opacity: disabled ? 0.5 : 1,
}}
>{opt}
);
})}
);
}
function PlaylistCard({ playlist, selected, onToggle }) {
return (
onToggle(playlist.id)}
style={{
padding: "16px", borderRadius: 10,
border: `1px solid ${selected ? T.accent : T.border}`,
background: selected ? T.accentGlow : T.surface,
cursor: "pointer", transition: "all 0.2s ease",
display: "flex", gap: 14, alignItems: "flex-start",
boxShadow: selected ? `0 0 16px ${T.accentGlow}` : "none",
}}
>
{playlist.img}
{playlist.curator}
{playlist.genre}
♫ {playlist.followers} followers
{playlist.slots} open slots
{selected && ✓ }
);
}
function ReviewRow({ label, value }) {
return (
{label}
{value || "—"}
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export default function CuratorackPortal() {
const [step, setStep] = useState(1);
const [submitted, setSubmitted] = useState(false);
const [errors, setErrors] = useState({});
const [form, setForm] = useState({
// Step 1
spotifyUrl: "",
trackName: "",
artistName: "",
// Step 2
genres: [],
moods: [],
explicit: false,
language: "English",
// Step 3
selectedPlaylists: [],
// Step 4
pitch: "",
instagram: "",
pressLink: "",
agreed: false,
});
const set = (key, val) => setForm(f => ({ ...f, [key]: val }));
const toggleArr = (key, val) => setForm(f => {
const arr = f[key];
return { ...f, [key]: arr.includes(val) ? arr.filter(x => x !== val) : [...arr, val] };
});
const validate = () => {
const e = {};
if (step === 1) {
if (!form.spotifyUrl) e.spotifyUrl = "Paste your Spotify track link.";
else if (!isValidSpotifyUrl(form.spotifyUrl)) e.spotifyUrl = "That doesn't look like a valid Spotify track URL.";
if (!form.trackName.trim()) e.trackName = "Track name is required.";
if (!form.artistName.trim()) e.artistName = "Artist name is required.";
}
if (step === 2) {
if (form.genres.length === 0) e.genres = "Pick at least one genre.";
if (form.moods.length === 0) e.moods = "Pick at least one mood.";
}
if (step === 3) {
if (form.selectedPlaylists.length === 0) e.playlists = "Select at least one playlist.";
}
if (step === 4) {
if (form.pitch.trim().length < 30) e.pitch = "Your pitch needs to be at least 30 characters.";
if (!form.agreed) e.agreed = "You must agree to the submission terms.";
}
setErrors(e);
return Object.keys(e).length === 0;
};
const next = () => {
if (validate()) {
if (step === 5) {
setSubmitted(true);
} else {
setStep(s => s + 1);
window.scrollTo(0, 0);
}
}
};
const back = () => { setStep(s => s - 1); setErrors({}); };
const selectedPlaylistNames = PLAYLISTS
.filter(p => form.selectedPlaylists.includes(p.id))
.map(p => p.curator)
.join(", ");
// ── SUBMITTED STATE ──────────────────────────────────────────────────────
if (submitted) {
return (
✓
Submission Received
"{form.trackName}" by {form.artistName} is now in the queue. Curators typically review within 5–7 business days .
Submitted to
{selectedPlaylistNames}
Track verified on Spotify ✓
{ setStep(1); setSubmitted(false); setForm({ spotifyUrl:"",trackName:"",artistName:"",genres:[],moods:[],explicit:false,language:"English",selectedPlaylists:[],pitch:"",instagram:"",pressLink:"",agreed:false }); }}
style={{
padding: "12px 28px", borderRadius: 8, border: `1px solid ${T.border}`,
background: "transparent", color: T.textMuted, cursor: "pointer",
fontFamily: "inherit", fontSize: 14,
}}
>Submit Another Track
);
}
// ── PORTAL LAYOUT ────────────────────────────────────────────────────────
return (
{/* Top bar */}
⬡
Curatorack
PLAYLIST SUBMISSION
{/* Content */}
{/* ── STEP 1: Track Details ─────────────────────────── */}
{step === 1 && (
)}
{/* ── STEP 2: Sound Profile ─────────────────────────── */}
{step === 2 && (
toggleArr("genres", v)} max={3} />
{errors.genres && {errors.genres}
}
toggleArr("moods", v)} max={3} />
{errors.moods && {errors.moods}
}
Language
set("language", e.target.value)} style={{
padding: "13px 16px", background: T.surface, border: `1px solid ${T.border}`,
borderRadius: 8, color: T.textPrimary, fontSize: 15, outline: "none",
fontFamily: "inherit", width: "100%", cursor: "pointer",
}}>
{["English","Luganda","Swahili","French","Spanish","Portuguese","Arabic","Other"].map(l => (
{l}
))}
set("explicit", !form.explicit)}>
{form.explicit && ✓ }
Track contains explicit content
)}
{/* ── STEP 3: Select Playlists ──────────────────────── */}
{step === 3 && (
{errors.playlists && (
{errors.playlists}
)}
{PLAYLISTS.map(p => (
toggleArr("selectedPlaylists", id)}
/>
))}
{form.selectedPlaylists.length > 0 && (
{form.selectedPlaylists.length} playlist{form.selectedPlaylists.length > 1 ? "s" : ""} selected
)}
)}
{/* ── STEP 4: Pitch ─────────────────────────────────── */}
{step === 4 && (
)}
{/* ── STEP 5: Review & Confirm ──────────────────────── */}
{step === 5 && (
40 ? form.spotifyUrl.slice(0, 40) + "..." : form.spotifyUrl} />
80 ? form.pitch.slice(0, 80) + "..." : form.pitch} />
{form.instagram && }
{form.pressLink && 40 ? form.pressLink.slice(0, 40) + "..." : form.pressLink} />}
One final check: {" "}
Your track is being submitted to {form.selectedPlaylists.length} playlist{form.selectedPlaylists.length !== 1 ? "s" : ""} . Curators will review within 5–7 business days and you'll hear back by email.
)}
{/* ── Navigation ──────────────────────────────────────── */}
{step > 1 && (
← Back
)}
{step === 5 ? "Confirm & Submit →" : step === 4 ? "Review Submission →" : "Next →"}
{/* Google Fonts */}
);
}
now
now