From 7c7bb45446cf3dbd8123ed8ea9b8c15ef2e39681 Mon Sep 17 00:00:00 2001 From: echo Date: Thu, 29 Jan 2026 11:27:45 +0100 Subject: [PATCH] admin dashboard --- frontend/package-lock.json | 11 + frontend/package.json | 1 + frontend/src/components/ArticleTicker.tsx | 8 +- .../admin/live-blog/LiveBlogManager.tsx | 2 +- .../features/live-blog/LiveBlogTicker.tsx | 4 +- .../features/live-blog/LiveBlogUpdate.tsx | 2 +- .../features/live-blog/LiveBlogViewer.tsx | 2 +- .../live-blog/PinnedLiveBlogSidebar.tsx | 209 ++++++++------ .../routes/AdminDashboardComponent.tsx | 260 ++++++++++++++++++ .../components/routes/ArticlesComponent.tsx | 4 +- .../components/routes/LiveBlogsComponent.tsx | 4 +- frontend/src/hooks/useLiveBlogStream.ts | 2 +- frontend/src/lib/api.ts | 2 + frontend/src/queries/live-blogs.ts | 6 +- frontend/src/routes.tsx | 39 ++- 15 files changed, 440 insertions(+), 116 deletions(-) create mode 100644 frontend/src/components/routes/AdminDashboardComponent.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 11de06a..6972466 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@tailwindcss/vite": "^4.1.18", "@tanstack/react-query": "^5.90.14", "@tanstack/react-router": "^1.144.0", + "date-fns": "^4.1.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, @@ -3433,6 +3434,16 @@ "devOptional": true, "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": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 48ea5ae..7d6f267 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@tailwindcss/vite": "^4.1.18", "@tanstack/react-query": "^5.90.14", "@tanstack/react-router": "^1.144.0", + "date-fns": "^4.1.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, diff --git a/frontend/src/components/ArticleTicker.tsx b/frontend/src/components/ArticleTicker.tsx index e4e1d5f..7c1657c 100644 --- a/frontend/src/components/ArticleTicker.tsx +++ b/frontend/src/components/ArticleTicker.tsx @@ -22,9 +22,9 @@ export function ArticleTicker() {
{articles.map((article, index) => ( - {article.title || 'No title'} @@ -32,9 +32,9 @@ export function ArticleTicker() { ))} {/* Duplicate for seamless scrolling */} {articles.map((article, index) => ( - {article.title || 'No title'} diff --git a/frontend/src/components/admin/live-blog/LiveBlogManager.tsx b/frontend/src/components/admin/live-blog/LiveBlogManager.tsx index d298d8c..b13061d 100644 --- a/frontend/src/components/admin/live-blog/LiveBlogManager.tsx +++ b/frontend/src/components/admin/live-blog/LiveBlogManager.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { useLiveBlog, useDeleteLiveBlogUpdate, diff --git a/frontend/src/components/features/live-blog/LiveBlogTicker.tsx b/frontend/src/components/features/live-blog/LiveBlogTicker.tsx index 9dbd17c..db0d883 100644 --- a/frontend/src/components/features/live-blog/LiveBlogTicker.tsx +++ b/frontend/src/components/features/live-blog/LiveBlogTicker.tsx @@ -23,7 +23,7 @@ export function LiveBlogTicker({ const [scrollPosition, setScrollPosition] = useState(0); const tickerRef = useRef(null); const contentRef = useRef(null); - const animationRef = useRef(); + const animationRef = useRef(undefined); // Calculate total width needed for scrolling const [totalWidth, setTotalWidth] = useState(0); @@ -55,7 +55,7 @@ export function LiveBlogTicker({ // Reset when scrolled past content if (newPos > totalWidth) { - newPos = -tickerRef.current?.offsetWidth || 0; + newPos = -(tickerRef.current?.offsetWidth || 0); } return newPos; diff --git a/frontend/src/components/features/live-blog/LiveBlogUpdate.tsx b/frontend/src/components/features/live-blog/LiveBlogUpdate.tsx index c3c174c..328481b 100644 --- a/frontend/src/components/features/live-blog/LiveBlogUpdate.tsx +++ b/frontend/src/components/features/live-blog/LiveBlogUpdate.tsx @@ -1,4 +1,4 @@ -import React from 'react'; + import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; diff --git a/frontend/src/components/features/live-blog/LiveBlogViewer.tsx b/frontend/src/components/features/live-blog/LiveBlogViewer.tsx index a391187..49cf066 100644 --- a/frontend/src/components/features/live-blog/LiveBlogViewer.tsx +++ b/frontend/src/components/features/live-blog/LiveBlogViewer.tsx @@ -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 { useLiveBlog, useLiveBlogUpdates } from '@/queries/live-blogs'; import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api'; diff --git a/frontend/src/components/features/live-blog/PinnedLiveBlogSidebar.tsx b/frontend/src/components/features/live-blog/PinnedLiveBlogSidebar.tsx index 106d835..8f3d651 100644 --- a/frontend/src/components/features/live-blog/PinnedLiveBlogSidebar.tsx +++ b/frontend/src/components/features/live-blog/PinnedLiveBlogSidebar.tsx @@ -1,9 +1,9 @@ -import React from 'react'; 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, @@ -49,11 +49,49 @@ export function PinnedLiveBlogSidebar({ ); } - if (error || !pinnedBlogs || pinnedBlogs.length === 0) { - return null; // Don't show sidebar if no pinned blogs + if (isLoading) { + return ( + + + + + Live Coverage + + + +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+ ))} +
+
+
+ ); } - const displayBlogs = pinnedBlogs.slice(0, maxItems); + if (error) { + return ( + + + + + Live Coverage + + + +

+ Error loading live coverage +

+
+
+ ); + } + + const displayBlogs = (pinnedBlogs || []).slice(0, maxItems); const getStatusColor = (status: string) => { switch (status) { @@ -99,99 +137,100 @@ export function PinnedLiveBlogSidebar({ Live Coverage - - {pinnedBlogs.length} pinned + + {(pinnedBlogs || []).length} pinned
- -
- {displayBlogs.map((blog) => { - const latestUpdate = getLatestUpdate(blog); - - return ( - -
- {/* Header */} -
-
-

- {blog.title} -

- - {blog.description && ( -

- {blog.description} -

- )} -
- - -
- {getStatusIcon(blog.status)} - {blog.status} -
-
-
- - {/* Latest update preview */} - {latestUpdate && ( -
-
- - {formatTime(latestUpdate.createdAt)} -
-

{latestUpdate.content}

-
- )} - - {/* Stats */} -
-
-
- - {blog.viewCount} + + {displayBlogs.length === 0 ? ( +
+

+ No pinned live blogs at the moment +

+

+ Check back later for live coverage +

+
+ ) : ( + <> +
+ {displayBlogs.map((blog) => { + const latestUpdate = getLatestUpdate(blog); + + return ( + +
+
+

+ {blog.title} +

+ {blog.description && ( +

+ {blog.description} +

+ )}
+ +
+ {getStatusIcon(blog.status)} + {blog.status} +
+
+
+ + {/* Latest update preview */} + {latestUpdate && ( +
+
+ + {formatTime(latestUpdate.createdAt)} +
+

{latestUpdate.content}

+
+ )} + + {/* Stats */} +
- {blog.updates?.length || 0} + {blog.updates?.length || 0} updates +
+
+ + {blog.viewCount} views
- -
- View live - -
-
-
- - ); - })} -
+ + ); + })} +
- {pinnedBlogs.length > maxItems && ( -
- - - -
+ )} + )} diff --git a/frontend/src/components/routes/AdminDashboardComponent.tsx b/frontend/src/components/routes/AdminDashboardComponent.tsx new file mode 100644 index 0000000..b72562e --- /dev/null +++ b/frontend/src/components/routes/AdminDashboardComponent.tsx @@ -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 ( +
+
+
+

Администраторски панел

+

+ Управување со сите написи и live блогови +

+
+
+ + +
+
+ +
+ {/* Live Blogs Section */} + + + + Live блогови + + {liveBlogs.length || 0} + + + + Сите live блогови со статус и датум на креирање + + + + {loadingLiveBlogs ? ( +
+
+

Вчитување...

+
+ ) : liveBlogs.length === 0 ? ( +
+

Нема live блогови

+ +
+ ) : ( +
+ {liveBlogs.map((blog) => ( +
+
+
+
+ + {blog.title} + + + {getStatusText(blog.status)} + + {blog.isPinned && ( + + Закачено + + )} +
+
+ Слаг: {blog.slug} + + + Креирано: {format(new Date(blog.createdAt), 'dd MMM yyyy', { locale: mk })} + + + Прегледи: {blog.viewCount} +
+
+
+ + +
+
+
+ ))} +
+ )} +
+
+ + {/* Articles Section */} + + + + Написи + + {articles.length || 0} + + + + Сите написи со статус и датум на креирање + + + + {loadingArticles ? ( +
+
+

Вчитување...

+
+ ) : articles.length === 0 ? ( +
+

Нема написи

+ +
+ ) : ( +
+ {articles.map((article) => ( +
+
+
+
+ + {article.title} + + + {getStatusText(article.status)} + +
+
+ Слаг: {article.slug} + + + Креирано: {format(new Date(article.createdAt), 'dd MMM yyyy', { locale: mk })} + + + Прегледи: {article.views} +
+ {article.excerpt && ( +

+ {article.excerpt} +

+ )} +
+
+ + +
+
+
+ ))} +
+ )} +
+
+
+ + {/* Quick Stats */} +
+ + +
+ {liveBlogs.filter(b => b.status === 'live').length || 0} +
+

Активни live блогови

+
+
+ + +
+ {articles.filter(a => a.status === 'published').length || 0} +
+

Објавени написи

+
+
+ + +
+ {liveBlogs.filter(b => b.isPinned).length || 0} +
+

Закачени live блогови

+
+
+ + +
+ {(liveBlogs.reduce((sum, b) => sum + b.viewCount, 0) || 0) + + (articles.reduce((sum, a) => sum + a.views, 0) || 0)} +
+

Вкупно прегледи

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/routes/ArticlesComponent.tsx b/frontend/src/components/routes/ArticlesComponent.tsx index b9dd345..0ddbca3 100644 --- a/frontend/src/components/routes/ArticlesComponent.tsx +++ b/frontend/src/components/routes/ArticlesComponent.tsx @@ -33,9 +33,9 @@ export function ArticlesComponent() {
{data?.data.map((article) => ( -

diff --git a/frontend/src/components/routes/LiveBlogsComponent.tsx b/frontend/src/components/routes/LiveBlogsComponent.tsx index 0777e12..612af23 100644 --- a/frontend/src/components/routes/LiveBlogsComponent.tsx +++ b/frontend/src/components/routes/LiveBlogsComponent.tsx @@ -36,9 +36,9 @@ export function LiveBlogsComponent() {
{data?.data.map((liveBlog) => ( -
diff --git a/frontend/src/hooks/useLiveBlogStream.ts b/frontend/src/hooks/useLiveBlogStream.ts index e2f07da..e96f1da 100644 --- a/frontend/src/hooks/useLiveBlogStream.ts +++ b/frontend/src/hooks/useLiveBlogStream.ts @@ -22,7 +22,7 @@ export function useLiveBlogStream( const [reconnectAttempts, setReconnectAttempts] = useState(0); const eventSourceRef = useRef(null); - const reconnectTimeoutRef = useRef(null); + const reconnectTimeoutRef = useRef | null>(null); const lastEventIdRef = useRef(null); const optionsRef = useRef(options); const reconnectAttemptsRef = useRef(reconnectAttempts); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 905e6dd..3c7acee 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -118,6 +118,7 @@ export interface LiveBlog { slug: string; description: string | null; status: 'draft' | 'live' | 'ended' | 'archived'; + isPinned: boolean; strapiId: string | null; authorId: string | null; categoryId: string | null; @@ -194,6 +195,7 @@ export interface UpdateLiveBlogDto { slug?: string; description?: string; status?: 'draft' | 'live' | 'ended' | 'archived'; + isPinned?: boolean; authorId?: string; categoryId?: string; strapiId?: string; diff --git a/frontend/src/queries/live-blogs.ts b/frontend/src/queries/live-blogs.ts index 095a747..4908e9f 100644 --- a/frontend/src/queries/live-blogs.ts +++ b/frontend/src/queries/live-blogs.ts @@ -66,7 +66,7 @@ export function useCreateLiveBlogUpdate() { return useMutation({ mutationFn: ({ liveBlogId, dto }: { liveBlogId: string; dto: api.CreateLiveBlogUpdateDto }) => api.createLiveBlogUpdate(liveBlogId, dto), - onSuccess: (data, variables) => { + onSuccess: (_data, variables) => { // Invalidate live blog queries queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] }); @@ -83,7 +83,7 @@ export function useUpdateLiveBlogUpdate() { updateId: string; dto: api.UpdateLiveBlogUpdateDto }) => api.updateLiveBlogUpdate(liveBlogId, updateId, dto), - onSuccess: (data, variables) => { + onSuccess: (_data, variables) => { // Invalidate live blog queries queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] }); @@ -97,7 +97,7 @@ export function useDeleteLiveBlogUpdate() { return useMutation({ mutationFn: ({ liveBlogId, updateId }: { liveBlogId: string; updateId: string }) => api.deleteLiveBlogUpdate(liveBlogId, updateId), - onSuccess: (data, variables) => { + onSuccess: (_data, variables) => { // Invalidate live blog queries queryClient.invalidateQueries({ queryKey: ['liveBlog', variables.liveBlogId] }); queryClient.invalidateQueries({ queryKey: ['liveBlogUpdates', variables.liveBlogId] }); diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 64647f5..b82b554 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -6,6 +6,7 @@ import { LiveBlogsComponent } from './components/routes/LiveBlogsComponent' import { LiveBlogDetailComponent } from './components/routes/LiveBlogDetailComponent' import { LiveBlogAdminComponent } from './components/routes/LiveBlogAdminComponent' import { CreateLiveBlogComponent } from './components/routes/CreateLiveBlogComponent' +import { AdminDashboardComponent } from './components/routes/AdminDashboardComponent' import { LiveBlogTicker } from './components/features/live-blog/LiveBlogTicker' import { PinnedLiveBlogSidebar } from './components/features/live-blog/PinnedLiveBlogSidebar' import './styles.css' @@ -26,20 +27,23 @@ const rootRoute = createRootRoute({

Placebo.mk

- +
@@ -247,6 +251,12 @@ const createLiveBlogRoute = createRoute({ component: CreateLiveBlogComponent, }) +const adminDashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/admin', + component: AdminDashboardComponent, +}) + const routeTree = rootRoute.addChildren([ indexRoute, articlesRoute, @@ -255,6 +265,7 @@ const routeTree = rootRoute.addChildren([ liveBlogDetailRoute, liveBlogAdminRoute, createLiveBlogRoute, + adminDashboardRoute, ]) export const router = createRouter({ routeTree })