admin dashboard

This commit is contained in:
echo 2026-01-29 11:27:45 +01:00
parent 2ff76ffda5
commit 7c7bb45446
15 changed files with 440 additions and 116 deletions

View File

@ -16,6 +16,7 @@
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.14",
"@tanstack/react-router": "^1.144.0", "@tanstack/react-router": "^1.144.0",
"date-fns": "^4.1.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0"
}, },
@ -3433,6 +3434,16 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",

View File

@ -23,6 +23,7 @@
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.14",
"@tanstack/react-router": "^1.144.0", "@tanstack/react-router": "^1.144.0",
"date-fns": "^4.1.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0"
}, },

View File

@ -22,9 +22,9 @@ export function ArticleTicker() {
<div className="overflow-hidden flex-1 relative"> <div className="overflow-hidden flex-1 relative">
<div className="flex animate-marquee whitespace-nowrap"> <div className="flex animate-marquee whitespace-nowrap">
{articles.map((article, index) => ( {articles.map((article, index) => (
<Link <Link
key={`${article.id}-${index}`} key={`${article.id}-${index}`}
to={`/articles/${article.id}`} to={`/articles/${article.id}` as any}
className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4" className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4"
> >
{article.title || 'No title'} {article.title || 'No title'}
@ -32,9 +32,9 @@ export function ArticleTicker() {
))} ))}
{/* Duplicate for seamless scrolling */} {/* Duplicate for seamless scrolling */}
{articles.map((article, index) => ( {articles.map((article, index) => (
<Link <Link
key={`dup-${article.id}-${index}`} key={`dup-${article.id}-${index}`}
to={`/articles/${article.id}`} to={`/articles/${article.id}` as any}
className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4" className="text-sm text-muted-foreground hover:text-foreground hover:underline inline-block px-4"
> >
{article.title || 'No title'} {article.title || 'No title'}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { import {
useLiveBlog, useLiveBlog,
useDeleteLiveBlogUpdate, useDeleteLiveBlogUpdate,

View File

@ -23,7 +23,7 @@ export function LiveBlogTicker({
const [scrollPosition, setScrollPosition] = useState(0); const [scrollPosition, setScrollPosition] = useState(0);
const tickerRef = useRef<HTMLDivElement>(null); const tickerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<number>(); const animationRef = useRef<number | undefined>(undefined);
// Calculate total width needed for scrolling // Calculate total width needed for scrolling
const [totalWidth, setTotalWidth] = useState(0); const [totalWidth, setTotalWidth] = useState(0);
@ -55,7 +55,7 @@ export function LiveBlogTicker({
// Reset when scrolled past content // Reset when scrolled past content
if (newPos > totalWidth) { if (newPos > totalWidth) {
newPos = -tickerRef.current?.offsetWidth || 0; newPos = -(tickerRef.current?.offsetWidth || 0);
} }
return newPos; return newPos;

View File

@ -1,4 +1,4 @@
import React from 'react';
import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api'; import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { useLiveBlogStream } from '@/hooks/useLiveBlogStream'; import { useLiveBlogStream } from '@/hooks/useLiveBlogStream';
import { useLiveBlog, useLiveBlogUpdates } from '@/queries/live-blogs'; import { useLiveBlog, useLiveBlogUpdates } from '@/queries/live-blogs';
import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api'; import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api';

View File

@ -1,9 +1,9 @@
import React from 'react';
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { usePinnedLiveBlogs } from '@/queries/live-blogs'; import { usePinnedLiveBlogs } from '@/queries/live-blogs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import type { LiveBlog } from '@/lib/api';
import { import {
Clock, Clock,
MessageSquare, MessageSquare,
@ -49,11 +49,49 @@ export function PinnedLiveBlogSidebar({
); );
} }
if (error || !pinnedBlogs || pinnedBlogs.length === 0) { if (isLoading) {
return null; // Don't show sidebar if no pinned blogs 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>
);
} }
const displayBlogs = pinnedBlogs.slice(0, maxItems); 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) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
@ -99,99 +137,100 @@ export function PinnedLiveBlogSidebar({
<Pin className="w-4 h-4" /> <Pin className="w-4 h-4" />
Live Coverage Live Coverage
</CardTitle> </CardTitle>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{pinnedBlogs.length} pinned {(pinnedBlogs || []).length} pinned
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
<div className="space-y-4"> {displayBlogs.length === 0 ? (
{displayBlogs.map((blog) => { <div className="py-6 text-center">
const latestUpdate = getLatestUpdate(blog); <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 ( return (
<Link <Link
key={blog.id} key={blog.id}
to="/live-blogs/$slug" to="/live-blogs/$slug"
params={{ slug: blog.slug }} params={{ slug: blog.slug }}
className="block group" className="block p-3 rounded-lg border hover:bg-accent/50 transition-colors group"
> >
<div className="p-3 rounded-lg border hover:border-primary/50 hover:bg-accent/50 transition-colors"> <div className="flex items-start justify-between mb-2">
{/* Header */} <div className="flex-1">
<div className="flex items-start justify-between mb-2"> <h4 className="font-medium text-sm mb-1 group-hover:text-primary transition-colors line-clamp-2">
<div className="flex-1"> {blog.title}
<h3 className="font-medium text-sm leading-tight group-hover:text-primary transition-colors line-clamp-2"> </h4>
{blog.title} {blog.description && (
</h3> <p className="text-xs text-muted-foreground line-clamp-2">
{blog.description}
</p>
)}
</div>
{blog.description && ( <Badge
<p className="text-xs text-muted-foreground mt-1 line-clamp-2"> className={`${getStatusColor(blog.status)} text-xs`}
{blog.description} variant="outline"
</p> >
)} <div className="flex items-center gap-1">
{getStatusIcon(blog.status)}
{blog.status}
</div>
</Badge>
</div> </div>
<Badge {/* Latest update preview */}
className={getStatusColor(blog.status)} {latestUpdate && (
variant="outline" <div className="mt-2 p-2 bg-muted/50 rounded text-xs">
size="sm" <div className="flex items-center gap-1 text-muted-foreground mb-1">
> <Clock className="w-3 h-3" />
<div className="flex items-center gap-1"> <span>{formatTime(latestUpdate.createdAt)}</span>
{getStatusIcon(blog.status)} </div>
{blog.status} <p className="line-clamp-2">{latestUpdate.content}</p>
</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="flex items-center justify-between mt-3 text-xs text-muted-foreground">
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<Eye className="w-3 h-3" />
<span>{blog.viewCount}</span>
</div> </div>
)}
{/* Stats */}
<div className="mt-2 flex items-center gap-3 text-xs text-muted-foreground">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<MessageSquare className="w-3 h-3" /> <MessageSquare className="w-3 h-3" />
<span>{blog.updates?.length || 0}</span> <span>{blog.updates?.length || 0} updates</span>
</div>
<div className="flex items-center gap-1">
<Eye className="w-3 h-3" />
<span>{blog.viewCount} views</span>
</div> </div>
</div> </div>
</Link>
);
})}
</div>
<div className="flex items-center gap-1 text-primary"> {/* View all button */}
<span className="text-xs">View live</span> {(pinnedBlogs || []).length > maxItems && (
<ChevronRight className="w-3 h-3" /> <Button
</div> variant="ghost"
</div> size="sm"
</div> className="w-full mt-4"
</Link> asChild
); >
})} <Link to="/live-blogs">
</div> View all pinned blogs
<ChevronRight className="w-3 h-3 ml-1" />
{pinnedBlogs.length > maxItems && ( </Link>
<div className="mt-4 pt-3 border-t">
<Link
to="/live-blogs"
className="w-full"
>
<Button variant="ghost" size="sm" className="w-full justify-between">
<span>View all pinned blogs</span>
<ChevronRight className="w-4 h-4" />
</Button> </Button>
</Link> )}
</div> </>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View File

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

View File

@ -33,9 +33,9 @@ export function ArticlesComponent() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data?.data.map((article) => ( {data?.data.map((article) => (
<Link <Link
key={article.id} key={article.id}
to={`/articles/${article.id}`} to={`/articles/${article.id}` as any}
className="p-6 rounded-xl border bg-card hover:shadow-lg transition-shadow cursor-pointer block" className="p-6 rounded-xl border bg-card hover:shadow-lg transition-shadow cursor-pointer block"
> >
<h2 className="text-xl font-semibold mb-2 line-clamp-2"> <h2 className="text-xl font-semibold mb-2 line-clamp-2">

View File

@ -36,9 +36,9 @@ export function LiveBlogsComponent() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{data?.data.map((liveBlog) => ( {data?.data.map((liveBlog) => (
<Link <Link
key={liveBlog.id} key={liveBlog.id}
to={`/live-blogs/${liveBlog.slug}`} to={`/live-blogs/${liveBlog.slug}` as any}
className="p-6 rounded-xl border bg-card hover:shadow-lg transition-shadow cursor-pointer block" className="p-6 rounded-xl border bg-card hover:shadow-lg transition-shadow cursor-pointer block"
> >
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">

View File

@ -22,7 +22,7 @@ export function useLiveBlogStream(
const [reconnectAttempts, setReconnectAttempts] = useState(0); const [reconnectAttempts, setReconnectAttempts] = useState(0);
const eventSourceRef = useRef<EventSource | null>(null); const eventSourceRef = useRef<EventSource | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null); const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastEventIdRef = useRef<string | null>(null); const lastEventIdRef = useRef<string | null>(null);
const optionsRef = useRef(options); const optionsRef = useRef(options);
const reconnectAttemptsRef = useRef(reconnectAttempts); const reconnectAttemptsRef = useRef(reconnectAttempts);

View File

@ -118,6 +118,7 @@ export interface LiveBlog {
slug: string; slug: string;
description: string | null; description: string | null;
status: 'draft' | 'live' | 'ended' | 'archived'; status: 'draft' | 'live' | 'ended' | 'archived';
isPinned: boolean;
strapiId: string | null; strapiId: string | null;
authorId: string | null; authorId: string | null;
categoryId: string | null; categoryId: string | null;
@ -194,6 +195,7 @@ export interface UpdateLiveBlogDto {
slug?: string; slug?: string;
description?: string; description?: string;
status?: 'draft' | 'live' | 'ended' | 'archived'; status?: 'draft' | 'live' | 'ended' | 'archived';
isPinned?: boolean;
authorId?: string; authorId?: string;
categoryId?: string; categoryId?: string;
strapiId?: string; strapiId?: string;

View File

@ -66,7 +66,7 @@ export function useCreateLiveBlogUpdate() {
return useMutation({ return useMutation({
mutationFn: ({ liveBlogId, dto }: { liveBlogId: string; dto: api.CreateLiveBlogUpdateDto }) => mutationFn: ({ liveBlogId, dto }: { liveBlogId: string; dto: api.CreateLiveBlogUpdateDto }) =>
api.createLiveBlogUpdate(liveBlogId, dto), api.createLiveBlogUpdate(liveBlogId, dto),
onSuccess: (data, variables) => { onSuccess: (_data, variables) => {
// Invalidate live blog queries // Invalidate live blog queries
queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] });
queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] });
@ -83,7 +83,7 @@ export function useUpdateLiveBlogUpdate() {
updateId: string; updateId: string;
dto: api.UpdateLiveBlogUpdateDto dto: api.UpdateLiveBlogUpdateDto
}) => api.updateLiveBlogUpdate(liveBlogId, updateId, dto), }) => api.updateLiveBlogUpdate(liveBlogId, updateId, dto),
onSuccess: (data, variables) => { onSuccess: (_data, variables) => {
// Invalidate live blog queries // Invalidate live blog queries
queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] });
queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] });
@ -97,7 +97,7 @@ export function useDeleteLiveBlogUpdate() {
return useMutation({ return useMutation({
mutationFn: ({ liveBlogId, updateId }: { liveBlogId: string; updateId: string }) => mutationFn: ({ liveBlogId, updateId }: { liveBlogId: string; updateId: string }) =>
api.deleteLiveBlogUpdate(liveBlogId, updateId), api.deleteLiveBlogUpdate(liveBlogId, updateId),
onSuccess: (data, variables) => { onSuccess: (_data, variables) => {
// Invalidate live blog queries // Invalidate live blog queries
queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] });
queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] });

View File

@ -6,6 +6,7 @@ import { LiveBlogsComponent } from './components/routes/LiveBlogsComponent'
import { LiveBlogDetailComponent } from './components/routes/LiveBlogDetailComponent' import { LiveBlogDetailComponent } from './components/routes/LiveBlogDetailComponent'
import { LiveBlogAdminComponent } from './components/routes/LiveBlogAdminComponent' import { LiveBlogAdminComponent } from './components/routes/LiveBlogAdminComponent'
import { CreateLiveBlogComponent } from './components/routes/CreateLiveBlogComponent' import { CreateLiveBlogComponent } from './components/routes/CreateLiveBlogComponent'
import { AdminDashboardComponent } from './components/routes/AdminDashboardComponent'
import { LiveBlogTicker } from './components/features/live-blog/LiveBlogTicker' import { LiveBlogTicker } from './components/features/live-blog/LiveBlogTicker'
import { PinnedLiveBlogSidebar } from './components/features/live-blog/PinnedLiveBlogSidebar' import { PinnedLiveBlogSidebar } from './components/features/live-blog/PinnedLiveBlogSidebar'
import './styles.css' import './styles.css'
@ -26,20 +27,23 @@ const rootRoute = createRootRoute({
<h1 className="text-3xl font-bold"> <h1 className="text-3xl font-bold">
<Link to="/" className="hover:underline">Placebo.mk</Link> <Link to="/" className="hover:underline">Placebo.mk</Link>
</h1> </h1>
<nav className="flex gap-4"> <nav className="flex gap-4">
<Link to="/" className="text-sm font-medium hover:underline"> <Link to="/" className="text-sm font-medium hover:underline">
Home Home
</Link> </Link>
<Link to="/articles" className="text-sm font-medium hover:underline"> <Link to="/articles" className="text-sm font-medium hover:underline">
Articles Articles
</Link> </Link>
<Link to="/live-blogs" className="text-sm font-medium hover:underline"> <Link to="/live-blogs" className="text-sm font-medium hover:underline">
Live Live
</Link> </Link>
<Link to="/admin/live-blogs/create" className="text-sm font-medium hover:underline text-primary"> <Link to="/admin" className="text-sm font-medium hover:underline text-primary">
+ New Live Blog Admin
</Link> </Link>
</nav> <Link to="/admin/live-blogs/create" className="text-sm font-medium hover:underline text-primary">
+ New Live Blog
</Link>
</nav>
</div> </div>
</header> </header>
@ -247,6 +251,12 @@ const createLiveBlogRoute = createRoute({
component: CreateLiveBlogComponent, component: CreateLiveBlogComponent,
}) })
const adminDashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/admin',
component: AdminDashboardComponent,
})
const routeTree = rootRoute.addChildren([ const routeTree = rootRoute.addChildren([
indexRoute, indexRoute,
articlesRoute, articlesRoute,
@ -255,6 +265,7 @@ const routeTree = rootRoute.addChildren([
liveBlogDetailRoute, liveBlogDetailRoute,
liveBlogAdminRoute, liveBlogAdminRoute,
createLiveBlogRoute, createLiveBlogRoute,
adminDashboardRoute,
]) ])
export const router = createRouter({ routeTree }) export const router = createRouter({ routeTree })