import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { useAuth } from "../context/authContext"; interface Contributor { username: string; fullName: string; role: string; } interface ArtistSearchResult { username: string; fullName: string; profilePhoto?: string; } interface SongEntry { title: string; link: string; contributors: Contributor[]; } const ROLE_OPTIONS = [ { value: "FEATURED", label: "Featured" }, { value: "PRODUCER", label: "Producer" }, { value: "SONGWRITER", label: "Songwriter" }, { value: "COMPOSER", label: "Composer" }, { value: "MIXER", label: "Mixer" }, { value: "ENGINEER", label: "Engineer" }, ]; const GENRE_OPTIONS = [ "Pop", "Rock", "Hip-Hop", "R&B", "Electronic", "Jazz", "Classical", "Country", "Folk", "Latin", "Metal", "Punk", "Indie", "Alternative", "Blues", "Soul", "Reggae", "Funk", "Disco", "House", "Techno", "Ambient", "Other", ]; // Mock artist search results const MOCK_ARTISTS: ArtistSearchResult[] = [ { username: "john_doe", fullName: "John Doe" }, { username: "jane_smith", fullName: "Jane Smith" }, { username: "mike_producer", fullName: "Mike the Producer" }, { username: "sarah_vocals", fullName: "Sarah Vocals" }, { username: "alex_beats", fullName: "Alex Beats" }, ]; const MAX_CONTRIBUTORS = 5; const MAX_ALBUM_SONGS = 20; const PublishSong = () => { const { user } = useAuth(); const navigate = useNavigate(); // Form state const [releaseType, setReleaseType] = useState<"single" | "album">("single"); const [title, setTitle] = useState(""); const [genre, setGenre] = useState(""); const [link, setLink] = useState(""); const [_coverFile, setCoverFile] = useState(null); // TODO: Use in API call const [coverPreview, setCoverPreview] = useState(null); const [contributors, setContributors] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); // Album-specific state const [albumSongs, setAlbumSongs] = useState([ { title: "", link: "", contributors: [] }, ]); // Contributor search state const [contributorSearch, setContributorSearch] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showSearchDropdown, setShowSearchDropdown] = useState(false); const [selectedContributorRole, setSelectedContributorRole] = useState( ROLE_OPTIONS[0].value, ); const searchDropdownRef = useRef(null); const searchTimeoutRef = useRef | null>(null); // Album song contributor search state const [albumSongContributorSearch, setAlbumSongContributorSearch] = useState<{ [key: number]: string; }>({}); const [albumSongSearchResults, setAlbumSongSearchResults] = useState<{ [key: number]: ArtistSearchResult[]; }>({}); const [albumSongShowDropdown, setAlbumSongShowDropdown] = useState<{ [key: number]: boolean; }>({}); const [albumSongSelectedRole, setAlbumSongSelectedRole] = useState<{ [key: number]: string; }>({}); // Cleanup cover preview URL on unmount useEffect(() => { return () => { if (coverPreview) { URL.revokeObjectURL(coverPreview); } }; }, [coverPreview]); // Click outside handler for search dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( searchDropdownRef.current && !searchDropdownRef.current.contains(event.target as Node) ) { setShowSearchDropdown(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Search for artists (mock implementation) const searchArtists = useCallback(async (query: string) => { if (!query.trim()) { setSearchResults([]); return; } setIsSearching(true); try { // TODO: replace with api call // const response = await axiosInstance.get(`/users/search?type=ARTIST&q=${encodeURIComponent(query)}`); // setSearchResults(response.data); // Mock search - filter by name await new Promise((resolve) => setTimeout(resolve, 200)); const filtered = MOCK_ARTISTS.filter( (artist) => artist.fullName.toLowerCase().includes(query.toLowerCase()) || artist.username.toLowerCase().includes(query.toLowerCase()), ); setSearchResults(filtered); } catch (error) { console.error("Error searching artists:", error); setSearchResults([]); } finally { setIsSearching(false); } }, []); // Debounced search const handleContributorSearchChange = (value: string) => { setContributorSearch(value); setShowSearchDropdown(true); if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } searchTimeoutRef.current = setTimeout(() => { searchArtists(value); }, 300); }; // Add contributor const handleAddContributor = (artist: ArtistSearchResult) => { if (contributors.length >= MAX_CONTRIBUTORS) { toast.error(`Maximum ${MAX_CONTRIBUTORS} contributors allowed`); return; } if (contributors.some((c) => c.username === artist.username)) { toast.error("This contributor is already added"); return; } setContributors([ ...contributors, { username: artist.username, fullName: artist.fullName, role: selectedContributorRole, }, ]); setContributorSearch(""); setSearchResults([]); setShowSearchDropdown(false); }; // Remove contributor const handleRemoveContributor = (username: string) => { setContributors(contributors.filter((c) => c.username !== username)); }; // Handle cover file const handleCoverChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { if (file.size > 2 * 1024 * 1024) { toast.error("Max file size is 5MB"); return; } if (!file.type.startsWith("image/")) { toast.error("Only images allowed"); return; } if (coverPreview) { URL.revokeObjectURL(coverPreview); } setCoverFile(file); setCoverPreview(URL.createObjectURL(file)); } }; const handleRemoveCover = () => { if (coverPreview) { URL.revokeObjectURL(coverPreview); } setCoverFile(null); setCoverPreview(null); }; // Album song handlers const handleAddAlbumSong = () => { if (albumSongs.length >= MAX_ALBUM_SONGS) { toast.error(`Maximum ${MAX_ALBUM_SONGS} songs per album`); return; } setAlbumSongs([...albumSongs, { title: "", link: "", contributors: [] }]); }; const handleRemoveAlbumSong = (index: number) => { if (albumSongs.length === 1) { toast.error("Album must have at least one song"); return; } setAlbumSongs(albumSongs.filter((_, i) => i !== index)); }; const handleAlbumSongChange = ( index: number, field: keyof SongEntry, value: string | Contributor[], ) => { const updated = [...albumSongs]; updated[index] = { ...updated[index], [field]: value }; setAlbumSongs(updated); }; // Album song contributor search const handleAlbumSongContributorSearch = useCallback( async (index: number, query: string) => { setAlbumSongContributorSearch((prev) => ({ ...prev, [index]: query })); setAlbumSongShowDropdown((prev) => ({ ...prev, [index]: true })); if (!query.trim()) { setAlbumSongSearchResults((prev) => ({ ...prev, [index]: [] })); return; } // TODO: replace with api call await new Promise((resolve) => setTimeout(resolve, 200)); const filtered = MOCK_ARTISTS.filter( (artist) => artist.fullName.toLowerCase().includes(query.toLowerCase()) || artist.username.toLowerCase().includes(query.toLowerCase()), ); setAlbumSongSearchResults((prev) => ({ ...prev, [index]: filtered })); }, [], ); const handleAddAlbumSongContributor = ( songIndex: number, artist: ArtistSearchResult, ) => { const song = albumSongs[songIndex]; if (song.contributors.length >= MAX_CONTRIBUTORS) { toast.error(`Maximum ${MAX_CONTRIBUTORS} contributors per song`); return; } if (song.contributors.some((c) => c.username === artist.username)) { toast.error("This contributor is already added"); return; } const role = albumSongSelectedRole[songIndex] || ROLE_OPTIONS[0].value; const newContributor: Contributor = { username: artist.username, fullName: artist.fullName, role, }; handleAlbumSongChange(songIndex, "contributors", [ ...song.contributors, newContributor, ]); setAlbumSongContributorSearch((prev) => ({ ...prev, [songIndex]: "" })); setAlbumSongSearchResults((prev) => ({ ...prev, [songIndex]: [] })); setAlbumSongShowDropdown((prev) => ({ ...prev, [songIndex]: false })); }; const handleRemoveAlbumSongContributor = ( songIndex: number, username: string, ) => { const song = albumSongs[songIndex]; handleAlbumSongChange( songIndex, "contributors", song.contributors.filter((c) => c.username !== username), ); }; // Form submission const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Validation if (!title.trim()) { toast.error("Please enter a title"); return; } if (!genre) { toast.error("Please select a genre"); return; } if (releaseType === "single") { if (!link.trim()) { toast.error("Please enter a song link"); return; } } else { // Album validation for (let i = 0; i < albumSongs.length; i++) { if (!albumSongs[i].title.trim()) { toast.error(`Please enter a title for song ${i + 1}`); return; } if (!albumSongs[i].link.trim()) { toast.error(`Please enter a link for song ${i + 1}`); return; } } } setIsSubmitting(true); try { // TODO: replace with API call // const formData = new FormData(); // formData.append('title', title); // formData.append('genre', genre); // if (coverFile) formData.append('cover', coverFile); // ... // Simulate API call await new Promise((resolve) => setTimeout(resolve, 1000)); toast.success( releaseType === "single" ? "Song published successfully!" : "Album published successfully!", ); navigate("/my-songs"); } catch (error) { console.error("Error publishing:", error); toast.error("Failed to publish. Please try again."); } finally { setIsSubmitting(false); } }; if (!user?.isArtist) { return (

You need to be an artist to publish music.

← Back to Home
); } return (
{/* Back link */} Back to My Songs {/* Header */}

Publish New Music

{/* Release Type Selection */}

What are you releasing?

{/* Basic Info */}

{releaseType === "single" ? "Song Details" : "Album Details"}

{/* Title */}
setTitle(e.target.value)} placeholder={ releaseType === "single" ? "Enter song title" : "Enter album name" } className="w-full bg-[#282828] border border-white/10 rounded-lg py-3 px-4 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all" />
{/* Genre */}
{/* Cover Art */}
{coverPreview ? (
Cover preview
) : ( )}

Recommended: 1400x1400px

Max size: 5MB

JPG, PNG, or WebP

{/* Single: Link */} {releaseType === "single" && (
setLink(e.target.value)} placeholder="https://www.youtube.com/watch?v=..." className="w-full bg-[#282828] border border-white/10 rounded-lg py-3 px-4 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all" />
)}
{/* Single: Contributors */} {releaseType === "single" && (

Contributors (optional)

{contributors.length}/{MAX_CONTRIBUTORS}
{/* Added contributors */} {contributors.length > 0 && (
{contributors.map((contributor) => (
{contributor.fullName} ( { ROLE_OPTIONS.find((r) => r.value === contributor.role) ?.label } )
))}
)} {/* Add contributor */} {contributors.length < MAX_CONTRIBUTORS && (
handleContributorSearchChange(e.target.value) } onFocus={() => setShowSearchDropdown(true)} placeholder="Search for an artist..." className="w-full bg-[#282828] border border-white/10 rounded-lg py-2.5 px-4 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all" /> {/* Search dropdown */} {showSearchDropdown && contributorSearch && (
{isSearching ? (
) : searchResults.length > 0 ? ( searchResults.map((artist) => ( )) ) : (
No artists found
)}
)}
)}
)} {/* Album: Songs */} {releaseType === "album" && (

Album Songs

{albumSongs.length}/{MAX_ALBUM_SONGS} songs
{albumSongs.map((song, index) => (

Song {index + 1}

{albumSongs.length > 1 && ( )}
{/* Song title */} handleAlbumSongChange(index, "title", e.target.value) } placeholder="Song title" className="w-full bg-[#1a1a2e] border border-white/10 rounded-lg py-2.5 px-4 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all" /> {/* Song link */} handleAlbumSongChange(index, "link", e.target.value) } placeholder="Song link (YouTube)" className="w-full bg-[#1a1a2e] border border-white/10 rounded-lg py-2.5 px-4 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all" /> {/* Contributors */}
{song.contributors.length}/{MAX_CONTRIBUTORS}
{/* Added contributors */} {song.contributors.length > 0 && (
{song.contributors.map((contributor) => (
{contributor.fullName} ( { ROLE_OPTIONS.find( (r) => r.value === contributor.role, )?.label } )
))}
)} {/* Add contributor */} {song.contributors.length < MAX_CONTRIBUTORS && (
handleAlbumSongContributorSearch( index, e.target.value, ) } onFocus={() => setAlbumSongShowDropdown((prev) => ({ ...prev, [index]: true, })) } placeholder="Search artist..." className="w-full bg-[#1a1a2e] border border-white/10 rounded-lg py-2 px-3 text-white text-sm placeholder-gray-500 focus:outline-none focus:border-[#1db954] transition-all" /> {/* Dropdown */} {albumSongShowDropdown[index] && albumSongContributorSearch[index] && (
{(albumSongSearchResults[index] || []).length > 0 ? ( albumSongSearchResults[index].map( (artist) => ( ), ) ) : (
No artists found
)}
)}
)}
))} {/* Add song button */} {albumSongs.length < MAX_ALBUM_SONGS && ( )}
)} {/* Submit */}
); }; export default PublishSong;