dicking around
This commit is contained in:
@@ -250,3 +250,4 @@ Custom
|
|||||||
.d-light > * {
|
.d-light > * {
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,32 +7,36 @@ import { PrimeReactProvider } from "primereact/api";
|
|||||||
import { usePrimeReactThemeSwitcher } from '@/hooks/usePrimeReactThemeSwitcher.jsx';
|
import { usePrimeReactThemeSwitcher } from '@/hooks/usePrimeReactThemeSwitcher.jsx';
|
||||||
import CustomToastContainer from '../components/ToastProvider.jsx';
|
import CustomToastContainer from '../components/ToastProvider.jsx';
|
||||||
import LyricSearch from './LyricSearch.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/themes/bootstrap4-light-blue/theme.css';
|
||||||
import 'primereact/resources/primereact.min.css';
|
import 'primereact/resources/primereact.min.css';
|
||||||
|
|
||||||
export default function Root({child}) {
|
export default function Root({ child }) {
|
||||||
window.toast = toast;
|
window.toast = toast;
|
||||||
const theme = document.documentElement.getAttribute("data-theme")
|
const theme = document.documentElement.getAttribute("data-theme")
|
||||||
usePrimeReactThemeSwitcher(theme);
|
usePrimeReactThemeSwitcher(theme);
|
||||||
// console.log(opts.children);
|
// console.log(opts.children);
|
||||||
return (
|
return (
|
||||||
<PrimeReactProvider>
|
<PrimeReactProvider>
|
||||||
<CustomToastContainer
|
<CustomToastContainer
|
||||||
theme={theme}
|
theme={theme}
|
||||||
newestOnTop={true}
|
newestOnTop={true}
|
||||||
closeOnClick={true}/>
|
closeOnClick={true} />
|
||||||
<JoyUIRootIsland>
|
<JoyUIRootIsland>
|
||||||
{/* <Alert
|
{/* <Alert
|
||||||
className="alert"
|
className="alert"
|
||||||
startDecorator={<WarningIcon />}
|
startDecorator={<WarningIcon />}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
color="danger">
|
color="danger">
|
||||||
Work in progress... bugs are to be expected.
|
Work in progress... bugs are to be expected.
|
||||||
</Alert> */}
|
</Alert> */}
|
||||||
{child == "LyricSearch" && (<LyricSearch client:only="react" />)}
|
{child == "LyricSearch" && (<LyricSearch client:only="react" />)}
|
||||||
{child == "Player" && (<Player client:only="react" />)}
|
{child == "Player" && (<Player client:only="react" />)}
|
||||||
{child == "Memes" && <Memes client:only="react" />}
|
{child == "Memes" && <Memes client:only="react" />}
|
||||||
</JoyUIRootIsland>
|
{child == "qs2.MediaRequestForm" && <MediaRequestForm client:only="react" />}
|
||||||
|
{child == "qs2.RequestManagement" && <RequestManagement client:only="react" />}
|
||||||
|
</JoyUIRootIsland>
|
||||||
</PrimeReactProvider>
|
</PrimeReactProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
25
src/components/qs2/BreadcrumbNav.jsx
Normal file
25
src/components/qs2/BreadcrumbNav.jsx
Normal file
@@ -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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
229
src/components/qs2/MediaRequestForm.jsx
Normal file
229
src/components/qs2/MediaRequestForm.jsx
Normal file
@@ -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 (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
ref={checkboxRef}
|
||||||
|
onChange={onChange}
|
||||||
|
onClick={(e) => 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 (
|
||||||
|
<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">
|
||||||
|
<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={artist}
|
||||||
|
onChange={(e) => setArtist(e.target.value)}
|
||||||
|
placeholder="Artist"
|
||||||
|
/>
|
||||||
|
{(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" ? album : track}
|
||||||
|
onChange={(e) =>
|
||||||
|
type === "album" ? setAlbum(e.target.value) : setTrack(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder={type === "album" ? "Album" : "Track"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button onClick={handleSearch}>Search</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type === "artist" && albums.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Accordion multiple className="mt-4">
|
||||||
|
{albums.map((albumName) => (
|
||||||
|
<AccordionTab
|
||||||
|
key={albumName}
|
||||||
|
header={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlbumCheckbox albumName={albumName} />
|
||||||
|
<span>{albumName}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{(tracksByAlbum[albumName] || []).map((track) => (
|
||||||
|
<label key={track} className="flex items-center gap-3">
|
||||||
|
<Checkbox
|
||||||
|
inputId={`${albumName}-${track}`}
|
||||||
|
checked={selectedTracks[albumName]?.includes(track)}
|
||||||
|
onChange={() => toggleTrack(albumName, track)}
|
||||||
|
/>
|
||||||
|
<span>{track}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AccordionTab>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={handleSubmit} color="primary" className="mt-4">
|
||||||
|
Submit Request
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
333
src/components/qs2/RequestManagement.jsx
Normal file
333
src/components/qs2/RequestManagement.jsx
Normal file
@@ -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 (
|
||||||
|
<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;
|
||||||
|
}`}</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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
src/pages/qs2/new.astro
Normal file
13
src/pages/qs2/new.astro
Normal file
@@ -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";
|
||||||
|
---
|
||||||
|
<Base>
|
||||||
|
<section>
|
||||||
|
<div class="prose prose-neutral dark:prose-invert">
|
||||||
|
<Root child="qs2.MediaRequestForm" client:only="react">
|
||||||
|
</Root>
|
||||||
|
</section>
|
||||||
|
</Base>
|
13
src/pages/qs2/requests.astro
Normal file
13
src/pages/qs2/requests.astro
Normal file
@@ -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";
|
||||||
|
---
|
||||||
|
<Base>
|
||||||
|
<section>
|
||||||
|
<div class="prose prose-neutral dark:prose-invert">
|
||||||
|
<Root child="qs2.RequestManagement" client:only="react">
|
||||||
|
</Root>
|
||||||
|
</section>
|
||||||
|
</Base>
|
Reference in New Issue
Block a user