214 lines
7.0 KiB
TypeScript
214 lines
7.0 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Link } from '@tanstack/react-router';
|
|
import { useActiveLiveBlogs } from '@/queries/live-blogs';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Play, Pause, ChevronRight, Radio } from 'lucide-react';
|
|
|
|
interface LiveBlogTickerProps {
|
|
className?: string;
|
|
maxItems?: number;
|
|
autoScroll?: boolean;
|
|
scrollSpeed?: number;
|
|
}
|
|
|
|
export function LiveBlogTicker({
|
|
className = '',
|
|
maxItems = 10,
|
|
autoScroll = true,
|
|
scrollSpeed = 30
|
|
}: LiveBlogTickerProps) {
|
|
const { data: activeBlogs, isLoading } = useActiveLiveBlogs();
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
const [scrollPosition, setScrollPosition] = useState(0);
|
|
const tickerRef = useRef<HTMLDivElement>(null);
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
const animationRef = useRef<number | undefined>(undefined);
|
|
|
|
const [totalWidth, setTotalWidth] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (contentRef.current) {
|
|
const width = contentRef.current.scrollWidth;
|
|
setTotalWidth(width);
|
|
}
|
|
}, [activeBlogs]);
|
|
|
|
useEffect(() => {
|
|
if (!autoScroll || isPaused || !activeBlogs || activeBlogs.length === 0) {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let lastTime: number;
|
|
const animate = (time: number) => {
|
|
if (!lastTime) lastTime = time;
|
|
const delta = time - lastTime;
|
|
lastTime = time;
|
|
|
|
setScrollPosition(prev => {
|
|
let newPos = prev + (scrollSpeed * delta) / 1000;
|
|
|
|
if (newPos > totalWidth) {
|
|
newPos = -(tickerRef.current?.offsetWidth || 0);
|
|
}
|
|
|
|
return newPos;
|
|
});
|
|
|
|
animationRef.current = requestAnimationFrame(animate);
|
|
};
|
|
|
|
animationRef.current = requestAnimationFrame(animate);
|
|
|
|
return () => {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
}
|
|
};
|
|
}, [autoScroll, isPaused, activeBlogs, scrollSpeed, totalWidth]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className={`border-brutal-sm bg-card p-3 ${className}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="animate-pulse h-4 w-24 bg-muted rounded"></div>
|
|
</div>
|
|
<div className="animate-pulse h-8 w-20 bg-muted rounded"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!activeBlogs || activeBlogs.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const displayBlogs = activeBlogs.slice(0, maxItems);
|
|
|
|
const handlePauseToggle = () => {
|
|
setIsPaused(!isPaused);
|
|
};
|
|
|
|
const handleBlogClick = () => {
|
|
setScrollPosition(0);
|
|
};
|
|
|
|
return (
|
|
<div className={`border-brutal-sm bg-card overflow-hidden ${className}`}>
|
|
<div className="flex items-center justify-between px-4 py-2 border-b-2 border-foreground/10 bg-secondary">
|
|
<div className="flex items-center gap-3">
|
|
<Radio className="w-4 h-4 text-accent animate-pulse" />
|
|
<span className="font-body text-sm font-bold uppercase tracking-wider">LIVE</span>
|
|
<span className="px-2 py-0.5 bg-accent text-foreground text-xs font-body font-bold uppercase">
|
|
{activeBlogs.length}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{autoScroll && (
|
|
<Button
|
|
variant="brutal"
|
|
size="sm"
|
|
onClick={handlePauseToggle}
|
|
className="h-7 px-2 text-xs"
|
|
>
|
|
{isPaused ? (
|
|
<Play className="w-3 h-3" />
|
|
) : (
|
|
<Pause className="w-3 h-3" />
|
|
)}
|
|
</Button>
|
|
)}
|
|
|
|
<Link to="/live-blogs">
|
|
<Button variant="brutal" size="sm" className="h-7 px-2 text-xs">
|
|
Сите
|
|
<ChevronRight className="w-3 h-3 ml-1" />
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
ref={tickerRef}
|
|
className="relative h-12 overflow-hidden"
|
|
>
|
|
<div
|
|
ref={contentRef}
|
|
className="absolute top-0 left-0 h-full flex items-center whitespace-nowrap"
|
|
style={{
|
|
transform: `translateX(-${scrollPosition}px)`,
|
|
transition: isPaused ? 'transform 0.3s ease' : 'none'
|
|
}}
|
|
>
|
|
{displayBlogs.map((blog) => (
|
|
<React.Fragment key={blog.id}>
|
|
<Link
|
|
to="/live-blogs/$slug"
|
|
params={{ slug: blog.slug }}
|
|
onClick={handleBlogClick}
|
|
className="inline-flex items-center gap-2 px-4 py-1 border-r-2 border-foreground/10 hover:bg-accent transition-colors group"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative">
|
|
<div className="w-2 h-2 bg-accent rounded-full animate-pulse"></div>
|
|
</div>
|
|
<span className="font-body text-sm font-medium group-hover:text-foreground transition-colors">
|
|
{blog.title}
|
|
</span>
|
|
{blog.updates && blog.updates.length > 0 && (
|
|
<span className="px-2 py-0.5 border border-foreground bg-background text-xs font-body">
|
|
{blog.updates.length}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
</React.Fragment>
|
|
))}
|
|
|
|
{displayBlogs.map((blog) => (
|
|
<React.Fragment key={`${blog.id}-dup`}>
|
|
<Link
|
|
to="/live-blogs/$slug"
|
|
params={{ slug: blog.slug }}
|
|
onClick={handleBlogClick}
|
|
className="inline-flex items-center gap-2 px-4 py-1 border-r-2 border-foreground/10 hover:bg-accent transition-colors group"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative">
|
|
<div className="w-2 h-2 bg-accent rounded-full animate-pulse"></div>
|
|
</div>
|
|
<span className="font-body text-sm font-medium group-hover:text-foreground transition-colors">
|
|
{blog.title}
|
|
</span>
|
|
{blog.updates && blog.updates.length > 0 && (
|
|
<span className="px-2 py-0.5 border border-foreground bg-background text-xs font-body">
|
|
{blog.updates.length}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{autoScroll && totalWidth > 0 && (
|
|
<div className="px-4 pb-2">
|
|
<div className="h-1 bg-foreground/10">
|
|
<div
|
|
className="h-full bg-accent transition-all duration-300"
|
|
style={{
|
|
width: `${Math.min(100, (scrollPosition / totalWidth) * 100 * 2)}%`
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|