This commit is contained in:
2025-12-19 13:45:30 -05:00
parent 823c8b52b3
commit 7b3862c43a
21 changed files with 2405 additions and 373 deletions

View File

@@ -46,7 +46,8 @@ export interface RootProps {
export default function Root({ child, user = undefined, ...props }: RootProps): React.ReactElement {
window.toast = toast;
const theme = document.documentElement.getAttribute("data-theme")
const theme = document.documentElement.getAttribute("data-theme") ?? null;
const toastTheme = theme ?? undefined;
const loggedIn = props.loggedIn ?? Boolean(user);
usePrimeReactThemeSwitcher(theme);
// Avoid adding the Player island for subsite requests. We expose a
@@ -81,7 +82,7 @@ export default function Root({ child, user = undefined, ...props }: RootProps):
let mounted = true;
if (wantPlayer) {
if (import.meta.env.DEV) { try { console.debug('[AppLayout] dynamic-import: requesting AudioPlayer'); } catch (e) { } }
import('./AudioPlayer.jsx')
import('./Radio.js')
.then((mod) => {
if (!mounted) return;
// set the component factory
@@ -102,7 +103,7 @@ export default function Root({ child, user = undefined, ...props }: RootProps):
return (
<PrimeReactProvider>
<CustomToastContainer
theme={theme}
theme={toastTheme}
newestOnTop={true}
closeOnClick={true} />
<JoyUIRootIsland>
@@ -114,22 +115,22 @@ export default function Root({ child, user = undefined, ...props }: RootProps):
Work in progress... bugs are to be expected.
</Alert> */}
{child == "LoginPage" && (<LoginPage {...props} loggedIn={loggedIn} />)}
{child == "LyricSearch" && (<LyricSearch {...props} client:only="react" />)}
{child == "LyricSearch" && (<LyricSearch />)}
{child == "Player" && !isSubsite && PlayerComp && (
<Suspense fallback={null}>
<PlayerComp client:only="react" user={user} />
<PlayerComp user={user} />
</Suspense>
)}
{child == "Memes" && <Memes client:only="react" />}
{child == "Memes" && <Memes />}
{child == "DiscordLogs" && (
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
<DiscordLogs client:only="react" />
<DiscordLogs />
</Suspense>
)}
{child == "qs2.MediaRequestForm" && <MediaRequestForm client:only="react" />}
{child == "qs2.RequestManagement" && <RequestManagement client:only="react" />}
{child == "ReqForm" && <ReqForm {...props} client:only="react" />}
{child == "Lighting" && <Lighting key={window.location.pathname + Math.random()} client:only="react" />}
{child == "qs2.MediaRequestForm" && <MediaRequestForm />}
{child == "qs2.RequestManagement" && <RequestManagement />}
{child == "ReqForm" && <ReqForm />}
{child == "Lighting" && <Lighting key={window.location.pathname + Math.random()} />}
</JoyUIRootIsland>
</PrimeReactProvider>
);

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,13 @@
* This is a minimal version that avoids importing from shared modules
* that would pull in heavy CSS (AppLayout, Components, etc.)
*/
import React, { Suspense, lazy, ReactNode } from 'react';
import React, { Suspense, lazy } from 'react';
import type { ReactNode } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import { CssVarsProvider } from "@mui/joy";
import { CacheProvider, EmotionCache } from "@emotion/react";
import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
import type { EmotionCache } from "@emotion/cache";
import { PrimeReactProvider } from "primereact/api";
// Import only minimal CSS - no theme CSS, no primeicons

File diff suppressed because it is too large Load Diff

View File

@@ -5,19 +5,46 @@ import { Button } from "@mui/joy";
import { AutoComplete } from "primereact/autocomplete";
import { InputText } from "primereact/inputtext";
declare global {
interface Window {
_t?: string;
}
}
type MediaType = 'movie' | 'tv' | string;
interface SearchItem {
label: string;
year?: string;
mediaType?: MediaType;
poster_path?: string | null;
overview?: string;
title?: string;
type?: string;
requester?: string;
}
interface SubmittedRequest {
title: string;
year: string;
type: string;
requester: string;
poster_path?: string | null;
}
export default function ReqForm() {
const [type, setType] = useState("");
const [title, setTitle] = useState("");
const [year, setYear] = useState("");
const [requester, setRequester] = useState("");
const [selectedItem, setSelectedItem] = useState(null);
const [selectedOverview, setSelectedOverview] = useState("");
const [selectedTitle, setSelectedTitle] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [suggestions, setSuggestions] = useState([]);
const [posterLoading, setPosterLoading] = useState(true);
const [submittedRequest, setSubmittedRequest] = useState(null); // Track successful submission
const [csrfToken, setCsrfToken] = useState(null);
const [type, setType] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [year, setYear] = useState<string>("");
const [requester, setRequester] = useState<string>("");
const [selectedItem, setSelectedItem] = useState<SearchItem | null>(null);
const [selectedOverview, setSelectedOverview] = useState<string>("");
const [selectedTitle, setSelectedTitle] = useState<string>("");
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [suggestions, setSuggestions] = useState<SearchItem[]>([]);
const [posterLoading, setPosterLoading] = useState<boolean>(true);
const [submittedRequest, setSubmittedRequest] = useState<SubmittedRequest | null>(null); // Track successful submission
const [csrfToken, setCsrfToken] = useState<string | null>(null);
// Get CSRF token from window global on mount
useEffect(() => {
@@ -36,7 +63,7 @@ export default function ReqForm() {
}
}, [title, selectedTitle, selectedOverview, selectedItem, type]);
const searchTitles = async (event) => {
const searchTitles = async (event: { query: string }) => {
const query = event.query;
if (query.length < 2) {
setSuggestions([]);
@@ -48,7 +75,7 @@ export default function ReqForm() {
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
const data: SearchItem[] = await response.json();
setSuggestions(data);
} catch (error) {
console.error('Error fetching suggestions:', error);
@@ -56,7 +83,7 @@ export default function ReqForm() {
}
};
const handleSubmit = async (e) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!title.trim()) {
toast.error("Please fill in the required fields.");
@@ -101,7 +128,7 @@ export default function ReqForm() {
year,
type,
requester,
poster_path: selectedItem?.poster_path,
poster_path: selectedItem?.poster_path ?? null,
});
} catch (error) {
console.error('Submission error:', error);
@@ -126,13 +153,13 @@ export default function ReqForm() {
const attachScrollFix = () => {
setTimeout(() => {
const panel = document.querySelector(".p-autocomplete-panel");
const items = panel?.querySelector(".p-autocomplete-items");
const panel = document.querySelector<HTMLElement>(".p-autocomplete-panel");
const items = panel?.querySelector<HTMLElement>(".p-autocomplete-items");
if (items) {
items.style.maxHeight = "200px";
items.style.overflowY = "auto";
items.style.overscrollBehavior = "contain";
const wheelHandler = (e) => {
const wheelHandler = (e: WheelEvent) => {
const delta = e.deltaY;
const atTop = items.scrollTop === 0;
const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight;
@@ -148,7 +175,7 @@ export default function ReqForm() {
}, 0);
};
const formatMediaType = (mediaTypeValue) => {
const formatMediaType = (mediaTypeValue: MediaType | undefined) => {
if (!mediaTypeValue) return "";
if (mediaTypeValue === "tv") return "TV Series";
if (mediaTypeValue === "movie") return "Movie";
@@ -239,16 +266,17 @@ export default function ReqForm() {
delay={300}
onChange={(e) => {
// Handle both string input and object selection
const val = e.target?.value ?? e.value;
setTitle(typeof val === 'string' ? val : val?.label || '');
const val = (e as any).target?.value ?? e.value;
setTitle(typeof val === 'string' ? val : (val as SearchItem | undefined)?.label || '');
}}
onSelect={(e) => {
setType(e.value.mediaType === 'tv' ? 'tv' : 'movie');
setTitle(e.value.label);
setSelectedTitle(e.value.label);
setSelectedItem(e.value);
if (e.value.year) setYear(e.value.year);
setSelectedOverview(e.value.overview || "");
onSelect={(e: { value: SearchItem }) => {
const item = e.value;
setType(item.mediaType === 'tv' ? 'tv' : 'movie');
setTitle(item.label);
setSelectedTitle(item.label);
setSelectedItem(item);
if (item.year) setYear(item.year);
setSelectedOverview(item.overview || "");
}}
placeholder="Enter movie or TV title"
title="Enter movie or TV show title"