Redirect user to requests page after successful media request submission
js->ts
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
@@ -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.");
|
||||||
|
|||||||
Reference in New Issue
Block a user