diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css
index ab1f46c..1fceb0d 100644
--- a/src/assets/styles/global.css
+++ b/src/assets/styles/global.css
@@ -249,4 +249,5 @@ Custom
}
.d-light > * {
background-color: rgba(255, 255, 255, 0.9);
-}
\ No newline at end of file
+}
+
diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx
index b014462..fd73c10 100644
--- a/src/components/AppLayout.jsx
+++ b/src/components/AppLayout.jsx
@@ -7,32 +7,36 @@ import { PrimeReactProvider } from "primereact/api";
import { usePrimeReactThemeSwitcher } from '@/hooks/usePrimeReactThemeSwitcher.jsx';
import CustomToastContainer from '../components/ToastProvider.jsx';
import LyricSearch from './LyricSearch.jsx';
+import MediaRequestForm from './qs2/MediaRequestForm.jsx';
+import RequestManagement from './qs2/RequestManagement.jsx';
import 'primereact/resources/themes/bootstrap4-light-blue/theme.css';
import 'primereact/resources/primereact.min.css';
-export default function Root({child}) {
+export default function Root({ child }) {
window.toast = toast;
const theme = document.documentElement.getAttribute("data-theme")
usePrimeReactThemeSwitcher(theme);
// console.log(opts.children);
return (
-
-
- {/*
+
+ {/* }
variant="soft"
color="danger">
Work in progress... bugs are to be expected.
*/}
- {child == "LyricSearch" && ()}
- {child == "Player" && ()}
- {child == "Memes" && }
-
+ {child == "LyricSearch" && ()}
+ {child == "Player" && ()}
+ {child == "Memes" && }
+ {child == "qs2.MediaRequestForm" && }
+ {child == "qs2.RequestManagement" && }
+
);
}
diff --git a/src/components/qs2/BreadcrumbNav.jsx b/src/components/qs2/BreadcrumbNav.jsx
new file mode 100644
index 0000000..ba65fe6
--- /dev/null
+++ b/src/components/qs2/BreadcrumbNav.jsx
@@ -0,0 +1,25 @@
+import React from "react";
+
+export default function BreadcrumbNav({ currentPage }) {
+ const pages = [
+ { key: "request", label: "Request Media", href: "new" },
+ { key: "management", label: "Manage Requests", href: "requests" },
+ ];
+
+ return (
+
+ );
+}
diff --git a/src/components/qs2/MediaRequestForm.jsx b/src/components/qs2/MediaRequestForm.jsx
new file mode 100644
index 0000000..3908df5
--- /dev/null
+++ b/src/components/qs2/MediaRequestForm.jsx
@@ -0,0 +1,229 @@
+import React, { useState, useEffect, useRef } from "react";
+import { Button } from "@mui/joy";
+import { toast } from "react-toastify";
+import { Checkbox } from "primereact/checkbox";
+import { Accordion, AccordionTab } from "primereact/accordion";
+import BreadcrumbNav from "./BreadcrumbNav";
+
+export default function MediaRequestForm() {
+ const [type, setType] = useState("artist");
+ const [artist, setArtist] = useState("");
+ const [album, setAlbum] = useState("");
+ const [track, setTrack] = useState("");
+ const [selectedItem, setSelectedItem] = useState(null);
+ const [albums, setAlbums] = useState([]);
+ const [selectedAlbums, setSelectedAlbums] = useState([]);
+ const [tracksByAlbum, setTracksByAlbum] = useState({});
+ const [selectedTracks, setSelectedTracks] = useState({});
+
+ const handleSearch = async () => {
+ if (type === "artist" && !artist.trim()) {
+ toast.error("Artist is required.");
+ return;
+ }
+ if (type === "album" && (!artist.trim() || !album.trim())) {
+ toast.error("Artist and Album are required.");
+ return;
+ }
+ if (type === "track" && (!artist.trim() || !track.trim())) {
+ toast.error("Artist and Track are required.");
+ return;
+ }
+
+ setSelectedItem(
+ type === "artist"
+ ? artist
+ : type === "album"
+ ? `${artist} - ${album}`
+ : `${artist} - ${track}`
+ );
+
+ // TODO: Fetch albums or tracks based on type and inputs
+ if (type === "artist") {
+ const fakeAlbums = ["Album A", "Album B"];
+ setAlbums(fakeAlbums);
+ setSelectedAlbums(fakeAlbums);
+ setTracksByAlbum({
+ "Album A": ["Track A1", "Track A2"],
+ "Album B": ["Track B1", "Track B2"],
+ });
+ setSelectedTracks({
+ "Album A": ["Track A1", "Track A2"],
+ "Album B": ["Track B1", "Track B2"],
+ });
+ } else {
+ setAlbums([]);
+ setTracksByAlbum({});
+ setSelectedTracks({});
+ }
+ };
+
+ const toggleTrack = (album, track) => {
+ setSelectedTracks((prev) => {
+ const tracks = new Set(prev[album] || []);
+ if (tracks.has(track)) {
+ tracks.delete(track);
+ } else {
+ tracks.add(track);
+ }
+ return { ...prev, [album]: Array.from(tracks) };
+ });
+ };
+
+ // For managing indeterminate state of album-level checkboxes
+ function AlbumCheckbox({ albumName }) {
+ const checkboxRef = useRef(null);
+ const allTracks = tracksByAlbum[albumName] || [];
+ const selected = selectedTracks[albumName] || [];
+
+ useEffect(() => {
+ if (!checkboxRef.current) return;
+ const isChecked = selected.length === allTracks.length && allTracks.length > 0;
+ const isIndeterminate = selected.length > 0 && selected.length < allTracks.length;
+ checkboxRef.current.checked = isChecked;
+ checkboxRef.current.indeterminate = isIndeterminate;
+ }, [selected, allTracks]);
+
+ const onChange = () => {
+ const allSelected = selected.length === allTracks.length;
+ setSelectedTracks((prev) => ({
+ ...prev,
+ [albumName]: allSelected ? [] : [...allTracks],
+ }));
+ };
+
+ return (
+ e.stopPropagation()} // <-- Prevent accordion toggle
+ className="cursor-pointer"
+ aria-label={`Select all tracks for album ${albumName}`}
+ />
+ );
+ }
+
+ const handleSubmit = async () => {
+ if (!selectedItem) {
+ toast.error("Please perform a search before submitting.");
+ return;
+ }
+ // TODO: Send request to backend
+ toast.success("Request submitted!");
+ };
+
+ return (
+
+
+
+
+ }
+ >
+
+ {(tracksByAlbum[albumName] || []).map((track) => (
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/src/components/qs2/RequestManagement.jsx b/src/components/qs2/RequestManagement.jsx
new file mode 100644
index 0000000..f43a99f
--- /dev/null
+++ b/src/components/qs2/RequestManagement.jsx
@@ -0,0 +1,333 @@
+import React, { useState, useEffect } from "react";
+import { DataTable } from "primereact/datatable";
+import { Column } from "primereact/column";
+import { Dropdown } from "primereact/dropdown";
+import { Button } from "@mui/joy";
+import { toast } from "react-toastify";
+import { 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",
+ },
+ {
+ id: 2,
+ type: "Album",
+ artist: "Pink Floyd",
+ album: "Dark Side of the Moon",
+ track: "",
+ status: "Completed",
+ },
+ {
+ id: 3,
+ type: "Track",
+ artist: "We Butter The Bread With Butter",
+ album: "Das Album",
+ track: "20 km/h",
+ status: "Failed",
+ },
+ {
+ id: 4,
+ type: "Track",
+ artist: "Chappell Roan",
+ album: "Pink Pony Club",
+ track: "Pink Pony Club",
+ status: "Completed",
+ },
+];
+
+export default function RequestManagement() {
+ const [requests, setRequests] = useState(initialRequests);
+ const [filterType, setFilterType] = useState(null);
+ const [filterStatus, setFilterStatus] = useState(null);
+ const [filteredRequests, setFilteredRequests] = useState(initialRequests);
+
+ 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 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 (
+
+ {rowData.status}
+
+ );
+ };
+
+ const actionBodyTemplate = (rowData) => {
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+
+ Media Request Management
+
+
+
+ ({ label: t, value: t })),
+ ]}
+ onChange={(e) => setFilterType(e.value)}
+ placeholder="Filter by Type"
+ className="min-w-[180px]"
+ />
+ ({ label: s, value: s })),
+ ]}
+ onChange={(e) => setFilterStatus(e.value)}
+ placeholder="Filter by Status"
+ className="min-w-[180px]"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/qs2/new.astro b/src/pages/qs2/new.astro
new file mode 100644
index 0000000..2e31470
--- /dev/null
+++ b/src/pages/qs2/new.astro
@@ -0,0 +1,13 @@
+---
+import MediaRequestForm from "@/components/qs2/MediaRequestForm"
+import Base from "@/layouts/Base.astro";
+import Root from "@/components/AppLayout.jsx";
+import "@styles/MemeGrid.css";
+---
+
+
+
+
+
+
+
diff --git a/src/pages/qs2/requests.astro b/src/pages/qs2/requests.astro
new file mode 100644
index 0000000..ab9b56a
--- /dev/null
+++ b/src/pages/qs2/requests.astro
@@ -0,0 +1,13 @@
+---
+import MediaRequestForm from "@/components/qs2/MediaRequestForm"
+import Base from "@/layouts/Base.astro";
+import Root from "@/components/AppLayout.jsx";
+import "@styles/MemeGrid.css";
+---
+
+