misc
This commit is contained in:
@@ -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}>
|
||||||
@@ -20,6 +21,10 @@ export default function BreadcrumbNav({ currentPage }) {
|
|||||||
{i < pages.length - 1 && <span aria-hidden="true">/</span>}
|
{i < pages.length - 1 && <span aria-hidden="true">/</span>}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav >
|
||||||
|
<div class="mb-2">
|
||||||
|
Self Service
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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"
|
||||||
|
Reference in New Issue
Block a user