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 ( +
+ + +
+
+ + + +
+ +
+ setArtist(e.target.value)} + placeholder="Artist" + /> + {(type === "album" || type === "track") && ( + + type === "album" ? setAlbum(e.target.value) : setTrack(e.target.value) + } + placeholder={type === "album" ? "Album" : "Track"} + /> + )} + +
+ + {type === "artist" && albums.length > 0 && ( + <> + + {albums.map((albumName) => ( + + + {albumName} +
+ } + > +
+ {(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"; +--- + +
+
+ + +
+