import React, { useState, useEffect, useRef, Suspense, lazy } from "react"; import { toast } from "react-toastify"; import { Button } from "@mui/joy"; import { Accordion, AccordionTab } from "primereact/accordion"; import { AutoComplete } from "primereact/autocomplete"; import BreadcrumbNav from "./BreadcrumbNav"; import { API_URL } from "@/config"; export default function MediaRequestForm() { const [type, setType] = useState("artist"); const [selectedArtist, setSelectedArtist] = useState(null); const [artistInput, setArtistInput] = useState(""); const [albumInput, setAlbumInput] = useState(""); const [trackInput, setTrackInput] = useState(""); const [selectedItem, setSelectedItem] = useState(null); const [albums, setAlbums] = useState([]); const [tracksByAlbum, setTracksByAlbum] = useState({}); const [selectedTracks, setSelectedTracks] = useState({}); const [artistSuggestions, setArtistSuggestions] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [isSearching, setIsSearching] = useState(false); const debounceTimeout = useRef(null); const autoCompleteRef = useRef(null); // Helper fetch wrapper that includes cookies automatically const authFetch = async (url, options = {}) => { const opts = { ...options, credentials: "include", // <--- send HttpOnly cookies with requests }; return fetch(url, opts); }; // Fetch artist suggestions for autocomplete const searchArtists = (e) => { const query = e.query.trim(); if (!query) { setArtistSuggestions([]); setSelectedArtist(null); return; } if (debounceTimeout.current) clearTimeout(debounceTimeout.current); debounceTimeout.current = setTimeout(async () => { try { const res = await authFetch( `${API_URL}/trip/get_artists_by_name?artist=${encodeURIComponent( query )}`, ); if (!res.ok) throw new Error("API error"); const data = await res.json(); setArtistSuggestions(data); } catch (err) { toast.error("Failed to fetch artist suggestions."); setArtistSuggestions([]); } }, 300); }; //helpers for string truncation const truncate = (text, maxLen) => maxLen <= 3 ? text.slice(0, maxLen) : text.length <= maxLen ? text : text.slice(0, maxLen - 3) + '...'; const artistItemTemplate = (artist) => { if (!artist) return null; return
{truncate(artist.artist, 58)}
; }; // Handle autocomplete input changes (typing/selecting) const handleArtistChange = (e) => { if (typeof e.value === "string") { setArtistInput(e.value); setSelectedArtist(null); } else if (e.value && typeof e.value === "object") { setSelectedArtist(e.value); setArtistInput(e.value.artist); } else { setArtistInput(""); setSelectedArtist(null); } }; // Search button click handler const handleSearch = async () => { setIsSearching(true); if (type === "artist") { if (!selectedArtist) { toast.error("Please select a valid artist from suggestions."); setIsSearching(false); return; } setSelectedItem(selectedArtist.artist); try { const res = await authFetch( `${API_URL}/trip/get_albums_by_artist_id/${selectedArtist.id}` ); if (!res.ok) throw new Error("API error"); const data = await res.json(); data.sort((a, b) => (b.release_date || "").localeCompare(a.release_date || "") ); setAlbums(data); setTracksByAlbum({}); // Set selectedTracks for all albums as null (means tracks loading/not loaded) setSelectedTracks( data.reduce((acc, album) => { acc[album.id] = null; return acc; }, {}) ); } catch (err) { toast.error("Failed to fetch albums for artist."); setAlbums([]); setTracksByAlbum({}); setSelectedTracks({}); } } else if (type === "album") { if (!artistInput.trim() || !albumInput.trim()) { toast.error("Artist and Album are required."); setIsSearching(false); return; } setSelectedItem(`${artistInput} - ${albumInput}`); setAlbums([]); setTracksByAlbum({}); setSelectedTracks({}); } else if (type === "track") { if (!artistInput.trim() || !trackInput.trim()) { toast.error("Artist and Track are required."); setIsSearching(false); return; } setSelectedItem(`${artistInput} - ${trackInput}`); setAlbums([]); setTracksByAlbum({}); setSelectedTracks({}); } setIsSearching(false); }; const handleTrackClick = async (trackId, artist, title) => { try { const res = await authFetch(`${API_URL}/trip/get_track_by_id/${trackId}`); if (!res.ok) throw new Error("Failed to fetch track URL"); const data = await res.json(); if (data.stream_url) { const fileResponse = await authFetch(data.stream_url); if (!fileResponse.ok) throw new Error("Failed to fetch track file"); const blob = await fileResponse.blob(); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; // Sanitize filename (remove / or other illegal chars) const sanitize = (str) => str.replace(/[\\/:*?"<>|]/g, "_"); const filename = `${sanitize(artist)} - ${sanitize(title)}.flac`; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } else { toast.error("No stream URL returned for this track."); } } catch (error) { toast.error("Failed to get track download URL."); console.error(error); } }; // Sequentially fetch tracks for albums not loaded yet useEffect(() => { if (type !== "artist" || albums.length === 0) return; let isCancelled = false; const albumsToFetch = albums.filter((a) => !tracksByAlbum[a.id]); if (albumsToFetch.length === 0) return; const fetchTracksSequentially = async () => { for (const album of albumsToFetch) { if (isCancelled) break; try { const res = await authFetch( `${API_URL}/trip/get_tracks_by_album_id/${album.id}` ); if (!res.ok) throw new Error("API error"); const data = await res.json(); if (isCancelled) break; setTracksByAlbum((prev) => ({ ...prev, [album.id]: data })); setSelectedTracks((prev) => ({ ...prev, [album.id]: data.map((t) => String(t.id)), })); } catch (err) { toast.error(`Failed to fetch tracks for album ${album.album}.`); setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] })); setSelectedTracks((prev) => ({ ...prev, [album.id]: [] })); } } }; fetchTracksSequentially(); return () => { isCancelled = true; }; }, [albums, type]); // Toggle individual track checkbox const toggleTrack = (albumId, trackId) => { setSelectedTracks((prev) => { const current = new Set(prev[albumId] || []); if (current.has(String(trackId))) current.delete(String(trackId)); else current.add(String(trackId)); return { ...prev, [albumId]: Array.from(current) }; }); }; // Toggle album checkbox (select/deselect all tracks in album) const toggleAlbum = (albumId) => { const allTracks = tracksByAlbum[albumId]?.map((t) => String(t.id)) || []; setSelectedTracks((prev) => { const current = prev[albumId] || []; const allSelected = current.length === allTracks.length; return { ...prev, [albumId]: allSelected ? [] : [...allTracks], }; }); }; // Attach scroll fix for autocomplete panel const attachScrollFix = () => { setTimeout(() => { const panel = document.querySelector(".p-autocomplete-panel"); const items = panel?.querySelector(".p-autocomplete-items"); if (items) { items.style.maxHeight = "200px"; items.style.overflowY = "auto"; items.style.overscrollBehavior = "contain"; const wheelHandler = (e) => { const delta = e.deltaY; const atTop = items.scrollTop === 0; const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight; if ((delta < 0 && atTop) || (delta > 0 && atBottom)) { e.preventDefault(); } else { e.stopPropagation(); } }; items.removeEventListener("wheel", wheelHandler); items.addEventListener("wheel", wheelHandler, { passive: false }); } }, 0); }; // Submit request handler with progress indicator const handleSubmitRequest = async () => { setIsSubmitting(true); try { // Example: simulate submission delay await new Promise((resolve) => setTimeout(resolve, 1500)); toast.success("Request submitted!"); } catch (err) { toast.error("Failed to submit request."); } finally { setIsSubmitting(false); } }; return (
{(type === "album" || type === "track") && ( type === "album" ? setAlbumInput(e.target.value) : setTrackInput(e.target.value) } placeholder={type === "album" ? "Album" : "Track"} /> )}
{type === "artist" && albums.length > 0 && ( <> {albums.map(({ album, id, release_date }) => { const allTracks = tracksByAlbum[id] || []; const selected = selectedTracks[id]; // Album checkbox is checked if tracks not loaded (selected === null) // or all tracks loaded and all selected const allChecked = selected === null || (selected?.length === allTracks.length && allTracks.length > 0); const someChecked = selected !== null && selected.length > 0 && selected.length < allTracks.length; return ( { if (el) el.indeterminate = someChecked; }} onChange={() => toggleAlbum(id)} onClick={(e) => e.stopPropagation()} className="cursor-pointer" aria-label={`Select all tracks for album ${album}`} /> {album} ({release_date})
} > {allTracks.length > 0 ? ( ) : (
{tracksByAlbum[id] ? "No tracks found for this album." : "Loading tracks..."}
)} ); })}
)}
); }