Redirect user to requests page after successful media request submission
js->ts
This commit is contained in:
@@ -6,10 +6,10 @@
|
||||
}
|
||||
|
||||
: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-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 */
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ body {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.3s ease;
|
||||
max-height: 125px;
|
||||
max-height: 220px;
|
||||
max-width: 100%;
|
||||
overflow-y: auto;
|
||||
margin-top: 1rem;
|
||||
@@ -277,6 +277,9 @@ body {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #999 transparent;
|
||||
scroll-padding-bottom: 1rem;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.lrc-text.empty {
|
||||
@@ -297,9 +300,10 @@ body {
|
||||
|
||||
.lrc-line.active {
|
||||
color: var(--lrc-active-color);
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
text-shadow: var(--lrc-active-shadow);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.lrc-line:hover {
|
||||
|
||||
@@ -1,27 +1,49 @@
|
||||
import React, { Suspense, lazy, useState, useMemo, useEffect } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import Memes from './Memes.jsx';
|
||||
import Lighting from './Lighting.jsx';
|
||||
import Memes from './Memes.tsx';
|
||||
import Lighting from './Lighting.tsx';
|
||||
import { toast } from 'react-toastify';
|
||||
import { JoyUIRootIsland } from './Components.jsx';
|
||||
import { JoyUIRootIsland } from './Components.tsx';
|
||||
import { PrimeReactProvider } from "primereact/api";
|
||||
import { usePrimeReactThemeSwitcher } from '@/hooks/usePrimeReactThemeSwitcher.jsx';
|
||||
import CustomToastContainer from '../components/ToastProvider.jsx';
|
||||
import { usePrimeReactThemeSwitcher } from '@/hooks/usePrimeReactThemeSwitcher.tsx';
|
||||
import CustomToastContainer from '../components/ToastProvider.tsx';
|
||||
import 'primereact/resources/themes/bootstrap4-light-blue/theme.css';
|
||||
import 'primereact/resources/primereact.min.css';
|
||||
import "primeicons/primeicons.css";
|
||||
|
||||
const LoginPage = lazy(() => import('./Login.jsx'));
|
||||
const LoginPage = lazy(() => import('./Login.tsx'));
|
||||
const LyricSearch = lazy(() => import('./LyricSearch'));
|
||||
const MediaRequestForm = lazy(() => import('./TRip/MediaRequestForm.jsx'));
|
||||
const RequestManagement = lazy(() => import('./TRip/RequestManagement.jsx'));
|
||||
const DiscordLogs = lazy(() => import('./DiscordLogs.jsx'));
|
||||
const MediaRequestForm = lazy(() => import('./TRip/MediaRequestForm.tsx'));
|
||||
const RequestManagement = lazy(() => import('./TRip/RequestManagement.tsx'));
|
||||
const DiscordLogs = lazy(() => import('./DiscordLogs.tsx'));
|
||||
// NOTE: Player is intentionally NOT imported at module initialization.
|
||||
// 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
|
||||
// prevents bundling the player island into pages that are explicitly
|
||||
// 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 {
|
||||
interface Window {
|
||||
@@ -55,15 +77,18 @@ export default function Root({ child, user = undefined, ...props }: RootProps):
|
||||
// don't need to pass guards.
|
||||
const isSubsite = typeof document !== 'undefined' && document.documentElement.getAttribute('data-subsite') === 'true';
|
||||
// Log when the active child changes (DEV only)
|
||||
const devLogsEnabled = import.meta.env.DEV && import.meta.env.VITE_DEV_LOGS === '1';
|
||||
|
||||
useEffect(() => {
|
||||
if (!devLogsEnabled) return;
|
||||
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')}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// no-op
|
||||
}
|
||||
}, [child]);
|
||||
}, [child, devLogsEnabled]);
|
||||
|
||||
// Only initialize the lazy player when this is NOT a subsite and the
|
||||
// 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">
|
||||
Work in progress... bugs are to be expected.
|
||||
</Alert> */}
|
||||
{child == "LoginPage" && (<LoginPage {...props} loggedIn={loggedIn} />)}
|
||||
{child == "LyricSearch" && (<LyricSearch />)}
|
||||
{child == "LoginPage" && (
|
||||
<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 && (
|
||||
<Suspense fallback={null}>
|
||||
<PlayerComp user={user} />
|
||||
</Suspense>
|
||||
<LazyBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<PlayerComp user={user} />
|
||||
</Suspense>
|
||||
</LazyBoundary>
|
||||
)}
|
||||
{child == "Memes" && (
|
||||
<LazyBoundary>
|
||||
<Memes />
|
||||
</LazyBoundary>
|
||||
)}
|
||||
{child == "Memes" && <Memes />}
|
||||
{child == "DiscordLogs" && (
|
||||
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
|
||||
<DiscordLogs />
|
||||
</Suspense>
|
||||
<LazyBoundary>
|
||||
<Suspense fallback={<div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>}>
|
||||
<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>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
|
||||
@@ -44,6 +44,11 @@ export default function Player({ user }: PlayerProps) {
|
||||
// Global CSS now contains the paginator / dialog datatable dark rules.
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!isQueueVisible) return;
|
||||
@@ -249,16 +254,56 @@ export default function Player({ user }: PlayerProps) {
|
||||
|
||||
// Scroll active lyric into view
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const activeElement = document.querySelector('.lrc-line.active');
|
||||
const lyricsContainer = document.querySelector('.lrc-text');
|
||||
if (activeElement && lyricsContainer) {
|
||||
(lyricsContainer as HTMLElement).style.maxHeight = '220px';
|
||||
(lyricsContainer as HTMLElement).style.overflowY = 'auto';
|
||||
activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
const container = lrcContainerRef.current;
|
||||
if (!container || lyrics.length === 0) return;
|
||||
|
||||
const scheduleScroll = () => {
|
||||
// Read ref/DOM inside the callback so we get the updated element after render
|
||||
const activeElement = lrcActiveRef.current || (container.querySelector('.lrc-line.active') as HTMLElement | null);
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -746,10 +791,42 @@ export default function Player({ user }: PlayerProps) {
|
||||
></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) => (
|
||||
<p
|
||||
key={index}
|
||||
ref={index === currentLyricIndex ? (lrcActiveRef as React.RefObject<HTMLParagraphElement>) : undefined}
|
||||
className={`lrc-line text-sm ${index === currentLyricIndex ? "active font-bold" : ""}`}
|
||||
>
|
||||
{lyricObj.line}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -994,6 +994,10 @@ export default function MediaRequestForm() {
|
||||
|
||||
const data = await response.json();
|
||||
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) {
|
||||
console.error(err);
|
||||
toast.error("Failed to submit request.");
|
||||
|
||||
Reference in New Issue
Block a user