This commit is contained in:
2025-08-15 14:15:18 -04:00
parent 31bd4e1b54
commit e51be9697c
2 changed files with 113 additions and 54 deletions

View File

@@ -7,6 +7,7 @@ export default function BreadcrumbNav({ currentPage }) {
]; ];
return ( return (
<div>
<nav aria-label="breadcrumb" className="mb-6 flex gap-4 text-sm font-medium text-blue-600 dark:text-blue-400"> <nav aria-label="breadcrumb" className="mb-6 flex gap-4 text-sm font-medium text-blue-600 dark:text-blue-400">
{pages.map(({ key, label, href }, i) => ( {pages.map(({ key, label, href }, i) => (
<React.Fragment key={key}> <React.Fragment key={key}>
@@ -21,5 +22,9 @@ export default function BreadcrumbNav({ currentPage }) {
</React.Fragment> </React.Fragment>
))} ))}
</nav > </nav >
<div class="mb-2">
Self Service
</div>
</div>
); );
} }

View File

@@ -123,6 +123,8 @@ export default function MediaRequestForm() {
metadataFetchToastId.current = toast.info("Retrieving metadata...", metadataFetchToastId.current = toast.info("Retrieving metadata...",
{ {
autoClose: false, autoClose: false,
progress: 0,
closeOnClick: false,
} }
); );
if (type === "artist") { if (type === "artist") {
@@ -222,6 +224,42 @@ export default function MediaRequestForm() {
} }
}; };
const allTracksLoaded = albums.every(({ id }) => Array.isArray(tracksByAlbum[id]) && tracksByAlbum[id].length > 0);
const handleToggleAllAlbums = () => {
const allSelected = albums.every(({ id }) => {
const allTracks = tracksByAlbum[id] || [];
return selectedTracks[id]?.length === allTracks.length && allTracks.length > 0;
});
const newSelection = {};
albums.forEach(({ id }) => {
const allTracks = tracksByAlbum[id] || [];
if (allSelected) {
// Uncheck all
newSelection[id] = [];
} else {
// Check all tracks in the album
newSelection[id] = allTracks.map(track => String(track.id));
}
});
setSelectedTracks(newSelection);
};
<a
href="#"
role="button"
onClick={(e) => {
e.preventDefault();
if (!allTracksLoaded) return; // prevent clicking before data ready
handleToggleAllAlbums();
}}
className={`text-sm hover:underline cursor-pointer ${!allTracksLoaded ? "text-gray-400 dark:text-gray-500 pointer-events-none" : "text-blue-600"
}`}
>
Check / Uncheck All Albums
</a>
// Sequentially fetch tracks for albums not loaded yet // Sequentially fetch tracks for albums not loaded yet
@@ -233,9 +271,14 @@ export default function MediaRequestForm() {
if (albumsToFetch.length === 0) return; if (albumsToFetch.length === 0) return;
const fetchTracksSequentially = async () => { const fetchTracksSequentially = async () => {
const minDelay = 600; // ms between API requests const minDelay = 400; // ms between API requests
setIsFetching(true); setIsFetching(true);
for (const album of albumsToFetch) {
const totalAlbums = albumsToFetch.length;
for (let index = 0; index < totalAlbums; index++) {
const album = albumsToFetch[index];
if (isCancelled) break; if (isCancelled) break;
setLoadingAlbumId(album.id); setLoadingAlbumId(album.id);
@@ -263,14 +306,24 @@ export default function MediaRequestForm() {
setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] })); setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] }));
setSelectedTracks((prev) => ({ ...prev, [album.id]: [] })); setSelectedTracks((prev) => ({ ...prev, [album.id]: [] }));
} }
// Update progress toast
toast.update(metadataFetchToastId.current, {
progress: (index + 1) / totalAlbums,
render: `Retrieving metadata... (${index + 1} / ${totalAlbums})`,
});
} }
setLoadingAlbumId(null); setLoadingAlbumId(null);
setIsFetching(false); setIsFetching(false);
try {
toast.done(metadataFetchToastId.current); // Finish the toast
} catch (err) { toast.update(metadataFetchToastId.current, {
console.log(err); render: "Metadata retrieved!",
}; type: "success",
progress: 1,
autoClose: 1500,
});
}; };
fetchTracksSequentially(); fetchTracksSequentially();
@@ -281,6 +334,7 @@ export default function MediaRequestForm() {
}, [albums, type]); }, [albums, type]);
// Toggle individual track checkbox // Toggle individual track checkbox
const toggleTrack = (albumId, trackId) => { const toggleTrack = (albumId, trackId) => {
setSelectedTracks((prev) => { setSelectedTracks((prev) => {
@@ -337,21 +391,36 @@ export default function MediaRequestForm() {
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// Example: simulate submission delay
await new Promise((resolve) => setTimeout(resolve, 1500));
const allSelectedIds = Object.values(selectedTracks) const allSelectedIds = Object.values(selectedTracks)
.filter(arr => Array.isArray(arr)) // skip null entries .filter(arr => Array.isArray(arr)) // skip null entries
.flat(); .flat();
const response = await authFetch(`${API_URL}/trip/bulk_fetch`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({ track_ids: allSelectedIds }),
});
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const data = await response.json();
toast.success(`Request submitted! (${allSelectedIds.length} tracks)`); toast.success(`Request submitted! (${allSelectedIds.length} tracks)`);
console.debug("Requested: ", selectedTracks); console.debug("Requested: ", selectedTracks);
console.debug("Flattened: ", allSelectedIds); console.debug("Flattened: ", allSelectedIds);
console.debug("Server response: ", data);
} catch (err) { } catch (err) {
console.error(err);
toast.error("Failed to submit request."); toast.error("Failed to submit request.");
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
return ( return (
<div className="max-w-3xl mx-auto my-10 p-6 rounded-xl shadow-md bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100 border border-neutral-200 dark:border-neutral-700"> <div className="max-w-3xl mx-auto my-10 p-6 rounded-xl shadow-md bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100 border border-neutral-200 dark:border-neutral-700">
<style>{` <style>{`
@@ -424,42 +493,13 @@ export default function MediaRequestForm() {
color: #aaa; color: #aaa;
} }
`}</style> `}</style>
<BreadcrumbNav currentPage="request" /> <BreadcrumbNav currentPage="request" />
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex gap-4">
<label className="flex items-center gap-2">
<input
type="radio"
value="artist"
checked={type === "artist"}
onChange={() => setType("artist")}
/>
Artist
</label>
<label className="flex items-center gap-2">
<input
type="radio"
value="album"
checked={type === "album"}
onChange={() => setType("album")}
/>
Album
</label>
<label className="flex items-center gap-2">
<input
type="radio"
value="track"
checked={type === "track"}
onChange={() => setType("track")}
/>
Track
</label>
</div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<label for="artistInput">Artist: </label>
<AutoComplete <AutoComplete
id={artistInput}
ref={autoCompleteRef} ref={autoCompleteRef}
value={selectedArtist || artistInput} value={selectedArtist || artistInput}
suggestions={artistSuggestions} suggestions={artistSuggestions}
@@ -501,6 +541,20 @@ export default function MediaRequestForm() {
{type === "artist" && albums.length > 0 && ( {type === "artist" && albums.length > 0 && (
<> <>
<div className="flex justify-end mb-2">
<a
href="#"
role="button"
onClick={(e) => {
e.preventDefault(); // prevent page jump
handleToggleAllAlbums();
}}
className="text-sm text-blue-600 hover:underline cursor-pointer"
>
Check / Uncheck All Albums
</a>
</div>
<Accordion <Accordion
multiple multiple
className="mt-4" className="mt-4"