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:
kimjaehyeon0101
2025-10-14 10:22:35 +00:00
parent 3829c9bc87
commit db93468add
5 changed files with 159 additions and 22 deletions

View File

@ -18,10 +18,6 @@ externalPort = 80
localPort = 34047 localPort = 34047
externalPort = 3002 externalPort = 3002
[[ports]]
localPort = 34595
externalPort = 6800
[[ports]] [[ports]]
localPort = 36309 localPort = 36309
externalPort = 5173 externalPort = 5173

View File

@ -6,10 +6,10 @@ import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; 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 { useToast } from "@/hooks/use-toast";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { queryClient, apiRequest } from "@/lib/queryClient";
import type { MediaOutlet, Article } from "@shared/schema"; import type { MediaOutlet, Article, CommunityPost } from "@shared/schema";
interface MediaOutletManagementProps { interface MediaOutletManagementProps {
outlet: MediaOutlet; outlet: MediaOutlet;
@ -35,9 +35,13 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
queryKey: [`/api/media-outlets/${outlet.slug}/articles`], queryKey: [`/api/media-outlets/${outlet.slug}/articles`],
}); });
const { data: communityPosts = [], isLoading: communityLoading } = useQuery<CommunityPost[]>({
queryKey: [`/api/media-outlets/${outlet.slug}/community`],
});
const createArticleMutation = useMutation({ const createArticleMutation = useMutation({
mutationFn: async (data: typeof articleForm) => { 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: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] }); queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
@ -59,7 +63,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
const updateArticleMutation = useMutation({ const updateArticleMutation = useMutation({
mutationFn: async (data: { id: string; updates: Partial<typeof articleForm> }) => { 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: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] }); queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] });
@ -82,7 +86,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
const deleteArticleMutation = useMutation({ const deleteArticleMutation = useMutation({
mutationFn: async (id: string) => { mutationFn: async (id: string) => {
return await apiRequest(`/api/articles/${id}`, "DELETE", {}); return await apiRequest("DELETE", `/api/articles/${id}`, {});
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/articles`] }); 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 = () => { const resetArticleForm = () => {
setArticleForm({ setArticleForm({
title: "", title: "",
@ -193,7 +237,7 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
<TabsTrigger value="articles" data-testid="tab-articles">Articles</TabsTrigger> <TabsTrigger value="articles" data-testid="tab-articles">Articles</TabsTrigger>
<TabsTrigger value="predictions" data-testid="tab-predictions">Prediction Markets</TabsTrigger> <TabsTrigger value="predictions" data-testid="tab-predictions">Prediction Markets</TabsTrigger>
<TabsTrigger value="auctions" data-testid="tab-auctions">Auctions</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> </TabsList>
<TabsContent value="overview" className="mt-6"> <TabsContent value="overview" className="mt-6">
@ -454,16 +498,109 @@ export default function MediaOutletManagement({ outlet, onBack }: MediaOutletMan
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="comments" className="mt-6"> <TabsContent value="community" className="mt-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Comment Management</CardTitle> <CardTitle>Community Management ({communityPosts.length})</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-center py-12"> {communityLoading ? (
<div className="text-gray-400 text-lg mb-2">No comments to manage</div> <div className="space-y-3">
<div className="text-gray-500 text-sm">Comments will appear here when posted</div> {Array.from({ length: 3 }).map((_, i) => (
</div> <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> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>

View File

@ -38,7 +38,7 @@ export default function Community() {
const createPostMutation = useMutation({ const createPostMutation = useMutation({
mutationFn: async (data: { title: string; content: string; imageUrl?: string }) => { 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: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({

View File

@ -36,7 +36,7 @@ export default function CommunityPostPage() {
const likePostMutation = useMutation({ const likePostMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
return await apiRequest(`/api/community/posts/${postId}/like`, "POST", {}); return await apiRequest("POST", `/api/community/posts/${postId}/like`, {});
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`/api/community/posts/${postId}`] }); queryClient.invalidateQueries({ queryKey: [`/api/community/posts/${postId}`] });
@ -45,7 +45,7 @@ export default function CommunityPostPage() {
const createReplyMutation = useMutation({ const createReplyMutation = useMutation({
mutationFn: async (content: string) => { 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: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [`/api/community/posts/${postId}/replies`] }); queryClient.invalidateQueries({ queryKey: [`/api/community/posts/${postId}/replies`] });
@ -56,7 +56,7 @@ export default function CommunityPostPage() {
const deletePostMutation = useMutation({ const deletePostMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
return await apiRequest(`/api/community/posts/${postId}`, "DELETE", {}); return await apiRequest("DELETE", `/api/community/posts/${postId}`, {});
}, },
onSuccess: () => { onSuccess: () => {
setLocation(`/media/${slug}/community`); setLocation(`/media/${slug}/community`);

View File

@ -30,6 +30,8 @@ The system uses a PostgreSQL database with the following core entities:
- **Auctions**: eBay-style bidding system for media outlet management rights - **Auctions**: eBay-style bidding system for media outlet management rights
- **Bids**: Individual auction bids with quality scoring - **Bids**: Individual auction bids with quality scoring
- **Comments**: User-generated content on articles - **Comments**: User-generated content on articles
- **Community Posts**: DC Inside-style forum posts for each media outlet with views, likes, and replies
- **Community Replies**: Threaded replies to community posts
## Authentication & Authorization ## Authentication & Authorization
- **Multi-tier Access**: Three user roles with escalating permissions - **Multi-tier Access**: Three user roles with escalating permissions
@ -41,8 +43,10 @@ The system uses a PostgreSQL database with the following core entities:
- **Dual View Modes**: Grid and list views for article browsing - **Dual View Modes**: Grid and list views for article browsing
- **Auction System**: Real-time bidding with quality score integration - **Auction System**: Real-time bidding with quality score integration
- **Prediction Markets**: Integration with external prediction market APIs - **Prediction Markets**: Integration with external prediction market APIs
- **Admin Dashboards**: Role-specific management interfaces - **Admin Dashboards**: Role-specific management interfaces with article CRUD operations
- **Content Management**: Rich article creation and media outlet administration - **Content Management**: Rich article creation, editing, and deletion with media outlet administration
- **DC Inside-style Community**: Forum-style discussion boards for each media outlet with posts, replies, likes, and sorting options
- **Community Features**: Post creation, threaded replies, view counts, like system, and multiple sort options (latest, views, likes, replies)
# External Dependencies # External Dependencies