Add community features to media outlets and improve UI
Implement community section for each media outlet, including post creation, viewing, and replies, along with navigation and backend API routes. 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/AX0T336
This commit is contained in:
149
server/routes.ts
149
server/routes.ts
@ -2,7 +2,7 @@ import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { setupAuth, isAuthenticated } from "./simpleAuth";
|
||||
import { insertArticleSchema, insertMediaOutletRequestSchema, insertBidSchema, insertCommentSchema, insertPredictionBetSchema } from "@shared/schema";
|
||||
import { insertArticleSchema, insertMediaOutletRequestSchema, insertBidSchema, insertCommentSchema, insertPredictionBetSchema, insertCommunityPostSchema, insertCommunityReplySchema } from "@shared/schema";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Auth middleware
|
||||
@ -392,6 +392,153 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Community routes
|
||||
app.get('/api/media-outlets/:slug/community', async (req, res) => {
|
||||
try {
|
||||
const outlet = await storage.getMediaOutletBySlug(req.params.slug);
|
||||
if (!outlet) {
|
||||
return res.status(404).json({ message: "Media outlet not found" });
|
||||
}
|
||||
|
||||
const sort = req.query.sort as string || 'latest';
|
||||
const posts = await storage.getCommunityPostsByOutlet(outlet.id, sort);
|
||||
res.json(posts);
|
||||
} catch (error) {
|
||||
console.error("Error fetching community posts:", error);
|
||||
res.status(500).json({ message: "Failed to fetch community posts" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/community/posts/:id', async (req, res) => {
|
||||
try {
|
||||
const post = await storage.getCommunityPostById(req.params.id);
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" });
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
await storage.incrementPostViews(req.params.id);
|
||||
|
||||
res.json(post);
|
||||
} catch (error) {
|
||||
console.error("Error fetching community post:", error);
|
||||
res.status(500).json({ message: "Failed to fetch community post" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/media-outlets/:slug/community', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const outlet = await storage.getMediaOutletBySlug(req.params.slug);
|
||||
if (!outlet) {
|
||||
return res.status(404).json({ message: "Media outlet not found" });
|
||||
}
|
||||
|
||||
const userId = req.user.claims?.sub || req.user.id;
|
||||
const postData = insertCommunityPostSchema.parse({
|
||||
...req.body,
|
||||
mediaOutletId: outlet.id,
|
||||
authorId: userId
|
||||
});
|
||||
|
||||
const post = await storage.createCommunityPost(postData);
|
||||
res.status(201).json(post);
|
||||
} catch (error) {
|
||||
console.error("Error creating community post:", error);
|
||||
res.status(500).json({ message: "Failed to create community post" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/community/posts/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims?.sub || req.user.id;
|
||||
const post = await storage.getCommunityPostById(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" });
|
||||
}
|
||||
|
||||
if (post.authorId !== userId) {
|
||||
return res.status(403).json({ message: "Not authorized to edit this post" });
|
||||
}
|
||||
|
||||
const updated = await storage.updateCommunityPost(req.params.id, req.body);
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error("Error updating community post:", error);
|
||||
res.status(500).json({ message: "Failed to update community post" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/community/posts/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims?.sub || req.user.id;
|
||||
const user = await storage.getUser(userId);
|
||||
const post = await storage.getCommunityPostById(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" });
|
||||
}
|
||||
|
||||
if (post.authorId !== userId && user?.role !== 'admin' && user?.role !== 'superadmin') {
|
||||
return res.status(403).json({ message: "Not authorized to delete this post" });
|
||||
}
|
||||
|
||||
await storage.deleteCommunityPost(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error("Error deleting community post:", error);
|
||||
res.status(500).json({ message: "Failed to delete community post" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/community/posts/:id/like', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const post = await storage.getCommunityPostById(req.params.id);
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" });
|
||||
}
|
||||
|
||||
await storage.incrementPostLikes(req.params.id);
|
||||
const updated = await storage.getCommunityPostById(req.params.id);
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error("Error liking post:", error);
|
||||
res.status(500).json({ message: "Failed to like post" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/community/posts/:id/replies', async (req, res) => {
|
||||
try {
|
||||
const replies = await storage.getRepliesByPost(req.params.id);
|
||||
res.json(replies);
|
||||
} catch (error) {
|
||||
console.error("Error fetching replies:", error);
|
||||
res.status(500).json({ message: "Failed to fetch replies" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/community/posts/:id/replies', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const post = await storage.getCommunityPostById(req.params.id);
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" });
|
||||
}
|
||||
|
||||
const userId = req.user.claims?.sub || req.user.id;
|
||||
const replyData = insertCommunityReplySchema.parse({
|
||||
...req.body,
|
||||
postId: req.params.id,
|
||||
authorId: userId
|
||||
});
|
||||
|
||||
const reply = await storage.createCommunityReply(replyData);
|
||||
res.status(201).json(reply);
|
||||
} catch (error) {
|
||||
console.error("Error creating reply:", error);
|
||||
res.status(500).json({ message: "Failed to create reply" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
mediaOutletRequests,
|
||||
comments,
|
||||
predictionBets,
|
||||
communityPosts,
|
||||
communityReplies,
|
||||
type User,
|
||||
type UpsertUser,
|
||||
type MediaOutlet,
|
||||
@ -26,6 +28,10 @@ import {
|
||||
type InsertComment,
|
||||
type PredictionBet,
|
||||
type InsertPredictionBet,
|
||||
type CommunityPost,
|
||||
type InsertCommunityPost,
|
||||
type CommunityReply,
|
||||
type InsertCommunityReply,
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, desc, asc, and, ilike, sql } from "drizzle-orm";
|
||||
@ -77,6 +83,18 @@ export interface IStorage {
|
||||
getCommentsByArticle(articleId: string): Promise<Comment[]>;
|
||||
createComment(comment: InsertComment): Promise<Comment>;
|
||||
|
||||
// Community operations
|
||||
getCommunityPostsByOutlet(mediaOutletId: string, sort?: string): Promise<CommunityPost[]>;
|
||||
getCommunityPostById(id: string): Promise<CommunityPost | undefined>;
|
||||
createCommunityPost(post: InsertCommunityPost): Promise<CommunityPost>;
|
||||
updateCommunityPost(id: string, post: Partial<InsertCommunityPost>): Promise<CommunityPost>;
|
||||
deleteCommunityPost(id: string): Promise<void>;
|
||||
incrementPostViews(id: string): Promise<void>;
|
||||
incrementPostLikes(id: string): Promise<void>;
|
||||
|
||||
getRepliesByPost(postId: string): Promise<CommunityReply[]>;
|
||||
createCommunityReply(reply: InsertCommunityReply): Promise<CommunityReply>;
|
||||
|
||||
// Analytics operations
|
||||
getAnalytics(): Promise<{
|
||||
totalArticles: number;
|
||||
@ -368,6 +386,81 @@ export class DatabaseStorage implements IStorage {
|
||||
.where(eq(predictionBets.userId, userId))
|
||||
.orderBy(desc(predictionBets.createdAt));
|
||||
}
|
||||
|
||||
// Community operations
|
||||
async getCommunityPostsByOutlet(mediaOutletId: string, sort: string = 'latest'): Promise<CommunityPost[]> {
|
||||
const query = db
|
||||
.select()
|
||||
.from(communityPosts)
|
||||
.where(eq(communityPosts.mediaOutletId, mediaOutletId));
|
||||
|
||||
if (sort === 'views') {
|
||||
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.viewCount), desc(communityPosts.createdAt));
|
||||
} else if (sort === 'likes') {
|
||||
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.likeCount), desc(communityPosts.createdAt));
|
||||
} else if (sort === 'replies') {
|
||||
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.replyCount), desc(communityPosts.createdAt));
|
||||
} else {
|
||||
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.createdAt));
|
||||
}
|
||||
}
|
||||
|
||||
async getCommunityPostById(id: string): Promise<CommunityPost | undefined> {
|
||||
const [post] = await db.select().from(communityPosts).where(eq(communityPosts.id, id));
|
||||
return post;
|
||||
}
|
||||
|
||||
async createCommunityPost(post: InsertCommunityPost): Promise<CommunityPost> {
|
||||
const [newPost] = await db.insert(communityPosts).values(post).returning();
|
||||
return newPost;
|
||||
}
|
||||
|
||||
async updateCommunityPost(id: string, post: Partial<InsertCommunityPost>): Promise<CommunityPost> {
|
||||
const [updated] = await db
|
||||
.update(communityPosts)
|
||||
.set({ ...post, updatedAt: new Date() })
|
||||
.where(eq(communityPosts.id, id))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteCommunityPost(id: string): Promise<void> {
|
||||
await db.delete(communityPosts).where(eq(communityPosts.id, id));
|
||||
}
|
||||
|
||||
async incrementPostViews(id: string): Promise<void> {
|
||||
await db
|
||||
.update(communityPosts)
|
||||
.set({ viewCount: sql`view_count + 1` })
|
||||
.where(eq(communityPosts.id, id));
|
||||
}
|
||||
|
||||
async incrementPostLikes(id: string): Promise<void> {
|
||||
await db
|
||||
.update(communityPosts)
|
||||
.set({ likeCount: sql`like_count + 1` })
|
||||
.where(eq(communityPosts.id, id));
|
||||
}
|
||||
|
||||
async getRepliesByPost(postId: string): Promise<CommunityReply[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(communityReplies)
|
||||
.where(eq(communityReplies.postId, postId))
|
||||
.orderBy(asc(communityReplies.createdAt));
|
||||
}
|
||||
|
||||
async createCommunityReply(reply: InsertCommunityReply): Promise<CommunityReply> {
|
||||
const [newReply] = await db.insert(communityReplies).values(reply).returning();
|
||||
|
||||
// Increment reply count on the post
|
||||
await db
|
||||
.update(communityPosts)
|
||||
.set({ replyCount: sql`reply_count + 1` })
|
||||
.where(eq(communityPosts.id, reply.postId));
|
||||
|
||||
return newReply;
|
||||
}
|
||||
|
||||
// Analytics operations
|
||||
async getAnalytics(): Promise<{
|
||||
|
||||
Reference in New Issue
Block a user