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

@@ -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}