From 9ca05f5ea16ed983c23c0cb2c02af43d3a8e9764 Mon Sep 17 00:00:00 2001
From: echo
Date: Tue, 3 Feb 2026 23:14:36 +0100
Subject: [PATCH] delete/archive implemented
---
backend/src/main.ts | 4 +-
backend/src/modules/articles.controller.ts | 15 +
backend/src/modules/articles.dto.ts | 12 +-
backend/src/modules/articles.service.ts | 42 +-
backend/src/modules/live-blog.controller.ts | 15 +
backend/src/modules/live-blog.service.ts | 32 +-
backend/src/modules/strapi.controller.ts | 7 +-
backend/src/modules/strapi.service.ts | 135 ++++---
.../routes/AdminDashboardComponent.tsx | 364 +++++++++++++-----
frontend/src/lib/api.ts | 117 +++++-
frontend/src/queries/articles.ts | 67 +++-
frontend/src/queries/live-blogs.ts | 27 ++
todos.md | 6 +
13 files changed, 680 insertions(+), 163 deletions(-)
diff --git a/backend/src/main.ts b/backend/src/main.ts
index 3fd4ced..d4964c5 100644
--- a/backend/src/main.ts
+++ b/backend/src/main.ts
@@ -15,11 +15,11 @@ async function bootstrap() {
});
app.setGlobalPrefix('api/v1');
-
+
const port = process.env.PORT ?? 3000;
const host = '0.0.0.0'; // Bind to all interfaces for Docker
await app.listen(port, host);
-
+
console.log(`Application is running on: http://${host}:${port}`);
}
void bootstrap();
diff --git a/backend/src/modules/articles.controller.ts b/backend/src/modules/articles.controller.ts
index 7c549e7..8862c17 100644
--- a/backend/src/modules/articles.controller.ts
+++ b/backend/src/modules/articles.controller.ts
@@ -4,6 +4,7 @@ import {
Post,
Put,
Delete,
+ Patch,
Body,
Param,
Query,
@@ -15,6 +16,7 @@ import {
UpdateArticleDto,
FindArticlesDto,
} from './articles.dto';
+import { ArticleStatus } from './entities';
@Controller('articles')
export class ArticlesController {
@@ -54,4 +56,17 @@ export class ArticlesController {
remove(@Param('id') id: string) {
return this.articlesService.remove(id);
}
+
+ @Patch(':id/archive')
+ archive(@Param('id') id: string) {
+ return this.articlesService.archive(id);
+ }
+
+ @Patch(':id/publish')
+ publish(
+ @Param('id') id: string,
+ @Query('status') status?: ArticleStatus,
+ ) {
+ return this.articlesService.publish(id, status || ArticleStatus.PUBLISHED);
+ }
}
diff --git a/backend/src/modules/articles.dto.ts b/backend/src/modules/articles.dto.ts
index 116147c..0abc5a1 100644
--- a/backend/src/modules/articles.dto.ts
+++ b/backend/src/modules/articles.dto.ts
@@ -8,7 +8,13 @@ import {
IsBoolean,
IsDate,
} from 'class-validator';
-import { ArticleStatus, LiveBlogStatus, ImagePosition, ImageSize, VideoPosition } from './entities';
+import {
+ ArticleStatus,
+ LiveBlogStatus,
+ ImagePosition,
+ ImageSize,
+ VideoPosition,
+} from './entities';
export class CreateArticleDto {
@IsString()
@@ -148,8 +154,8 @@ export class FindArticlesDto {
tag?: string;
@IsOptional()
- @IsEnum(ArticleStatus)
- status?: ArticleStatus;
+ @IsString()
+ status?: string;
@IsOptional()
@IsString()
diff --git a/backend/src/modules/articles.service.ts b/backend/src/modules/articles.service.ts
index 955e0a5..aa35b48 100644
--- a/backend/src/modules/articles.service.ts
+++ b/backend/src/modules/articles.service.ts
@@ -28,21 +28,23 @@ export class ArticlesService {
async findAll(
dto: FindArticlesDto,
): Promise<{ data: Article[]; total: number }> {
- const {
- category,
- author,
- tag,
- status = ArticleStatus.PUBLISHED,
- search,
- page = 1,
- limit = 10,
- } = dto;
+ const { category, author, tag, status, search, page = 1, limit = 10 } = dto;
const queryBuilder = this.articleRepository
.createQueryBuilder('article')
.leftJoinAndSelect('article.author', 'author')
- .leftJoinAndSelect('article.category', 'category')
- .where('article.status = :status', { status });
+ .leftJoinAndSelect('article.category', 'category');
+
+ // Handle status filter - can be single value or comma-separated list
+ if (status) {
+ if (typeof status === 'string' && status.includes(',')) {
+ const statuses = status.split(',').map((s) => s.trim());
+ queryBuilder.where('article.status IN (:...statuses)', { statuses });
+ } else {
+ queryBuilder.where('article.status = :status', { status });
+ }
+ }
+ // If no status specified, return all articles (for admin dashboard)
if (category) {
queryBuilder.andWhere('category.slug = :category', { category });
@@ -109,6 +111,18 @@ export class ArticlesService {
await this.articleRepository.remove(article);
}
+ async archive(id: string): Promise {
+ const article = await this.findOne(id);
+ article.status = ArticleStatus.ARCHIVED;
+ return await this.articleRepository.save(article);
+ }
+
+ async publish(id: string, status: ArticleStatus = ArticleStatus.PUBLISHED): Promise {
+ const article = await this.findOne(id);
+ article.status = status;
+ return await this.articleRepository.save(article);
+ }
+
async syncFromStrapi(
strapiId: string,
data: Partial,
@@ -129,8 +143,10 @@ export class ArticlesService {
}
async removeByStrapiId(strapiId: string): Promise {
- const article = await this.articleRepository.findOne({ where: { strapiId } });
-
+ const article = await this.articleRepository.findOne({
+ where: { strapiId },
+ });
+
if (!article) {
this.logger.warn(`Article with strapiId ${strapiId} not found`);
return;
diff --git a/backend/src/modules/live-blog.controller.ts b/backend/src/modules/live-blog.controller.ts
index d0e52a5..4e66209 100644
--- a/backend/src/modules/live-blog.controller.ts
+++ b/backend/src/modules/live-blog.controller.ts
@@ -4,6 +4,7 @@ import {
Post,
Put,
Delete,
+ Patch,
Body,
Param,
Query,
@@ -21,6 +22,7 @@ import {
CreateLiveBlogUpdateDto,
UpdateLiveBlogUpdateDto,
} from './articles.dto';
+import { LiveBlogStatus } from './entities';
@Controller('live-blogs')
export class LiveBlogController {
@@ -121,6 +123,19 @@ export class LiveBlogController {
return this.liveBlogService.removeUpdate(liveBlogId, updateId);
}
+ @Patch(':id/archive')
+ archive(@Param('id') id: string) {
+ return this.liveBlogService.archive(id);
+ }
+
+ @Patch(':id/publish')
+ publish(
+ @Param('id') id: string,
+ @Query('status') status?: LiveBlogStatus,
+ ) {
+ return this.liveBlogService.publish(id, status || LiveBlogStatus.DRAFT);
+ }
+
// SSE endpoint for real-time updates
@Get(':id/stream')
stream(
diff --git a/backend/src/modules/live-blog.service.ts b/backend/src/modules/live-blog.service.ts
index 4db1b2c..6d2c65f 100644
--- a/backend/src/modules/live-blog.service.ts
+++ b/backend/src/modules/live-blog.service.ts
@@ -116,13 +116,9 @@ export class LiveBlogService implements OnModuleInit {
} else {
queryBuilder.where('liveBlog.status = :status', { status });
}
- } else if (!isPinned) {
- // Default to live blogs if no status specified AND not querying for pinned blogs
- // Pinned blogs can be either live or ended
- queryBuilder.where('liveBlog.status = :status', {
- status: LiveBlogStatus.LIVE,
- });
}
+ // If no status specified, return all live blogs (for admin dashboard)
+ // Note: Pinned blogs query should still work without status filter
if (category) {
queryBuilder.andWhere('category.slug = :category', { category });
@@ -262,6 +258,18 @@ export class LiveBlogService implements OnModuleInit {
await this.liveBlogRepository.remove(liveBlog);
}
+ async archive(id: string): Promise {
+ const liveBlog = await this.findOne(id);
+ liveBlog.status = LiveBlogStatus.ARCHIVED;
+ return await this.liveBlogRepository.save(liveBlog);
+ }
+
+ async publish(id: string, status: LiveBlogStatus = LiveBlogStatus.DRAFT): Promise {
+ const liveBlog = await this.findOne(id);
+ liveBlog.status = status;
+ return await this.liveBlogRepository.save(liveBlog);
+ }
+
// Live Blog Update CRUD operations
async createUpdate(
dto: CreateLiveBlogUpdateDto,
@@ -380,7 +388,7 @@ export class LiveBlogService implements OnModuleInit {
const keepAliveInterval = setInterval(() => {
try {
response.write(`: keep-alive\n\n`);
- } catch (error) {
+ } catch {
// Client disconnected, stop sending keep-alive
clearInterval(keepAliveInterval);
}
@@ -443,15 +451,19 @@ export class LiveBlogService implements OnModuleInit {
}
async removeByStrapiId(strapiId: string): Promise {
- const liveBlog = await this.liveBlogRepository.findOne({ where: { strapiId } });
-
+ const liveBlog = await this.liveBlogRepository.findOne({
+ where: { strapiId },
+ });
+
if (!liveBlog) {
this.logger.warn(`LiveBlog with strapiId ${strapiId} not found`);
return;
}
await this.liveBlogRepository.remove(liveBlog);
- this.logger.log(`Successfully deleted live blog with strapiId: ${strapiId}`);
+ this.logger.log(
+ `Successfully deleted live blog with strapiId: ${strapiId}`,
+ );
}
// Utility methods
diff --git a/backend/src/modules/strapi.controller.ts b/backend/src/modules/strapi.controller.ts
index cf4fede..cf6a02b 100644
--- a/backend/src/modules/strapi.controller.ts
+++ b/backend/src/modules/strapi.controller.ts
@@ -2,7 +2,12 @@ import { Controller, Post, Body, Logger } from '@nestjs/common';
import { StrapiService } from './strapi.service';
interface WebhookBody {
- event: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish' | 'entry.unpublish';
+ event:
+ | 'entry.create'
+ | 'entry.update'
+ | 'entry.delete'
+ | 'entry.publish'
+ | 'entry.unpublish';
model: string;
entry: {
documentId: string;
diff --git a/backend/src/modules/strapi.service.ts b/backend/src/modules/strapi.service.ts
index 225a74f..6a8d30a 100644
--- a/backend/src/modules/strapi.service.ts
+++ b/backend/src/modules/strapi.service.ts
@@ -5,7 +5,13 @@ import { lastValueFrom } from 'rxjs';
import { ArticlesService } from './articles.service';
import { LiveBlogService } from './live-blog.service';
import { CreateArticleDto, CreateLiveBlogDto } from './articles.dto';
-import { ArticleStatus, LiveBlogStatus, ImagePosition, ImageSize, VideoPosition } from './entities';
+import {
+ ArticleStatus,
+ LiveBlogStatus,
+ ImagePosition,
+ ImageSize,
+ VideoPosition,
+} from './entities';
interface StrapiArticle {
id: number;
@@ -88,18 +94,18 @@ export class StrapiService {
private extractImageUrl(strapiArticle: StrapiArticle): string | undefined {
// Try to get image from img field first (single image)
let imageUrl: string | undefined;
-
+
if (strapiArticle.img?.url) {
imageUrl = strapiArticle.img.url;
} else if (strapiArticle.media?.[0]?.url) {
// Try to get first image from media field (multiple images)
imageUrl = strapiArticle.media[0].url;
}
-
+
if (!imageUrl) {
return undefined;
}
-
+
// If URL is relative, prepend Strapi base URL
if (imageUrl.startsWith('/')) {
// Convert Docker service URL to localhost for frontend access
@@ -107,25 +113,27 @@ export class StrapiService {
const frontendStrapiUrl = this.strapiUrl.replace('cms:', 'localhost:');
return `${frontendStrapiUrl}${imageUrl}`;
}
-
+
return imageUrl;
}
- private extractLiveBlogImageUrl(strapiLiveBlog: StrapiLiveBlog): string | undefined {
+ private extractLiveBlogImageUrl(
+ strapiLiveBlog: StrapiLiveBlog,
+ ): string | undefined {
// Try to get image from img field first (single image)
let imageUrl: string | undefined;
-
+
if (strapiLiveBlog.img?.url) {
imageUrl = strapiLiveBlog.img.url;
} else if (strapiLiveBlog.media?.[0]?.url) {
// Try to get first image from media field (multiple images)
imageUrl = strapiLiveBlog.media[0].url;
}
-
+
if (!imageUrl) {
return undefined;
}
-
+
// If URL is relative, prepend Strapi base URL
if (imageUrl.startsWith('/')) {
// Convert Docker service URL to localhost for frontend access
@@ -133,7 +141,7 @@ export class StrapiService {
const frontendStrapiUrl = this.strapiUrl.replace('cms:', 'localhost:');
return `${frontendStrapiUrl}${imageUrl}`;
}
-
+
return imageUrl;
}
@@ -141,7 +149,7 @@ export class StrapiService {
try {
this.logger.log('Starting articles sync from Strapi...');
- const response = await lastValueFrom(
+ const response = await lastValueFrom(
this.httpService.get>(
`${this.strapiUrl}/api/articles?populate=*`,
{
@@ -155,7 +163,7 @@ export class StrapiService {
for (const strapiArticle of strapiArticles) {
const imageUrl = this.extractImageUrl(strapiArticle);
-
+
const articleData: Partial = {
title: strapiArticle.title,
excerpt: strapiArticle.description,
@@ -166,10 +174,12 @@ export class StrapiService {
: ArticleStatus.DRAFT,
tags: [],
featuredImage: imageUrl,
- imagePosition: (strapiArticle.imagePosition || 'top') as ImagePosition,
+ imagePosition: (strapiArticle.imagePosition ||
+ 'top') as ImagePosition,
imageSize: (strapiArticle.imageSize || 'medium') as ImageSize,
videoUrl: strapiArticle.videoUrl || '',
- videoPosition: (strapiArticle.videoPosition || 'inline') as VideoPosition,
+ videoPosition: (strapiArticle.videoPosition ||
+ 'inline') as VideoPosition,
videoCaption: strapiArticle.videoCaption || '',
};
@@ -191,12 +201,19 @@ export class StrapiService {
async syncSingleArticle(
strapiId: string,
- event?: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish' | 'entry.unpublish',
+ event?:
+ | 'entry.create'
+ | 'entry.update'
+ | 'entry.delete'
+ | 'entry.publish'
+ | 'entry.unpublish',
): Promise {
try {
- this.logger.log(`Syncing single article from Strapi: ${strapiId}, event: ${event}`);
+ this.logger.log(
+ `Syncing single article from Strapi: ${strapiId}, event: ${event}`,
+ );
- const response = await lastValueFrom(
+ const response = await lastValueFrom(
this.httpService.get>(
`${this.strapiUrl}/api/articles/${strapiId}?populate=*`,
{
@@ -206,7 +223,7 @@ export class StrapiService {
);
const strapiArticle = response.data.data;
-
+
// Determine status based on publishedAt and event type
let status: ArticleStatus;
if (event === 'entry.unpublish') {
@@ -221,7 +238,7 @@ export class StrapiService {
}
const imageUrl = this.extractImageUrl(strapiArticle);
-
+
const articleData: Partial = {
title: strapiArticle.title,
excerpt: strapiArticle.description,
@@ -233,7 +250,8 @@ export class StrapiService {
imagePosition: (strapiArticle.imagePosition || 'top') as ImagePosition,
imageSize: (strapiArticle.imageSize || 'medium') as ImageSize,
videoUrl: strapiArticle.videoUrl || '',
- videoPosition: (strapiArticle.videoPosition || 'inline') as VideoPosition,
+ videoPosition: (strapiArticle.videoPosition ||
+ 'inline') as VideoPosition,
videoCaption: strapiArticle.videoCaption || '',
};
@@ -241,20 +259,26 @@ export class StrapiService {
strapiArticle.documentId,
articleData,
);
- this.logger.log(`Successfully synced article: ${strapiArticle.title} with status: ${status}`);
+ this.logger.log(
+ `Successfully synced article: ${strapiArticle.title} with status: ${status}`,
+ );
} catch (error: any) {
// If we get a 404 and it's an unpublish event, we can still mark it as draft
if (event === 'entry.unpublish' && error.response?.status === 404) {
- this.logger.log(`Article ${strapiId} not found in Strapi, marking as draft based on unpublish event`);
-
+ this.logger.log(
+ `Article ${strapiId} not found in Strapi, marking as draft based on unpublish event`,
+ );
+
// Try to update the article status to draft directly
try {
const articleData: Partial = {
status: ArticleStatus.DRAFT,
};
-
+
await this.articlesService.syncFromStrapi(strapiId, articleData);
- this.logger.log(`Marked article ${strapiId} as draft based on unpublish event`);
+ this.logger.log(
+ `Marked article ${strapiId} as draft based on unpublish event`,
+ );
} catch (syncError) {
this.logger.error(
`Failed to mark article ${strapiId} as draft:`,
@@ -276,7 +300,7 @@ export class StrapiService {
try {
this.logger.log('Starting live blogs sync from Strapi...');
- const response = await lastValueFrom(
+ const response = await lastValueFrom(
this.httpService.get>(
`${this.strapiUrl}/api/live-blogs?populate=*`,
{
@@ -290,17 +314,19 @@ export class StrapiService {
for (const strapiLiveBlog of strapiLiveBlogs) {
const imageUrl = this.extractLiveBlogImageUrl(strapiLiveBlog);
-
+
const liveBlogData: Partial = {
title: strapiLiveBlog.title,
description: strapiLiveBlog.description,
slug: strapiLiveBlog.slug,
status: this.mapStrapiStatusToLiveBlogStatus(strapiLiveBlog.status),
featuredImage: imageUrl,
- imagePosition: (strapiLiveBlog.imagePosition || 'top') as ImagePosition,
+ imagePosition: (strapiLiveBlog.imagePosition ||
+ 'top') as ImagePosition,
imageSize: (strapiLiveBlog.imageSize || 'medium') as ImageSize,
videoUrl: strapiLiveBlog.videoUrl || '',
- videoPosition: (strapiLiveBlog.videoPosition || 'inline') as VideoPosition,
+ videoPosition: (strapiLiveBlog.videoPosition ||
+ 'inline') as VideoPosition,
videoCaption: strapiLiveBlog.videoCaption || '',
};
@@ -322,12 +348,19 @@ export class StrapiService {
async syncSingleLiveBlog(
strapiId: string,
- event?: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish' | 'entry.unpublish',
+ event?:
+ | 'entry.create'
+ | 'entry.update'
+ | 'entry.delete'
+ | 'entry.publish'
+ | 'entry.unpublish',
): Promise {
try {
- this.logger.log(`Syncing single live blog from Strapi: ${strapiId}, event: ${event}`);
+ this.logger.log(
+ `Syncing single live blog from Strapi: ${strapiId}, event: ${event}`,
+ );
- const response = await lastValueFrom(
+ const response = await lastValueFrom(
this.httpService.get>(
`${this.strapiUrl}/api/live-blogs/${strapiId}?populate=*`,
{
@@ -337,18 +370,18 @@ export class StrapiService {
);
const strapiLiveBlog = response.data.data;
-
+
// For live blogs, we use the status from Strapi directly
// but we might want to handle unpublish events differently
let status = this.mapStrapiStatusToLiveBlogStatus(strapiLiveBlog.status);
-
+
// If it's an unpublish event, set to draft
if (event === 'entry.unpublish') {
status = LiveBlogStatus.DRAFT;
}
const imageUrl = this.extractLiveBlogImageUrl(strapiLiveBlog);
-
+
const liveBlogData: Partial = {
title: strapiLiveBlog.title,
description: strapiLiveBlog.description,
@@ -358,7 +391,8 @@ export class StrapiService {
imagePosition: (strapiLiveBlog.imagePosition || 'top') as ImagePosition,
imageSize: (strapiLiveBlog.imageSize || 'medium') as ImageSize,
videoUrl: strapiLiveBlog.videoUrl || '',
- videoPosition: (strapiLiveBlog.videoPosition || 'inline') as VideoPosition,
+ videoPosition: (strapiLiveBlog.videoPosition ||
+ 'inline') as VideoPosition,
videoCaption: strapiLiveBlog.videoCaption || '',
};
@@ -366,20 +400,26 @@ export class StrapiService {
strapiLiveBlog.documentId,
liveBlogData,
);
- this.logger.log(`Successfully synced live blog: ${strapiLiveBlog.title} with status: ${status}`);
+ this.logger.log(
+ `Successfully synced live blog: ${strapiLiveBlog.title} with status: ${status}`,
+ );
} catch (error: any) {
// If we get a 404 and it's an unpublish event, we can still mark it as draft
if (event === 'entry.unpublish' && error.response?.status === 404) {
- this.logger.log(`Live blog ${strapiId} not found in Strapi, marking as draft based on unpublish event`);
-
+ this.logger.log(
+ `Live blog ${strapiId} not found in Strapi, marking as draft based on unpublish event`,
+ );
+
// Try to update the live blog status to draft directly
try {
const liveBlogData: Partial = {
status: LiveBlogStatus.DRAFT,
};
-
+
await this.liveBlogService.syncFromStrapi(strapiId, liveBlogData);
- this.logger.log(`Marked live blog ${strapiId} as draft based on unpublish event`);
+ this.logger.log(
+ `Marked live blog ${strapiId} as draft based on unpublish event`,
+ );
} catch (syncError) {
this.logger.error(
`Failed to mark live blog ${strapiId} as draft:`,
@@ -413,7 +453,12 @@ export class StrapiService {
}
async handleWebhook(
- event: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish' | 'entry.unpublish',
+ event:
+ | 'entry.create'
+ | 'entry.update'
+ | 'entry.delete'
+ | 'entry.publish'
+ | 'entry.unpublish',
data: { documentId: string; model?: string },
): Promise {
this.logger.log(
@@ -421,8 +466,10 @@ export class StrapiService {
);
if (event === 'entry.delete') {
- this.logger.log(`Handling delete for document: ${data.documentId}, model: ${data.model}`);
-
+ this.logger.log(
+ `Handling delete for document: ${data.documentId}, model: ${data.model}`,
+ );
+
if (data.model === 'article') {
await this.articlesService.removeByStrapiId(data.documentId);
} else if (data.model === 'live-blog') {
diff --git a/frontend/src/components/routes/AdminDashboardComponent.tsx b/frontend/src/components/routes/AdminDashboardComponent.tsx
index b72562e..5c396a6 100644
--- a/frontend/src/components/routes/AdminDashboardComponent.tsx
+++ b/frontend/src/components/routes/AdminDashboardComponent.tsx
@@ -1,5 +1,7 @@
+import { useState } from 'react';
import { useLiveBlogs } from '@/queries/live-blogs';
-import { useArticles } from '@/queries/articles';
+import { useArticles, useDeleteArticle, useArchiveArticle, usePublishArticle } from '@/queries/articles';
+import { useDeleteLiveBlog, useArchiveLiveBlog, usePublishLiveBlog } from '@/queries/live-blogs';
import { Link } from '@tanstack/react-router';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -8,11 +10,97 @@ 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 });
+ // State for confirmation dialog and filters
+ const [showConfirmDialog, setShowConfirmDialog] = useState(false);
+ const [dialogType, setDialogType] = useState<'delete' | 'archive'>('delete');
+ const [itemToDelete, setItemToDelete] = useState<{
+ type: 'article' | 'liveBlog';
+ id: string;
+ title: string;
+ } | null>(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [showArchived, setShowArchived] = useState(false);
+
+ const { data: liveBlogsData, isLoading: loadingLiveBlogs } = useLiveBlogs({
+ limit: 50,
+ status: showArchived ? 'archived' : 'draft,live,ended'
+ });
+ const { data: articlesData, isLoading: loadingArticles } = useArticles({
+ limit: 50,
+ status: showArchived ? 'archived' : 'draft,published'
+ });
+ const deleteArticleMutation = useDeleteArticle();
+ const deleteLiveBlogMutation = useDeleteLiveBlog();
+ const archiveArticleMutation = useArchiveArticle();
+ const archiveLiveBlogMutation = useArchiveLiveBlog();
+ const publishArticleMutation = usePublishArticle();
+ const publishLiveBlogMutation = usePublishLiveBlog();
const liveBlogs = liveBlogsData?.data || [];
const articles = articlesData?.data || [];
+
+ // No need to filter items - API already filters based on showArchived state
+ const filteredLiveBlogs = liveBlogs;
+ const filteredArticles = articles;
+
+ const handleDeleteClick = (type: 'article' | 'liveBlog', id: string, title: string) => {
+ setItemToDelete({ type, id, title });
+ setDialogType('delete');
+ setShowConfirmDialog(true);
+ };
+
+ const handleArchiveClick = (type: 'article' | 'liveBlog', id: string, title: string) => {
+ setItemToDelete({ type, id, title });
+ setDialogType('archive');
+ setShowConfirmDialog(true);
+ };
+
+ const handlePublishClick = async (type: 'article' | 'liveBlog', id: string) => {
+ setIsProcessing(true);
+ try {
+ if (type === 'article') {
+ await publishArticleMutation.mutateAsync({ id, status: 'published' });
+ } else {
+ await publishLiveBlogMutation.mutateAsync({ id, status: 'draft' });
+ }
+ } catch (error) {
+ console.error('Failed to publish:', error);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const handleConfirmAction = async () => {
+ if (!itemToDelete) return;
+
+ setIsProcessing(true);
+ try {
+ if (dialogType === 'delete') {
+ if (itemToDelete.type === 'article') {
+ await deleteArticleMutation.mutateAsync(itemToDelete.id);
+ } else {
+ await deleteLiveBlogMutation.mutateAsync(itemToDelete.id);
+ }
+ } else { // archive
+ if (itemToDelete.type === 'article') {
+ await archiveArticleMutation.mutateAsync(itemToDelete.id);
+ } else {
+ await archiveLiveBlogMutation.mutateAsync(itemToDelete.id);
+ }
+ }
+ setShowConfirmDialog(false);
+ setItemToDelete(null);
+ } catch (error) {
+ console.error(`Failed to ${dialogType}:`, error);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const handleCancelAction = () => {
+ setShowConfirmDialog(false);
+ setItemToDelete(null);
+ };
const getStatusColor = (status: string) => {
switch (status) {
@@ -49,14 +137,20 @@ export function AdminDashboardComponent() {
Управување со сите написи и live блогови
-
-
-
-
+
+
+
+
+
@@ -64,10 +158,10 @@ export function AdminDashboardComponent() {
- Live блогови
-
- {liveBlogs.length || 0}
-
+ {showArchived ? 'Архивирани Live блогови' : 'Live блогови'}
+
+ {filteredLiveBlogs.length || 0}
+
Сите live блогови со статус и датум на креирање
@@ -79,16 +173,20 @@ export function AdminDashboardComponent() {
+ {dialogType === 'delete'
+ ? `Дали сте сигурни дека сакате да го избришете "${itemToDelete.title}"?`
+ : `Дали сте сигурни дека сакате да го архивирате "${itemToDelete.title}"?`}
+