- Change date format from 'short' to 'long' month names in Macedonian - Translate 'views' to 'прегледи' across all components - Translate 'shares' to 'споделувања' - Translate 'updates' to 'ажурирања' - Translate 'By' to 'Од' for author attribution - Translate 'Back to articles' to 'Назад кон вести' - Translate archive page headers to Macedonian - Translate auto-scroll button text to Macedonian - Translate connection status to Macedonian - Add fallback to 0 for undefined view counts (|| 0)
239 lines
7.7 KiB
TypeScript
239 lines
7.7 KiB
TypeScript
import { Link } from '@tanstack/react-router';
|
||
import { usePinnedLiveBlogs } from '@/queries/live-blogs';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import type { LiveBlog } from '@/lib/api';
|
||
import {
|
||
Clock,
|
||
MessageSquare,
|
||
Eye,
|
||
Pin,
|
||
Play,
|
||
Square,
|
||
ChevronRight
|
||
} from 'lucide-react';
|
||
|
||
interface PinnedLiveBlogSidebarProps {
|
||
className?: string;
|
||
maxItems?: number;
|
||
}
|
||
|
||
export function PinnedLiveBlogSidebar({
|
||
className = '',
|
||
maxItems = 3
|
||
}: PinnedLiveBlogSidebarProps) {
|
||
const { data: pinnedBlogs, isLoading, error } = usePinnedLiveBlogs();
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<Card className={className}>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg flex items-center gap-2">
|
||
<Pin className="w-4 h-4" />
|
||
Во живо
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
{[1, 2, 3].map((i) => (
|
||
<div key={i} className="animate-pulse space-y-2">
|
||
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||
<div className="h-3 bg-muted rounded w-1/2"></div>
|
||
<div className="h-2 bg-muted rounded w-1/4"></div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<Card className={className}>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg flex items-center gap-2">
|
||
<Pin className="w-4 h-4" />
|
||
Live Coverage
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
{[1, 2, 3].map((i) => (
|
||
<div key={i} className="animate-pulse space-y-2">
|
||
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||
<div className="h-3 bg-muted rounded w-1/2"></div>
|
||
<div className="h-2 bg-muted rounded w-1/4"></div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<Card className={className}>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg flex items-center gap-2">
|
||
<Pin className="w-4 h-4" />
|
||
Live Coverage
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-sm text-muted-foreground text-center py-4">
|
||
Error loading live coverage
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
const displayBlogs = (pinnedBlogs || []).slice(0, maxItems);
|
||
|
||
const getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'live': return 'bg-green-100 text-green-800 border-green-200';
|
||
case 'ended': return 'bg-red-100 text-red-800 border-red-200';
|
||
default: return 'bg-gray-100 text-gray-800 border-gray-200';
|
||
}
|
||
};
|
||
|
||
const getStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'live': return <Play className="w-3 h-3" />;
|
||
case 'ended': return <Square className="w-3 h-3" />;
|
||
default: return null;
|
||
}
|
||
};
|
||
|
||
const formatTime = (dateString: string) => {
|
||
const date = new Date(dateString);
|
||
const now = new Date();
|
||
const diffMs = now.getTime() - date.getTime();
|
||
const diffMins = Math.floor(diffMs / 60000);
|
||
|
||
if (diffMins < 60) {
|
||
return `${diffMins}m ago`;
|
||
} else if (diffMins < 1440) {
|
||
return `${Math.floor(diffMins / 60)}h ago`;
|
||
} else {
|
||
return `${Math.floor(diffMins / 1440)}d ago`;
|
||
}
|
||
};
|
||
|
||
const getLatestUpdate = (blog: LiveBlog) => {
|
||
if (!blog.updates || blog.updates.length === 0) return null;
|
||
return blog.updates[blog.updates.length - 1];
|
||
};
|
||
|
||
return (
|
||
<Card className={className}>
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-lg flex items-center gap-2">
|
||
<Pin className="w-4 h-4" />
|
||
Live Coverage
|
||
</CardTitle>
|
||
<Badge variant="outline" className="text-xs">
|
||
{(pinnedBlogs || []).length} pinned
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<CardContent className="pt-0">
|
||
{displayBlogs.length === 0 ? (
|
||
<div className="py-6 text-center">
|
||
<p className="text-sm text-muted-foreground">
|
||
No pinned live blogs at the moment
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
Check back later for live coverage
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="space-y-4">
|
||
{displayBlogs.map((blog) => {
|
||
const latestUpdate = getLatestUpdate(blog);
|
||
|
||
return (
|
||
<Link
|
||
key={blog.id}
|
||
to="/live-blogs/$slug"
|
||
params={{ slug: blog.slug }}
|
||
className="block p-3 rounded-lg border hover:bg-accent/50 transition-colors group"
|
||
>
|
||
<div className="flex items-start justify-between mb-2">
|
||
<div className="flex-1">
|
||
<h4 className="font-medium text-sm mb-1 group-hover:text-primary transition-colors line-clamp-2">
|
||
{blog.title}
|
||
</h4>
|
||
{blog.description && (
|
||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||
{blog.description}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<Badge
|
||
className={`${getStatusColor(blog.status)} text-xs`}
|
||
variant="outline"
|
||
>
|
||
<div className="flex items-center gap-1">
|
||
{getStatusIcon(blog.status)}
|
||
{blog.status}
|
||
</div>
|
||
</Badge>
|
||
</div>
|
||
|
||
{/* Latest update preview */}
|
||
{latestUpdate && (
|
||
<div className="mt-2 p-2 bg-muted/50 rounded text-xs">
|
||
<div className="flex items-center gap-1 text-muted-foreground mb-1">
|
||
<Clock className="w-3 h-3" />
|
||
<span>{formatTime(latestUpdate.createdAt)}</span>
|
||
</div>
|
||
<p className="line-clamp-2">{latestUpdate.content}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Stats */}
|
||
<div className="mt-2 flex items-center gap-3 text-xs text-muted-foreground">
|
||
<div className="flex items-center gap-1">
|
||
<MessageSquare className="w-3 h-3" />
|
||
<span>{blog.updates?.length || 0} ажурирања</span>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<Eye className="w-3 h-3" />
|
||
<span>{blog.viewCount || 0} прегледи</span>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* View all button */}
|
||
{(pinnedBlogs || []).length > maxItems && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="w-full mt-4"
|
||
asChild
|
||
>
|
||
<Link to="/live-blogs">
|
||
View all pinned blogs
|
||
<ChevronRight className="w-3 h-3 ml-1" />
|
||
</Link>
|
||
</Button>
|
||
)}
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|