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, ENVIRONMENT } 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 [loadingAlbumId, setLoadingAlbumId] = useState(null);
const [expandedAlbums, setExpandedAlbums] = useState([]);
const debounceTimeout = useRef(null);
const autoCompleteRef = useRef(null);
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Helper for delays
// 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);
};
const Spinner = () => (
);
// 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 {
// Ensure at least 600ms between actual requests
const now = Date.now();
if (!searchArtists.lastCall) searchArtists.lastCall = 0;
const elapsed = now - searchArtists.lastCall;
const minDelay = 600; // ms
if (elapsed < minDelay) await delay(minDelay - elapsed);
searchArtists.lastCall = Date.now();
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([]);
}
}, 500); // debounce 500ms
};
//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({});
setExpandedAlbums([]);
// 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 () => {
const minDelay = 600; // ms between API requests
for (const album of albumsToFetch) {
if (isCancelled) break;
setLoadingAlbumId(album.id);
try {
const now = Date.now();
if (!fetchTracksSequentially.lastCall) fetchTracksSequentially.lastCall = 0;
const elapsed = now - fetchTracksSequentially.lastCall;
if (elapsed < minDelay) await delay(minDelay - elapsed);
fetchTracksSequentially.lastCall = Date.now();
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]: [] }));
}
}
setLoadingAlbumId(null);
};
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));
const allSelectedIds = Object.values(selectedTracks)
.filter(arr => Array.isArray(arr)) // skip null entries
.flat();
toast.success(`Request submitted! (${allSelectedIds.length} tracks)`);
console.debug("Requested: ", selectedTracks);
console.debug("Flattened: ", allSelectedIds);
} catch (err) {
toast.error("Failed to submit request.");
} finally {
setIsSubmitting(false);
}
};
return (
}
>
{allTracks.length > 0 ? (
) : (
{tracksByAlbum[id] ? "No tracks found for this album." : "Loading tracks..."}
)}
);
})}
>
)}
);
}