various changes

This commit is contained in:
2025-08-09 07:10:04 -04:00
parent fbd342c6a7
commit 21796e768e
20 changed files with 886 additions and 342 deletions

View File

@@ -0,0 +1,25 @@
import React from "react";
export default function BreadcrumbNav({ currentPage }) {
const pages = [
{ key: "request", label: "Request Media", href: "/qs2" },
{ key: "management", label: "Manage Requests", href: "/qs2/requests" },
];
return (
<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) => (
<React.Fragment key={key}>
<a
href={href}
className={`hover:underline ${currentPage === key ? "font-bold underline" : ""}`}
aria-current={currentPage === key ? "page" : undefined}
>
{label}
</a>
{i < pages.length - 1 && <span aria-hidden="true">/</span>}
</React.Fragment>
))}
</nav>
);
}

View File

@@ -0,0 +1,483 @@
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";
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(
`https://api.codey.lol/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 <div>{truncate(artist.artist, 58)}</div>;
};
// 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(
`https://api.codey.lol/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(`https://api.codey.lol/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(
`https://api.codey.lol/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 (
<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>{`
.p-accordion-tab {
background-color: #ffffff;
color: #000000;
}
[data-theme="dark"] .p-accordion-tab {
background-color: #1e1e1e;
color: #ffffff;
}
[data-theme="dark"] .p-accordion-header .p-accordion-header-link {
background-color: #1e1e1e !important;
color: #ffffff !important;
}
[data-theme="dark"] .p-accordion-content {
background-color: #2a2a2a;
color: #ffffff;
}
`}</style>
<BreadcrumbNav currentPage="request" />
<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">
<AutoComplete
ref={autoCompleteRef}
value={selectedArtist || artistInput}
suggestions={artistSuggestions}
field="artist"
completeMethod={searchArtists}
onChange={handleArtistChange}
placeholder="Artist"
dropdown
className="w-full"
inputClassName="w-full px-3 py-2 rounded border border-neutral-300 dark:border-neutral-600 text-black dark:text-white dark:bg-neutral-800"
onShow={attachScrollFix}
itemTemplate={artistItemTemplate}
/>
{(type === "album" || type === "track") && (
<input
type="text"
className="w-full dark:bg-neutral-800 dark:text-white border border-neutral-300 dark:border-neutral-600 rounded px-3 py-2"
value={type === "album" ? albumInput : trackInput}
onChange={(e) =>
type === "album" ? setAlbumInput(e.target.value) : setTrackInput(e.target.value)
}
placeholder={type === "album" ? "Album" : "Track"}
/>
)}
<Button onClick={handleSearch} disabled={isSearching}>
{isSearching ? (
<span className="flex items-center gap-2">
<span className="animate-spin h-4 w-4 border-2 border-t-2 border-gray-200 border-t-primary rounded-full"></span>
Searching...
</span>
) : (
"Search"
)}
</Button>
</div>
{type === "artist" && albums.length > 0 && (
<>
<Accordion multiple className="mt-4">
{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 (
<AccordionTab
key={id}
header={
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={allChecked}
ref={(el) => {
if (el) el.indeterminate = someChecked;
}}
onChange={() => toggleAlbum(id)}
onClick={(e) => e.stopPropagation()}
className="cursor-pointer"
aria-label={`Select all tracks for album ${album}`}
/>
<span>{album}</span>
<small className="ml-2 text-neutral-500 dark:text-neutral-400">({release_date})</small>
</div>
}
>
{allTracks.length > 0 ? (
<ul className="text-sm">
{allTracks.map((track) => (
<li key={track.id} className="py-1 flex items-center gap-2">
<input
type="checkbox"
checked={selected?.includes(String(track.id))}
onChange={() => toggleTrack(id, track.id)}
className="cursor-pointer"
aria-label={`Select track ${track.title}`}
/>
<button
type="button"
onClick={() => handleTrackClick(track.id, selectedArtist.artist, track.title)}
className="font-medium text-blue-600 hover:underline cursor-pointer bg-transparent border-none p-0"
aria-label={`Download track ${track.title}`}
>
{track.title}
</button>
<span className="text-xs text-neutral-500">{track.audioQuality}</span>
{track.version && (
<span className="text-xs text-neutral-400">({track.version})</span>
)}
{track.duration && (
<span className="text-xs text-neutral-500 ml-auto tabular-nums">{track.duration}</span>
)}
</li>
))}
</ul>
) : (
<div className="text-sm italic text-neutral-600 dark:text-neutral-400">
{tracksByAlbum[id] ? "No tracks found for this album." : "Loading tracks..."}
</div>
)}
</AccordionTab>
);
})}
</Accordion>
<div className="flex justify-end">
<Button onClick={handleSubmitRequest} color="primary" className="mt-4" disabled={isSubmitting}>
{isSubmitting ? (
<span className="flex items-center gap-2">
<span className="animate-spin h-4 w-4 border-2 border-t-2 border-gray-200 border-t-primary rounded-full"></span>
Submitting...
</span>
) : (
"Submit Request"
)}
</Button>
</div>
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,493 @@
import React, { useState, useEffect, Suspense, lazy } from "react";
import { toast } from 'react-toastify';
import { DataTable } from "primereact/datatable";
import { Column } from "primereact/column";
import { Dropdown } from "primereact/dropdown";
import { Button } from "@mui/joy";
import { Dialog } from "primereact/dialog";
import { confirmDialog, ConfirmDialog } from "primereact/confirmdialog";
import BreadcrumbNav from "./BreadcrumbNav";
const STATUS_OPTIONS = ["Pending", "Completed", "Failed"];
const TYPE_OPTIONS = ["Artist", "Album", "Track"];
const initialRequests = [
{
id: 1,
type: "Artist",
artist: "Bring Me The Horizon",
album: "",
track: "",
status: "Pending",
details: {
requestedBy: "codey",
timestamp: "2025-07-01T12:00:00Z",
comments: "",
},
},
{
id: 2,
type: "Track",
artist: "We Butter The Bread With Butter",
album: "Das Album",
track: "20 km/h",
status: "Failed",
details: {
requestedBy: "codey",
timestamp: "2025-06-11T09:00:00Z",
comments: "Track not found in external database.",
},
},
{
id: 3,
type: "Track",
artist: "We Butter The Bread With Butter",
album: "Das Album",
track: "20 km/h",
status: "Completed",
details: {
requestedBy: "codey",
timestamp: "2025-06-11T09:00:00Z",
comments: "Track retrieved successfully.",
},
},
];
export default function RequestManagement() {
const [requests, setRequests] = useState(initialRequests);
const [filterType, setFilterType] = useState(null);
const [filterStatus, setFilterStatus] = useState(null);
const [filteredRequests, setFilteredRequests] = useState(initialRequests);
const [selectedRequest, setSelectedRequest] = useState(null);
const [isDialogVisible, setIsDialogVisible] = useState(false);
useEffect(() => {
let filtered = [...requests];
if (filterType) {
filtered = filtered.filter((r) => r.type === filterType);
}
if (filterStatus) {
filtered = filtered.filter((r) => r.status === filterStatus);
}
setFilteredRequests(filtered);
}, [filterType, filterStatus, requests]);
const getStatusTextColor = (status) => {
switch (status) {
case "Pending":
return "text-yellow-500";
case "Completed":
return "text-green-500";
case "Failed":
return "text-red-500";
default:
return "text-neutral-500";
}
};
const confirmDelete = (requestId) => {
confirmDialog({
message: "Are you sure you want to delete this request?",
header: "Confirm Delete",
icon: "pi pi-exclamation-triangle",
accept: () => deleteRequest(requestId),
});
};
const deleteRequest = (requestId) => {
setRequests((prev) => prev.filter((r) => r.id !== requestId));
toast.success("Request deleted");
};
const statusBodyTemplate = (rowData) => {
let colorClass = "";
switch (rowData.status) {
case "Pending":
colorClass = "bg-yellow-300 text-yellow-900";
break;
case "Completed":
colorClass = "bg-green-300 text-green-900";
break;
case "Failed":
colorClass = "bg-red-300 text-red-900";
break;
default:
colorClass = "bg-gray-300 text-gray-900";
}
return (
<span
className={`inline-block px-3 py-1 rounded-full font-semibold text-sm ${colorClass}`}
aria-label={`Status: ${rowData.status}`}
>
{rowData.status}
</span>
);
};
const actionBodyTemplate = (rowData) => {
return (
<Button
color="danger"
size="sm"
onClick={() => confirmDelete(rowData.id)}
variant="outlined"
>
Delete
</Button>
);
};
return (
<div
className="bg-white dark:bg-neutral-900 dark:text-neutral-100 rounded-xl shadow-md border border-neutral-200 dark:border-neutral-700 p-6 space-y-6"
style={{ display: "inline-block" }}
>
<style>{`
.p-datatable {
background-color: white;
color: #1a1a1a;
border-color: #ccc;
}
.p-datatable-header {
background-color: #f9f9f9;
color: #333;
border-bottom: 1px solid #ccc;
}
.p-datatable-thead > tr > th {
background-color: #f0f0f0;
color: #222;
border-bottom: 1px solid #ccc;
}
.p-datatable-tbody > tr > td {
border-bottom: 1px solid #eee;
}
/* Dark mode improvements with brighter text */
[data-theme="dark"] .p-datatable {
background-color: #121212;
color: #f0f0f0; /* brighter text */
border-color: #333;
}
[data-theme="dark"] .p-datatable-header {
background-color: #1f1f1f;
color: #f5f5f5; /* brighter */
border-bottom: 1px solid #444;
}
[data-theme="dark"] .p-datatable-thead > tr > th {
background-color: #222222;
color: #f5f5f5; /* brighter */
border-bottom: 1px solid #555;
}
[data-theme="dark"] .p-datatable-tbody > tr {
background-color: #1a1a1a;
border-bottom: 1px solid #333;
transition: background-color 0.2s ease;
color: #f0f0f0; /* brighter text */
}
/* Zebra stripes */
[data-theme="dark"] .p-datatable-tbody > tr:nth-child(odd) {
background-color: #181818;
color: #f0f0f0;
}
/* Hover effect */
[data-theme="dark"] .p-datatable-tbody > tr:hover {
background-color: #333333;
cursor: pointer;
color: #fff; /* brightest on hover */
}
/* Dropdown inside dark mode */
[data-theme="dark"] .p-dropdown,
[data-theme="dark"] .p-dropdown-label,
[data-theme="dark"] .p-dropdown-panel,
[data-theme="dark"] .p-dropdown-item {
background-color: #2a2a2a !important;
color: #f0f0f0 !important; /* brighter */
}
/* Buttons */
[data-theme="dark"] .p-button {
background-color: transparent !important;
color: #f0f0f0 !important; /* brighter */
border-color: #555 !important;
transition: background-color 0.2s ease;
}
[data-theme="dark"] .p-button:hover {
background-color: #555 !important;
color: #fff !important;
}
[data-theme="dark"] .p-button.p-button-danger {
border-color: #ff4d4f !important;
color: #ff4d4f !important;
}
[data-theme="dark"] .p-button.p-button-danger:hover {
background-color: #ff4d4f !important;
color: #fff !important;
}
/* ===== PrimeReact Paginator Light Mode ===== */
.p-paginator {
background-color: #fff;
color: #1a1a1a;
border-top: 1px solid #ddd;
}
.p-paginator .p-paginator-page,
.p-paginator .p-paginator-next,
.p-paginator .p-paginator-prev {
color: #1a1a1a;
border: none;
background: transparent;
}
.p-paginator .p-highlight {
background-color: #007bff;
color: white;
border-radius: 6px;
}
/* ===== PrimeReact Paginator Dark Mode ===== */
[data-theme="dark"] .p-paginator {
background-color: #121212;
color: #f0f0f0;
border-top: 1px solid #333;
}
[data-theme="dark"] .p-paginator .p-paginator-page,
[data-theme="dark"] .p-paginator .p-paginator-next,
[data-theme="dark"] .p-paginator .p-paginator-prev {
color: #ccc;
border: none;
background: transparent;
}
[data-theme="dark"] .p-paginator .p-paginator-page:hover,
[data-theme="dark"] .p-paginator .p-paginator-next:hover,
[data-theme="dark"] .p-paginator .p-paginator-prev:hover {
background-color: #333;
color: #fff;
border-radius: 6px;
}
[data-theme="dark"] .p-paginator .p-highlight {
background-color: #3b82f6; /* Tailwind blue-500 */
color: white;
border-radius: 6px;
}
.p-paginator .p-paginator-first,
.p-paginator .p-paginator-last {
color: #1a1a1a;
background: transparent;
border: none;
}
.p-paginator .p-paginator-first:hover,
.p-paginator .p-paginator-last:hover {
background-color: #f0f0f0;
border-radius: 6px;
}
/* Dark Mode: First/Last page buttons */
[data-theme="dark"] .p-paginator .p-paginator-first,
[data-theme="dark"] .p-paginator .p-paginator-last {
color: #ccc;
background: transparent;
border: none;
}
[data-theme="dark"] .p-paginator .p-paginator-first:hover,
[data-theme="dark"] .p-paginator .p-paginator-last:hover {
background-color: #333;
color: #fff;
border-radius: 6px;
}
/* ConfirmDialog - Light Theme */
.p-confirm-dialog {
background-color: #ffffff;
color: #1a1a1a;
border: 1px solid #ccc;
}
.p-confirm-dialog .p-dialog-header {
background-color: #f5f5f5;
color: #222;
}
.p-confirm-dialog .p-dialog-content {
background-color: #ffffff;
color: #333;
}
.p-confirm-dialog .p-dialog-footer {
background-color: #fafafa;
border-top: 1px solid #ddd;
}
.p-confirm-dialog .p-button {
border-radius: 0.5rem;
}
/* ConfirmDialog - Dark Theme */
[data-theme='dark'] .p-confirm-dialog {
background-color: #2a2a2a;
color: #e5e5e5;
border: 1px solid #444;
}
[data-theme='dark'] .p-confirm-dialog .p-dialog-header {
background-color: #1f1f1f;
color: #ddd;
}
[data-theme='dark'] .p-confirm-dialog .p-dialog-content {
background-color: #2a2a2a;
color: #ccc;
}
[data-theme='dark'] .p-confirm-dialog .p-dialog-footer {
background-color: #242424;
border-top: 1px solid #333;
}
[data-theme='dark'] .p-confirm-dialog .p-button {
background-color: transparent !important;
color: #ddd !important;
border-color: #555 !important;
}
[data-theme='dark'] .p-confirm-dialog .p-button.p-button-danger {
color: #ff4d4f !important;
border-color: #ff4d4f !important;
}
[data-theme='dark'] .p-dialog-title {
color: #eee;
}
[data-theme='dark'] .p-dialog-header-icon {
color: #ccc;
}
/* PrimeReact Dialog Theming */
.p-dialog {
background-color: #ffffff;
color: #000000;
}
[data-theme="dark"] .p-dialog {
background-color: #1e1e1e;
color: #ffffff;
}
[data-theme="dark"] .p-dialog .p-dialog-header {
background-color: #1e1e1e;
border-bottom: 1px solid #444;
}
[data-theme="dark"] .p-dialog .p-dialog-content {
background-color: #2a2a2a;
color: #ffffff;
}
[data-theme="dark"] .p-dialog .p-dialog-footer {
background-color: #1e1e1e;
border-top: 1px solid #444;
}
`}</style>
<BreadcrumbNav currentPage="management" />
<h2 className="text-3xl font-semibold" style={{ marginTop: 0 }}>
Media Request Management
</h2>
<div className="flex flex-wrap gap-6 mb-6">
<Dropdown
value={filterType}
options={[
{ label: "All Types", value: null },
...TYPE_OPTIONS.map((t) => ({ label: t, value: t })),
]}
onChange={(e) => setFilterType(e.value)}
placeholder="Filter by Type"
className="min-w-[180px]"
/>
<Dropdown
value={filterStatus}
options={[
{ label: "All Statuses", value: null },
...STATUS_OPTIONS.map((s) => ({ label: s, value: s })),
]}
onChange={(e) => setFilterStatus(e.value)}
placeholder="Filter by Status"
className="min-w-[180px]"
/>
</div>
<DataTable
value={filteredRequests}
paginator
rows={10}
removableSort
sortMode="multiple"
emptyMessage="No requests found."
rowClassName="!py-4"
className="min-w-full bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100"
onRowClick={(e) => {
setSelectedRequest(e.data);
setIsDialogVisible(true);
}}
>
<Column field="id" header="ID" sortable style={{ width: "5rem" }} />
<Column field="type" header="Type" sortable />
<Column field="artist" header="Artist" sortable />
<Column field="album" header="Album" sortable />
<Column field="track" header="Track" sortable />
<Column
field="status"
header="Status"
body={statusBodyTemplate}
style={{ width: "10rem", textAlign: "center" }}
sortable
/>
<Column
body={actionBodyTemplate}
header="Actions"
style={{ width: "9rem", textAlign: "center" }}
/>
</DataTable>
<ConfirmDialog />
<Dialog
header="Request Details"
visible={isDialogVisible}
style={{ width: "500px" }}
onHide={() => setIsDialogVisible(false)}
breakpoints={{ '960px': '95vw' }}
modal
dismissableMask
>
{selectedRequest && (
<div className="space-y-4 text-sm">
<p><strong>ID:</strong> {selectedRequest.id}</p>
<p><strong>Type:</strong> {selectedRequest.type}</p>
<p><strong>Artist:</strong> {selectedRequest.artist}</p>
{selectedRequest.album && <p><strong>Album:</strong> {selectedRequest.album}</p>}
{selectedRequest.track && <p><strong>Track:</strong> {selectedRequest.track}</p>}
<p className="text-sm flex items-center gap-2">
<span className="font-semibold">Status:</span>
<span
className={`px-2 py-0.5 rounded-full text-xs font-bold ${selectedRequest.status === "Pending"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100"
: selectedRequest.status === "Completed"
? "bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100"
: "bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100"
}`}
>
{selectedRequest.status}
</span>
</p>
{selectedRequest.details && (
<>
<p><strong>Requested By:</strong> {selectedRequest.details.requestedBy}</p>
<p><strong>Timestamp:</strong> {new Date(selectedRequest.details.timestamp).toLocaleString()}</p>
{selectedRequest.details.comments && (
<p><strong>Comments:</strong> {selectedRequest.details.comments}</p>
)}
</>
)}
</div>
)}
</Dialog>
</div>
);
}