Redirect user to requests page after successful media request submission

js->ts
This commit is contained in:
2025-12-24 09:55:08 -05:00
parent bb71f662ed
commit 256d5d9c7f
5 changed files with 186 additions and 1389 deletions

View File

@@ -6,10 +6,10 @@
} }
:root { :root {
--lrc-text-color: #333; /* darker text */ --lrc-text-color: #666; /* muted text for inactive lines */
--lrc-bg-color: rgba(0, 0, 0, 0.05); --lrc-bg-color: rgba(0, 0, 0, 0.05);
--lrc-active-color: #000; /* bold black for active */ --lrc-active-color: #000; /* bold black for active */
--lrc-active-shadow: none; /* no glow in light mode */ --lrc-active-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); /* subtle shadow in light mode */
--lrc-hover-color: #005fcc; /* darker blue hover */ --lrc-hover-color: #005fcc; /* darker blue hover */
} }
@@ -259,7 +259,7 @@ body {
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
max-height: 125px; max-height: 220px;
max-width: 100%; max-width: 100%;
overflow-y: auto; overflow-y: auto;
margin-top: 1rem; margin-top: 1rem;
@@ -277,6 +277,9 @@ body {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #999 transparent; scrollbar-color: #999 transparent;
scroll-padding-bottom: 1rem; scroll-padding-bottom: 1rem;
overscroll-behavior: contain;
touch-action: pan-y;
scroll-behavior: smooth;
} }
.lrc-text.empty { .lrc-text.empty {
@@ -297,9 +300,10 @@ body {
.lrc-line.active { .lrc-line.active {
color: var(--lrc-active-color); color: var(--lrc-active-color);
font-weight: 600; font-weight: 700;
font-size: 0.8rem; font-size: 0.95rem;
text-shadow: var(--lrc-active-shadow); text-shadow: var(--lrc-active-shadow);
transform: scale(1.02);
} }
.lrc-line:hover { .lrc-line:hover {

View File

@@ -1,27 +1,49 @@
import React, { Suspense, lazy, useState, useMemo, useEffect } from 'react'; import React, { Suspense, lazy, useState, useMemo, useEffect } from 'react';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import Memes from './Memes.jsx'; import Memes from './Memes.tsx';
import Lighting from './Lighting.jsx'; import Lighting from './Lighting.tsx';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { JoyUIRootIsland } from './Components.jsx'; import { JoyUIRootIsland } from './Components.tsx';
import { PrimeReactProvider } from "primereact/api"; import { PrimeReactProvider } from "primereact/api";
import { usePrimeReactThemeSwitcher } from '@/hooks/usePrimeReactThemeSwitcher.jsx'; import { usePrimeReactThemeSwitcher } from '@/hooks/usePrimeReactThemeSwitcher.tsx';
import CustomToastContainer from '../components/ToastProvider.jsx'; import CustomToastContainer from '../components/ToastProvider.tsx';
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';
import "primeicons/primeicons.css"; import "primeicons/primeicons.css";
const LoginPage = lazy(() => import('./Login.jsx')); const LoginPage = lazy(() => import('./Login.tsx'));
const LyricSearch = lazy(() => import('./LyricSearch')); const LyricSearch = lazy(() => import('./LyricSearch'));
const MediaRequestForm = lazy(() => import('./TRip/MediaRequestForm.jsx')); const MediaRequestForm = lazy(() => import('./TRip/MediaRequestForm.tsx'));
const RequestManagement = lazy(() => import('./TRip/RequestManagement.jsx')); const RequestManagement = lazy(() => import('./TRip/RequestManagement.tsx'));
const DiscordLogs = lazy(() => import('./DiscordLogs.jsx')); const DiscordLogs = lazy(() => import('./DiscordLogs.tsx'));
// NOTE: Player is intentionally NOT imported at module initialization. // NOTE: Player is intentionally NOT imported at module initialization.
// We create the lazy import inside the component at render-time only when // We create the lazy import inside the component at render-time only when
// we are on the main site and the Player island should be rendered. This // we are on the main site and the Player island should be rendered. This
// prevents bundling the player island into pages that are explicitly // prevents bundling the player island into pages that are explicitly
// identified as subsites. // identified as subsites.
const ReqForm = lazy(() => import('./req/ReqForm.jsx')); const ReqForm = lazy(() => import('./req/ReqForm.tsx'));
// Simple error boundary for lazy islands
class LazyBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(err) {
if (import.meta.env.DEV) {
console.error('[AppLayout] lazy island error', err);
}
}
render() {
if (this.state.hasError) {
return <div style={{ padding: '1rem', textAlign: 'center' }}>Something went wrong loading this module.</div>;
}
return this.props.children;
}
}
declare global { declare global {
interface Window { interface Window {
@@ -55,15 +77,18 @@ export default function Root({ child, user = undefined, ...props }: RootProps):
// don't need to pass guards. // don't need to pass guards.
const isSubsite = typeof document !== 'undefined' && document.documentElement.getAttribute('data-subsite') === 'true'; const isSubsite = typeof document !== 'undefined' && document.documentElement.getAttribute('data-subsite') === 'true';
// Log when the active child changes (DEV only) // Log when the active child changes (DEV only)
const devLogsEnabled = import.meta.env.DEV && import.meta.env.VITE_DEV_LOGS === '1';
useEffect(() => { useEffect(() => {
if (!devLogsEnabled) return;
try { try {
if (typeof console !== 'undefined' && typeof document !== 'undefined' && import.meta.env.DEV) { if (typeof console !== 'undefined' && typeof document !== 'undefined') {
console.debug(`[AppLayout] child=${String(child)}, data-subsite=${document.documentElement.getAttribute('data-subsite')}`); console.debug(`[AppLayout] child=${String(child)}, data-subsite=${document.documentElement.getAttribute('data-subsite')}`);
} }
} catch (e) { } catch (e) {
// no-op // no-op
} }
}, [child]); }, [child, devLogsEnabled]);
// Only initialize the lazy player when this is NOT a subsite and the // Only initialize the lazy player when this is NOT a subsite and the
// active child is the Player island. Placing the lazy() call here // active child is the Player island. Placing the lazy() call here
@@ -114,23 +139,59 @@ export default function Root({ child, user = undefined, ...props }: RootProps):
color="danger"> color="danger">
Work in progress... bugs are to be expected. Work in progress... bugs are to be expected.
</Alert> */} </Alert> */}
{child == "LoginPage" && (<LoginPage {...props} loggedIn={loggedIn} />)} {child == "LoginPage" && (
{child == "LyricSearch" && (<LyricSearch />)} <LazyBoundary>
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
<LoginPage {...props} loggedIn={loggedIn} />
</Suspense>
</LazyBoundary>
)}
{child == "LyricSearch" && (
<LazyBoundary>
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
<LyricSearch />
</Suspense>
</LazyBoundary>
)}
{child == "Player" && !isSubsite && PlayerComp && ( {child == "Player" && !isSubsite && PlayerComp && (
<Suspense fallback={null}> <LazyBoundary>
<PlayerComp user={user} /> <Suspense fallback={null}>
</Suspense> <PlayerComp user={user} />
</Suspense>
</LazyBoundary>
)}
{child == "Memes" && (
<LazyBoundary>
<Memes />
</LazyBoundary>
)} )}
{child == "Memes" && <Memes />}
{child == "DiscordLogs" && ( {child == "DiscordLogs" && (
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}> <LazyBoundary>
<DiscordLogs /> <Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
</Suspense> <DiscordLogs />
</Suspense>
</LazyBoundary>
)}
{child == "qs2.MediaRequestForm" && (
<LazyBoundary>
<MediaRequestForm />
</LazyBoundary>
)}
{child == "qs2.RequestManagement" && (
<LazyBoundary>
<RequestManagement />
</LazyBoundary>
)}
{child == "ReqForm" && (
<LazyBoundary>
<ReqForm />
</LazyBoundary>
)}
{child == "Lighting" && (
<LazyBoundary>
<Lighting key={window.location.pathname + Math.random()} />
</LazyBoundary>
)} )}
{child == "qs2.MediaRequestForm" && <MediaRequestForm />}
{child == "qs2.RequestManagement" && <RequestManagement />}
{child == "ReqForm" && <ReqForm />}
{child == "Lighting" && <Lighting key={window.location.pathname + Math.random()} />}
</JoyUIRootIsland> </JoyUIRootIsland>
</PrimeReactProvider> </PrimeReactProvider>
); );

View File

@@ -44,6 +44,11 @@ export default function Player({ user }: PlayerProps) {
// Global CSS now contains the paginator / dialog datatable dark rules. // Global CSS now contains the paginator / dialog datatable dark rules.
const [isQueueVisible, setQueueVisible] = useState(false); const [isQueueVisible, setQueueVisible] = useState(false);
const lrcContainerRef = useRef<HTMLDivElement | null>(null);
const lrcWheelHandlerRef = useRef<((e: WheelEvent) => void) | null>(null);
const lrcScrollTimeout = useRef<number | null>(null);
const lrcScrollRaf = useRef<number | null>(null);
const lrcActiveRef = useRef<HTMLElement | null>(null);
// Mouse wheel scroll fix for queue modal // Mouse wheel scroll fix for queue modal
useEffect(() => { useEffect(() => {
if (!isQueueVisible) return; if (!isQueueVisible) return;
@@ -249,16 +254,56 @@ export default function Player({ user }: PlayerProps) {
// Scroll active lyric into view // Scroll active lyric into view
useEffect(() => { useEffect(() => {
setTimeout(() => { const container = lrcContainerRef.current;
const activeElement = document.querySelector('.lrc-line.active'); if (!container || lyrics.length === 0) return;
const lyricsContainer = document.querySelector('.lrc-text');
if (activeElement && lyricsContainer) { const scheduleScroll = () => {
(lyricsContainer as HTMLElement).style.maxHeight = '220px'; // Read ref/DOM inside the callback so we get the updated element after render
(lyricsContainer as HTMLElement).style.overflowY = 'auto'; const activeElement = lrcActiveRef.current || (container.querySelector('.lrc-line.active') as HTMLElement | null);
activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (!activeElement) return;
// Use getBoundingClientRect for accurate positioning
const containerRect = container.getBoundingClientRect();
const activeRect = activeElement.getBoundingClientRect();
// Calculate where the element is relative to the container's current scroll
const elementTopInContainer = activeRect.top - containerRect.top + container.scrollTop;
// Center the active line in the container
const targetScrollTop = elementTopInContainer - (container.clientHeight / 2) + (activeElement.offsetHeight / 2);
container.scrollTo({ top: Math.max(targetScrollTop, 0), behavior: 'smooth' });
};
// Debounce a tick then align to paint frame so ref is updated after render
if (lrcScrollTimeout.current) window.clearTimeout(lrcScrollTimeout.current);
lrcScrollTimeout.current = window.setTimeout(() => {
if (lrcScrollRaf.current) cancelAnimationFrame(lrcScrollRaf.current);
lrcScrollRaf.current = requestAnimationFrame(scheduleScroll);
}, 16);
const wheelHandler = (e: WheelEvent) => {
const atTop = container.scrollTop === 0;
const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight;
if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
e.preventDefault();
} else {
e.stopPropagation();
} }
}, 0); };
}, [currentLyricIndex, lyrics]);
if (lrcWheelHandlerRef.current) {
container.removeEventListener('wheel', lrcWheelHandlerRef.current as EventListener);
}
lrcWheelHandlerRef.current = wheelHandler;
container.addEventListener('wheel', wheelHandler, { passive: false });
return () => {
if (lrcScrollTimeout.current) window.clearTimeout(lrcScrollTimeout.current);
if (lrcScrollRaf.current) cancelAnimationFrame(lrcScrollRaf.current);
container.removeEventListener('wheel', wheelHandler as EventListener);
};
}, [currentLyricIndex, lyrics.length]);
// Handle station changes: reset and start new stream // Handle station changes: reset and start new stream
useEffect(() => { useEffect(() => {
@@ -746,10 +791,42 @@ export default function Player({ user }: PlayerProps) {
></div> ></div>
</div> </div>
<div className={`lrc-text mt-4 p-4 rounded-lg bg-neutral-100 dark:bg-neutral-800 ${lyrics.length === 0 ? "empty" : ""}`}> <div
ref={lrcContainerRef}
className={`lrc-text mt-4 p-4 rounded-lg bg-neutral-100 dark:bg-neutral-800 ${lyrics.length === 0 ? "empty" : ""}`}
tabIndex={lyrics.length > 0 ? 0 : -1}
aria-label="Lyrics"
onKeyDown={(e) => {
if (!lrcContainerRef.current || lyrics.length === 0) return;
const container = lrcContainerRef.current;
const step = 24;
switch (e.key) {
case 'ArrowDown':
container.scrollBy({ top: step, behavior: 'smooth' });
e.preventDefault();
break;
case 'ArrowUp':
container.scrollBy({ top: -step, behavior: 'smooth' });
e.preventDefault();
break;
case 'Home':
container.scrollTo({ top: 0, behavior: 'smooth' });
e.preventDefault();
break;
case 'End':
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
e.preventDefault();
break;
}
}}
>
{lyrics.length === 0 && (
<p className="lrc-line text-sm text-neutral-400 dark:text-neutral-500">No lyrics available.</p>
)}
{lyrics.map((lyricObj, index) => ( {lyrics.map((lyricObj, index) => (
<p <p
key={index} key={index}
ref={index === currentLyricIndex ? (lrcActiveRef as React.RefObject<HTMLParagraphElement>) : undefined}
className={`lrc-line text-sm ${index === currentLyricIndex ? "active font-bold" : ""}`} className={`lrc-line text-sm ${index === currentLyricIndex ? "active font-bold" : ""}`}
> >
{lyricObj.line} {lyricObj.line}

File diff suppressed because it is too large Load Diff

View File

@@ -994,6 +994,10 @@ export default function MediaRequestForm() {
const data = await response.json(); const data = await response.json();
toast.success(`Request submitted! (${allSelectedIds.length} tracks)`); toast.success(`Request submitted! (${allSelectedIds.length} tracks)`);
// Send the user to the requests page to monitor progress
if (typeof window !== "undefined") {
window.location.href = "/TRip/requests";
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast.error("Failed to submit request."); toast.error("Failed to submit request.");