Add community features and enhance media outlet management
Integrates community post functionality (CRUD, likes, replies) and updates media outlet management with new icons and API endpoints for community posts. 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
This commit is contained in:
@ -6,10 +6,10 @@ 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import { ArrowLeft, Edit, Plus, BarChart3, Gavel, MessageSquare, Trash2, Eye } from "lucide-react";
|
||||
import { ArrowLeft, Edit, Plus, BarChart3, Gavel, MessageSquare, Trash2, Eye, Pin, Bell } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { queryClient, apiRequest } from "@/lib/queryClient";
|
||||
import type { MediaOutlet, Article } from "@shared/schema";
|
||||
import type { MediaOutlet, Article, CommunityPost } from "@shared/schema";
|
||||
|
||||
interface MediaOutletManagementProps {
|
||||
outlet: MediaOutlet;
|
||||
@ -35,9 +35,13 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
||||
queryKey: [`/api/media-outlets/${outlet.slug}/articles`],
|
||||
});
|
||||
|
||||
const { data: communityPosts = [], isLoading: communityLoading } = useQuery<CommunityPost[]>({
|
||||
queryKey: [`/api/media-outlets/${outlet.slug}/community`],
|
||||
});
|
||||
|
||||
const createArticleMutation = useMutation({
|
||||
mutationFn: async (data: typeof articleForm) => {
|
||||
return await apiRequest("/api/articles", "POST", { ...data, mediaOutletId: outlet.id });
|
||||
return await apiRequest("POST", "/api/articles", { ...data, mediaOutletId: outlet.id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
|
||||
@ -59,7 +63,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
||||
|
||||
const updateArticleMutation = useMutation({
|
||||
mutationFn: async (data: { id: string; updates: Partial<typeof articleForm> }) => {
|
||||
return await apiRequest(`/api/articles/${data.id}`, "PATCH", data.updates);
|
||||
return await apiRequest("PATCH", `/api/articles/${data.id}`, data.updates);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
|
||||
@ -82,7 +86,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
||||
|
||||
const deleteArticleMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
return await apiRequest(`/api/articles/${id}`, "DELETE", {});
|
||||
return await apiRequest("DELETE", `/api/articles/${id}`, {});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
|
||||
@ -100,6 +104,46 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
||||
}
|
||||
});
|
||||
|
||||
const updateCommunityPostMutation = useMutation({
|
||||
mutationFn: async (data: { id: string; updates: Partial<CommunityPost> }) => {
|
||||
return await apiRequest("PATCH", `/api/community/posts/${data.id}`, data.updates);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/community`] });
|
||||
toast({
|
||||
title: "Post Updated",
|
||||
description: "The community post has been updated successfully.",
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to update post",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const deleteCommunityPostMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
return await apiRequest("DELETE", `/api/community/posts/${id}`, {});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/community`] });
|
||||
toast({
|
||||
title: "Post Deleted",
|
||||
description: "The community post has been deleted successfully.",
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to delete post",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const resetArticleForm = () => {
|
||||
setArticleForm({
|
||||
title: "",
|
||||
@ -193,7 +237,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
||||
<TabsTrigger value="articles" data-testid="tab-articles">Articles</TabsTrigger>
|
||||
<TabsTrigger value="predictions" data-testid="tab-predictions">Prediction Markets</TabsTrigger>
|
||||
<TabsTrigger value="auctions" data-testid="tab-auctions">Auctions</TabsTrigger>
|
||||
<TabsTrigger value="comments" data-testid="tab-comments">Comments</TabsTrigger>
|
||||
<TabsTrigger value="community" data-testid="tab-community">Community</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="mt-6">
|
||||
@ -454,16 +498,109 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="comments" className="mt-6">
|
||||
<TabsContent value="community" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Comment Management</CardTitle>
|
||||
<CardTitle>Community Management ({communityPosts.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-lg mb-2">No comments to manage</div>
|
||||
<div className="text-gray-500 text-sm">Comments will appear here when posted</div>
|
||||
</div>
|
||||
{communityLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="animate-pulse flex items-center space-x-4 p-4 border rounded-lg">
|
||||
<div className="flex-1">
|
||||
<div className="h-4 bg-gray-200 rounded mb-2 w-3/4"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-8 w-16 bg-gray-200 rounded"></div>
|
||||
<div className="h-8 w-16 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : communityPosts.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-lg mb-2">No community posts yet</div>
|
||||
<div className="text-gray-500 text-sm">Posts will appear here when users create them</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{communityPosts.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition"
|
||||
data-testid={`community-post-item-${post.id}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium text-gray-900 truncate">{post.title}</h3>
|
||||
{post.isPinned && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 rounded">
|
||||
<Pin className="h-3 w-3 mr-1" />
|
||||
Pinned
|
||||
</span>
|
||||
)}
|
||||
{post.isNotice && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-red-100 text-red-800 rounded">
|
||||
<Bell className="h-3 w-3 mr-1" />
|
||||
Notice
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 truncate mt-1">{post.content}</p>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Views: {post.viewCount} • Likes: {post.likeCount} • Replies: {post.replyCount}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateCommunityPostMutation.mutate({
|
||||
id: post.id,
|
||||
updates: { isPinned: !post.isPinned }
|
||||
})}
|
||||
data-testid={`button-pin-${post.id}`}
|
||||
>
|
||||
<Pin className={`h-4 w-4 ${post.isPinned ? 'text-blue-600' : ''}`} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateCommunityPostMutation.mutate({
|
||||
id: post.id,
|
||||
updates: { isNotice: !post.isNotice }
|
||||
})}
|
||||
data-testid={`button-notice-${post.id}`}
|
||||
>
|
||||
<Bell className={`h-4 w-4 ${post.isNotice ? 'text-red-600' : ''}`} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/media/${outlet.slug}/community/${post.id}`, '_blank')}
|
||||
data-testid={`button-view-post-${post.id}`}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to delete this community post?')) {
|
||||
deleteCommunityPostMutation.mutate(post.id);
|
||||
}
|
||||
}}
|
||||
data-testid={`button-delete-post-${post.id}`}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
@ -38,7 +38,7 @@ export default function Community() {
|
||||
|
||||
const createPostMutation = useMutation({
|
||||
mutationFn: async (data: { title: string; content: string; imageUrl?: string }) => {
|
||||
return await apiRequest(`/api/media-outlets/${slug}/community`, "POST", data);
|
||||
return await apiRequest("POST", `/api/media-outlets/${slug}/community`, data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
|
||||
@ -36,7 +36,7 @@ export default function CommunityPostPage() {
|
||||
|
||||
const likePostMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await apiRequest(`/api/community/posts/${postId}/like`, "POST", {});
|
||||
return await apiRequest("POST", `/api/community/posts/${postId}/like`, {});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/community/posts/${postId}`] });
|
||||
@ -45,7 +45,7 @@ export default function CommunityPostPage() {
|
||||
|
||||
const createReplyMutation = useMutation({
|
||||
mutationFn: async (content: string) => {
|
||||
return await apiRequest(`/api/community/posts/${postId}/replies`, "POST", { content });
|
||||
return await apiRequest("POST", `/api/community/posts/${postId}/replies`, { content });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/community/posts/${postId}/replies`] });
|
||||
@ -56,7 +56,7 @@ export default function CommunityPostPage() {
|
||||
|
||||
const deletePostMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await apiRequest(`/api/community/posts/${postId}`, "DELETE", {});
|
||||
return await apiRequest("DELETE", `/api/community/posts/${postId}`, {});
|
||||
},
|
||||
onSuccess: () => {
|
||||
setLocation(`/media/${slug}/community`);
|
||||
|
||||
Reference in New Issue
Block a user