From 3829c9bc87a2e71b9ef1520e998926ac6efbee2b Mon Sep 17 00:00:00 2001 From: kimjaehyeon0101 <47347352-kimjaehyeon0101@users.noreply.replit.com> Date: Tue, 14 Oct 2025 10:01:14 +0000 Subject: [PATCH] Enable management of media outlet articles including creation, editing, and deletion Adds article management functionality to media outlets, including API endpoints for creating, updating, and deleting articles, and integrates these features into the client-side UI with dialogs and mutations. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/QTw0kIA --- .replit | 4 + .../src/components/MediaOutletManagement.tsx | 327 +++++++++++++++++- server/routes.ts | 43 +++ server/storage.ts | 11 + 4 files changed, 370 insertions(+), 15 deletions(-) diff --git a/.replit b/.replit index 48ea545..1dcf651 100644 --- a/.replit +++ b/.replit @@ -18,6 +18,10 @@ externalPort = 80 localPort = 34047 externalPort = 3002 +[[ports]] +localPort = 34595 +externalPort = 6800 + [[ports]] localPort = 36309 externalPort = 5173 diff --git a/client/src/components/MediaOutletManagement.tsx b/client/src/components/MediaOutletManagement.tsx index 5038d9b..2fb6824 100644 --- a/client/src/components/MediaOutletManagement.tsx +++ b/client/src/components/MediaOutletManagement.tsx @@ -1,11 +1,15 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useMutation } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { ArrowLeft, Edit, Plus, BarChart3, Gavel, MessageSquare } from "lucide-react"; -import type { MediaOutlet } from "@shared/schema"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { ArrowLeft, Edit, Plus, BarChart3, Gavel, MessageSquare, Trash2, Eye } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import { queryClient, apiRequest } from "@/lib/queryClient"; +import type { MediaOutlet, Article } from "@shared/schema"; interface MediaOutletManagementProps { outlet: MediaOutlet; @@ -14,12 +18,127 @@ interface MediaOutletManagementProps { export default function MediaOutletManagement({ outlet, onBack }: MediaOutletManagementProps) { const [activeTab, setActiveTab] = useState("overview"); - - // TODO: Add queries for articles, predictions, auctions, comments related to this outlet - const { data: articles = [] } = useQuery({ - queryKey: ["/api/articles", outlet.id], - enabled: false, // Disabled for now as articles API is not implemented + const [isArticleDialogOpen, setIsArticleDialogOpen] = useState(false); + const [editingArticle, setEditingArticle] = useState
(null); + const [articleForm, setArticleForm] = useState({ + title: "", + slug: "", + content: "", + excerpt: "", + imageUrl: "", + publishedMinutesAgo: 0, + isFeatured: false }); + const { toast } = useToast(); + + const { data: articles = [], isLoading: articlesLoading } = useQuery({ + queryKey: [`/api/media-outlets/${outlet.slug}/articles`], + }); + + const createArticleMutation = useMutation({ + mutationFn: async (data: typeof articleForm) => { + return await apiRequest("/api/articles", "POST", { ...data, mediaOutletId: outlet.id }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] }); + setIsArticleDialogOpen(false); + resetArticleForm(); + toast({ + title: "Article Created", + description: "The article has been created successfully.", + }); + }, + onError: (error: Error) => { + toast({ + title: "Error", + description: error.message || "Failed to create article", + variant: "destructive", + }); + } + }); + + const updateArticleMutation = useMutation({ + mutationFn: async (data: { id: string; updates: Partial }) => { + return await apiRequest(`/api/articles/${data.id}`, "PATCH", data.updates); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] }); + setIsArticleDialogOpen(false); + setEditingArticle(null); + resetArticleForm(); + toast({ + title: "Article Updated", + description: "The article has been updated successfully.", + }); + }, + onError: (error: Error) => { + toast({ + title: "Error", + description: error.message || "Failed to update article", + variant: "destructive", + }); + } + }); + + const deleteArticleMutation = useMutation({ + mutationFn: async (id: string) => { + return await apiRequest(`/api/articles/${id}`, "DELETE", {}); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] }); + toast({ + title: "Article Deleted", + description: "The article has been deleted successfully.", + }); + }, + onError: (error: Error) => { + toast({ + title: "Error", + description: error.message || "Failed to delete article", + variant: "destructive", + }); + } + }); + + const resetArticleForm = () => { + setArticleForm({ + title: "", + slug: "", + content: "", + excerpt: "", + imageUrl: "", + publishedMinutesAgo: 0, + isFeatured: false + }); + }; + + const openNewArticleDialog = () => { + resetArticleForm(); + setEditingArticle(null); + setIsArticleDialogOpen(true); + }; + + const openEditArticleDialog = (article: Article) => { + setArticleForm({ + title: article.title, + slug: article.slug, + content: article.content || "", + excerpt: article.excerpt || "", + imageUrl: article.imageUrl || "", + publishedMinutesAgo: article.publishedMinutesAgo, + isFeatured: article.isFeatured || false + }); + setEditingArticle(article); + setIsArticleDialogOpen(true); + }; + + const handleSubmitArticle = () => { + if (editingArticle) { + updateArticleMutation.mutate({ id: editingArticle.id, updates: articleForm }); + } else { + createArticleMutation.mutate(articleForm); + } + }; return (
@@ -84,7 +203,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan

Total Articles

-

0

+

{articles.length}

@@ -207,18 +326,90 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
- Article Management -
-
-
No articles yet
-
Create your first article
-
+ {articlesLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : articles.length === 0 ? ( +
+
No articles yet
+
Create your first article
+
+ ) : ( +
+ {articles.map((article) => ( +
+
+
+

{article.title}

+ {article.isFeatured && ( + + Featured + + )} +
+

{article.excerpt || "No excerpt"}

+
+ Published {article.publishedMinutesAgo} minutes ago • Slug: {article.slug} +
+
+
+ + + +
+
+ ))} +
+ )}
@@ -278,6 +469,112 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
+ + {/* Article Dialog */} + + + + {editingArticle ? "Edit Article" : "Create New Article"} + + {editingArticle ? "Update the article details below." : "Fill in the details to create a new article."} + + +
+
+ + setArticleForm({ ...articleForm, title: e.target.value })} + placeholder="Enter article title" + data-testid="input-article-title" + /> +
+
+ + setArticleForm({ ...articleForm, slug: e.target.value })} + placeholder="article-slug-url" + data-testid="input-article-slug" + /> +

URL-friendly identifier (e.g., my-article-title)

+
+
+ +