admin dashboard

This commit is contained in:
dimitar committed 2026-01-29 11:27:45 +01:00
1 parent 2ff76ffda5
commit 7c7bb45446
15 files changed
+440 -116

No files matched your search

@@ -0,0 +1,260 @@
import { useLiveBlogs } from '@/queries/live-blogs';
import { useArticles } from '@/queries/articles';
import { Link } from '@tanstack/react-router';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { format } from 'date-fns';
import { mk } from 'date-fns/locale';
export function AdminDashboardComponent() {
const { data: liveBlogsData, isLoading: loadingLiveBlogs } = useLiveBlogs({ limit: 50 });
const { data: articlesData, isLoading: loadingArticles } = useArticles({ limit: 50 });
const liveBlogs = liveBlogsData?.data || [];
const articles = articlesData?.data || [];
const getStatusColor = (status: string) => {
switch (status) {
case 'published':
case 'live':
return 'bg-green-100 text-green-800 border-green-200';
case 'draft':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'archived':
case 'ended':
return 'bg-gray-100 text-gray-800 border-gray-200';
default:
return 'bg-blue-100 text-blue-800 border-blue-200';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'published': return 'Објавено';
case 'draft': return 'Нацрт';
case 'archived': return 'Архивирано';
case 'live': return 'Во живо';
case 'ended': return 'Завршено';
default: return status;
}
};
return (
<div className="py-8 space-y-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">Администраторски панел</h1>
<p className="text-muted-foreground">
Управување со сите написи и live блогови
</p>
</div>
<div className="flex gap-2">
<Button asChild variant="outline">
<Link to="/admin/live-blogs/create">+ Нов Live блог</Link>
</Button>
<Button asChild>
<Link to="/">Назад кон сајтот</Link>
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Live Blogs Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Live блогови</span>
<Badge variant="outline" className="ml-2">
{liveBlogs.length || 0}
</Badge>
</CardTitle>
<CardDescription>
Сите live блогови со статус и датум на креирање
</CardDescription>
</CardHeader>
<CardContent>
{loadingLiveBlogs ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="mt-2 text-sm text-muted-foreground">Вчитување...</p>
</div>
) : liveBlogs.length === 0 ? (
<div className="text-center py-8 border rounded-lg">
<p className="text-muted-foreground">Нема live блогови</p>
<Button asChild variant="outline" className="mt-4">
<Link to="/admin/live-blogs/create">Креирај нов live блог</Link>
</Button>
</div>
) : (
<div className="space-y-4">
{liveBlogs.map((blog) => (
<div
key={blog.id}
className="p-4 border rounded-lg hover:bg-accent/50 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Link
to={`/admin/live-blogs/${blog.slug}` as any}
className="font-medium hover:text-primary hover:underline"
>
{blog.title}
</Link>
<Badge variant="outline" className={getStatusColor(blog.status)}>
{getStatusText(blog.status)}
</Badge>
{blog.isPinned && (
<Badge variant="outline" className="bg-yellow-100 text-yellow-800 border-yellow-200">
Закачено
</Badge>
)}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>Слаг: {blog.slug}</span>
<span>•</span>
<span>
Креирано: {format(new Date(blog.createdAt), 'dd MMM yyyy', { locale: mk })}
</span>
<span>•</span>
<span>Прегледи: {blog.viewCount}</span>
</div>
</div>
<div className="flex gap-2 ml-4">
<Button asChild size="sm" variant="outline">
<Link to={`/admin/live-blogs/${blog.slug}` as any}>Уреди</Link>
</Button>
<Button asChild size="sm" variant="ghost">
<Link to={`/live-blogs/${blog.slug}` as any} target="_blank">
Преглед
</Link>
</Button>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Articles Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Написи</span>
<Badge variant="outline" className="ml-2">
{articles.length || 0}
</Badge>
</CardTitle>
<CardDescription>
Сите написи со статус и датум на креирање
</CardDescription>
</CardHeader>
<CardContent>
{loadingArticles ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="mt-2 text-sm text-muted-foreground">Вчитување...</p>
</div>
) : articles.length === 0 ? (
<div className="text-center py-8 border rounded-lg">
<p className="text-muted-foreground">Нема написи</p>
<Button asChild variant="outline" className="mt-4">
<Link to="/">Креирај нов напис</Link>
</Button>
</div>
) : (
<div className="space-y-4">
{articles.map((article) => (
<div
key={article.id}
className="p-4 border rounded-lg hover:bg-accent/50 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Link
to={`/articles/${article.id}` as any}
className="font-medium hover:text-primary hover:underline"
>
{article.title}
</Link>
<Badge variant="outline" className={getStatusColor(article.status)}>
{getStatusText(article.status)}
</Badge>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>Слаг: {article.slug}</span>
<span>•</span>
<span>
Креирано: {format(new Date(article.createdAt), 'dd MMM yyyy', { locale: mk })}
</span>
<span>•</span>
<span>Прегледи: {article.views}</span>
</div>
{article.excerpt && (
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
{article.excerpt}
</p>
)}
</div>
<div className="flex gap-2 ml-4">
<Button asChild size="sm" variant="outline">
<Link to={`/articles/${article.id}` as any}>Уреди</Link>
</Button>
<Button asChild size="sm" variant="ghost">
<Link to={`/articles/${article.id}` as any} target="_blank">
Преглед
</Link>
</Button>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">
{liveBlogs.filter(b => b.status === 'live').length || 0}
</div>
<p className="text-sm text-muted-foreground">Активни live блогови</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">
{articles.filter(a => a.status === 'published').length || 0}
</div>
<p className="text-sm text-muted-foreground">Објавени написи</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">
{liveBlogs.filter(b => b.isPinned).length || 0}
</div>
<p className="text-sm text-muted-foreground">Закачени live блогови</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">
{(liveBlogs.reduce((sum, b) => sum + b.viewCount, 0) || 0) +
(articles.reduce((sum, a) => sum + a.views, 0) || 0)}
</div>
<p className="text-sm text-muted-foreground">Вкупно прегледи</p>
</CardContent>
</Card>
</div>
</div>
);
}