Add core UI components and layout for media platform
Initializes the client-side application with fundamental UI components, including navigation, cards for articles and auctions, and various elements for user interaction and display. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 069d4324-6c40-4355-955e-c714a50de1ea Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/069d4324-6c40-4355-955e-c714a50de1ea/bVdKIaU
This commit is contained in:
15
server/db.ts
Normal file
15
server/db.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Pool, neonConfig } from '@neondatabase/serverless';
|
||||
import { drizzle } from 'drizzle-orm/neon-serverless';
|
||||
import ws from "ws";
|
||||
import * as schema from "@shared/schema";
|
||||
|
||||
neonConfig.webSocketConstructor = ws;
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error(
|
||||
"DATABASE_URL must be set. Did you forget to provision a database?",
|
||||
);
|
||||
}
|
||||
|
||||
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
export const db = drizzle({ client: pool, schema });
|
||||
71
server/index.ts
Normal file
71
server/index.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
||||
|
||||
const originalResJson = res.json;
|
||||
res.json = function (bodyJson, ...args) {
|
||||
capturedJsonResponse = bodyJson;
|
||||
return originalResJson.apply(res, [bodyJson, ...args]);
|
||||
};
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
if (path.startsWith("/api")) {
|
||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||
if (capturedJsonResponse) {
|
||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||
}
|
||||
|
||||
if (logLine.length > 80) {
|
||||
logLine = logLine.slice(0, 79) + "…";
|
||||
}
|
||||
|
||||
log(logLine);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const server = await registerRoutes(app);
|
||||
|
||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
const message = err.message || "Internal Server Error";
|
||||
|
||||
res.status(status).json({ message });
|
||||
throw err;
|
||||
});
|
||||
|
||||
// importantly only setup vite in development and after
|
||||
// setting up all the other routes so the catch-all route
|
||||
// doesn't interfere with the other routes
|
||||
if (app.get("env") === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// ALWAYS serve the app on the port specified in the environment variable PORT
|
||||
// Other ports are firewalled. Default to 5000 if not specified.
|
||||
// this serves both the API and the client.
|
||||
// It is the only port that is not firewalled.
|
||||
const port = parseInt(process.env.PORT || '5000', 10);
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
reusePort: true,
|
||||
}, () => {
|
||||
log(`serving on port ${port}`);
|
||||
});
|
||||
})();
|
||||
157
server/replitAuth.ts
Normal file
157
server/replitAuth.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import * as client from "openid-client";
|
||||
import { Strategy, type VerifyFunction } from "openid-client/passport";
|
||||
|
||||
import passport from "passport";
|
||||
import session from "express-session";
|
||||
import type { Express, RequestHandler } from "express";
|
||||
import memoize from "memoizee";
|
||||
import connectPg from "connect-pg-simple";
|
||||
import { storage } from "./storage";
|
||||
|
||||
if (!process.env.REPLIT_DOMAINS) {
|
||||
throw new Error("Environment variable REPLIT_DOMAINS not provided");
|
||||
}
|
||||
|
||||
const getOidcConfig = memoize(
|
||||
async () => {
|
||||
return await client.discovery(
|
||||
new URL(process.env.ISSUER_URL ?? "https://replit.com/oidc"),
|
||||
process.env.REPL_ID!
|
||||
);
|
||||
},
|
||||
{ maxAge: 3600 * 1000 }
|
||||
);
|
||||
|
||||
export function getSession() {
|
||||
const sessionTtl = 7 * 24 * 60 * 60 * 1000; // 1 week
|
||||
const pgStore = connectPg(session);
|
||||
const sessionStore = new pgStore({
|
||||
conString: process.env.DATABASE_URL,
|
||||
createTableIfMissing: false,
|
||||
ttl: sessionTtl,
|
||||
tableName: "sessions",
|
||||
});
|
||||
return session({
|
||||
secret: process.env.SESSION_SECRET!,
|
||||
store: sessionStore,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
maxAge: sessionTtl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserSession(
|
||||
user: any,
|
||||
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers
|
||||
) {
|
||||
user.claims = tokens.claims();
|
||||
user.access_token = tokens.access_token;
|
||||
user.refresh_token = tokens.refresh_token;
|
||||
user.expires_at = user.claims?.exp;
|
||||
}
|
||||
|
||||
async function upsertUser(
|
||||
claims: any,
|
||||
) {
|
||||
await storage.upsertUser({
|
||||
id: claims["sub"],
|
||||
email: claims["email"],
|
||||
firstName: claims["first_name"],
|
||||
lastName: claims["last_name"],
|
||||
profileImageUrl: claims["profile_image_url"],
|
||||
});
|
||||
}
|
||||
|
||||
export async function setupAuth(app: Express) {
|
||||
app.set("trust proxy", 1);
|
||||
app.use(getSession());
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
const config = await getOidcConfig();
|
||||
|
||||
const verify: VerifyFunction = async (
|
||||
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers,
|
||||
verified: passport.AuthenticateCallback
|
||||
) => {
|
||||
const user = {};
|
||||
updateUserSession(user, tokens);
|
||||
await upsertUser(tokens.claims());
|
||||
verified(null, user);
|
||||
};
|
||||
|
||||
for (const domain of process.env
|
||||
.REPLIT_DOMAINS!.split(",")) {
|
||||
const strategy = new Strategy(
|
||||
{
|
||||
name: `replitauth:${domain}`,
|
||||
config,
|
||||
scope: "openid email profile offline_access",
|
||||
callbackURL: `https://${domain}/api/callback`,
|
||||
},
|
||||
verify,
|
||||
);
|
||||
passport.use(strategy);
|
||||
}
|
||||
|
||||
passport.serializeUser((user: Express.User, cb) => cb(null, user));
|
||||
passport.deserializeUser((user: Express.User, cb) => cb(null, user));
|
||||
|
||||
app.get("/api/login", (req, res, next) => {
|
||||
passport.authenticate(`replitauth:${req.hostname}`, {
|
||||
prompt: "login consent",
|
||||
scope: ["openid", "email", "profile", "offline_access"],
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.get("/api/callback", (req, res, next) => {
|
||||
passport.authenticate(`replitauth:${req.hostname}`, {
|
||||
successReturnToOrRedirect: "/",
|
||||
failureRedirect: "/api/login",
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.get("/api/logout", (req, res) => {
|
||||
req.logout(() => {
|
||||
res.redirect(
|
||||
client.buildEndSessionUrl(config, {
|
||||
client_id: process.env.REPL_ID!,
|
||||
post_logout_redirect_uri: `${req.protocol}://${req.hostname}`,
|
||||
}).href
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const isAuthenticated: RequestHandler = async (req, res, next) => {
|
||||
const user = req.user as any;
|
||||
|
||||
if (!req.isAuthenticated() || !user.expires_at) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (now <= user.expires_at) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const refreshToken = user.refresh_token;
|
||||
if (!refreshToken) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getOidcConfig();
|
||||
const tokenResponse = await client.refreshTokenGrant(config, refreshToken);
|
||||
updateUserSession(user, tokenResponse);
|
||||
return next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
};
|
||||
246
server/routes.ts
Normal file
246
server/routes.ts
Normal file
@ -0,0 +1,246 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { setupAuth, isAuthenticated } from "./replitAuth";
|
||||
import { insertArticleSchema, insertMediaOutletRequestSchema, insertBidSchema, insertCommentSchema } from "@shared/schema";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Auth middleware
|
||||
await setupAuth(app);
|
||||
|
||||
// Auth routes
|
||||
app.get('/api/auth/user', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user:", error);
|
||||
res.status(500).json({ message: "Failed to fetch user" });
|
||||
}
|
||||
});
|
||||
|
||||
// Media outlet routes
|
||||
app.get('/api/media-outlets', async (req, res) => {
|
||||
try {
|
||||
const category = req.query.category as string;
|
||||
const outlets = await storage.getMediaOutlets(category);
|
||||
res.json(outlets);
|
||||
} catch (error) {
|
||||
console.error("Error fetching media outlets:", error);
|
||||
res.status(500).json({ message: "Failed to fetch media outlets" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/media-outlets/:slug', async (req, res) => {
|
||||
try {
|
||||
const outlet = await storage.getMediaOutletBySlug(req.params.slug);
|
||||
if (!outlet) {
|
||||
return res.status(404).json({ message: "Media outlet not found" });
|
||||
}
|
||||
res.json(outlet);
|
||||
} catch (error) {
|
||||
console.error("Error fetching media outlet:", error);
|
||||
res.status(500).json({ message: "Failed to fetch media outlet" });
|
||||
}
|
||||
});
|
||||
|
||||
// Article routes
|
||||
app.get('/api/media-outlets/:outletId/articles', async (req, res) => {
|
||||
try {
|
||||
const articles = await storage.getArticlesByOutlet(req.params.outletId);
|
||||
res.json(articles);
|
||||
} catch (error) {
|
||||
console.error("Error fetching articles:", error);
|
||||
res.status(500).json({ message: "Failed to fetch articles" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/articles/:slug', async (req, res) => {
|
||||
try {
|
||||
const article = await storage.getArticleBySlug(req.params.slug);
|
||||
if (!article) {
|
||||
return res.status(404).json({ message: "Article not found" });
|
||||
}
|
||||
res.json(article);
|
||||
} catch (error) {
|
||||
console.error("Error fetching article:", error);
|
||||
res.status(500).json({ message: "Failed to fetch article" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/articles/featured', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
const articles = await storage.getFeaturedArticles(limit);
|
||||
res.json(articles);
|
||||
} catch (error) {
|
||||
console.error("Error fetching featured articles:", error);
|
||||
res.status(500).json({ message: "Failed to fetch featured articles" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/articles', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'superadmin')) {
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
const articleData = insertArticleSchema.parse({
|
||||
...req.body,
|
||||
authorId: userId
|
||||
});
|
||||
|
||||
const article = await storage.createArticle(articleData);
|
||||
res.status(201).json(article);
|
||||
} catch (error) {
|
||||
console.error("Error creating article:", error);
|
||||
res.status(500).json({ message: "Failed to create article" });
|
||||
}
|
||||
});
|
||||
|
||||
// Prediction market routes
|
||||
app.get('/api/prediction-markets', async (req, res) => {
|
||||
try {
|
||||
const articleId = req.query.articleId as string;
|
||||
const markets = await storage.getPredictionMarkets(articleId);
|
||||
res.json(markets);
|
||||
} catch (error) {
|
||||
console.error("Error fetching prediction markets:", error);
|
||||
res.status(500).json({ message: "Failed to fetch prediction markets" });
|
||||
}
|
||||
});
|
||||
|
||||
// Auction routes
|
||||
app.get('/api/auctions', async (req, res) => {
|
||||
try {
|
||||
const auctions = await storage.getActiveAuctions();
|
||||
res.json(auctions);
|
||||
} catch (error) {
|
||||
console.error("Error fetching auctions:", error);
|
||||
res.status(500).json({ message: "Failed to fetch auctions" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auctions/:id/bid', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const bidData = insertBidSchema.parse({
|
||||
...req.body,
|
||||
auctionId: req.params.id,
|
||||
bidderId: userId
|
||||
});
|
||||
|
||||
const bid = await storage.placeBid(bidData);
|
||||
res.status(201).json(bid);
|
||||
} catch (error) {
|
||||
console.error("Error placing bid:", error);
|
||||
res.status(500).json({ message: "Failed to place bid" });
|
||||
}
|
||||
});
|
||||
|
||||
// Media outlet request routes
|
||||
app.get('/api/media-outlet-requests', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
if (!user || user.role !== 'superadmin') {
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
const status = req.query.status as string;
|
||||
const requests = await storage.getMediaOutletRequests(status);
|
||||
res.json(requests);
|
||||
} catch (error) {
|
||||
console.error("Error fetching requests:", error);
|
||||
res.status(500).json({ message: "Failed to fetch requests" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/media-outlet-requests', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const requestData = insertMediaOutletRequestSchema.parse({
|
||||
...req.body,
|
||||
requesterId: userId
|
||||
});
|
||||
|
||||
const request = await storage.createMediaOutletRequest(requestData);
|
||||
res.status(201).json(request);
|
||||
} catch (error) {
|
||||
console.error("Error creating request:", error);
|
||||
res.status(500).json({ message: "Failed to create request" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/media-outlet-requests/:id', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
if (!user || user.role !== 'superadmin') {
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
const { status } = req.body;
|
||||
const request = await storage.updateMediaOutletRequestStatus(req.params.id, status, userId);
|
||||
res.json(request);
|
||||
} catch (error) {
|
||||
console.error("Error updating request:", error);
|
||||
res.status(500).json({ message: "Failed to update request" });
|
||||
}
|
||||
});
|
||||
|
||||
// Comment routes
|
||||
app.get('/api/articles/:articleId/comments', async (req, res) => {
|
||||
try {
|
||||
const comments = await storage.getCommentsByArticle(req.params.articleId);
|
||||
res.json(comments);
|
||||
} catch (error) {
|
||||
console.error("Error fetching comments:", error);
|
||||
res.status(500).json({ message: "Failed to fetch comments" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/articles/:articleId/comments', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const commentData = insertCommentSchema.parse({
|
||||
...req.body,
|
||||
articleId: req.params.articleId,
|
||||
authorId: userId
|
||||
});
|
||||
|
||||
const comment = await storage.createComment(commentData);
|
||||
res.status(201).json(comment);
|
||||
} catch (error) {
|
||||
console.error("Error creating comment:", error);
|
||||
res.status(500).json({ message: "Failed to create comment" });
|
||||
}
|
||||
});
|
||||
|
||||
// Analytics routes
|
||||
app.get('/api/analytics', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.claims.sub;
|
||||
const user = await storage.getUser(userId);
|
||||
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'superadmin')) {
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
const analytics = await storage.getAnalytics();
|
||||
res.json(analytics);
|
||||
} catch (error) {
|
||||
console.error("Error fetching analytics:", error);
|
||||
res.status(500).json({ message: "Failed to fetch analytics" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
return httpServer;
|
||||
}
|
||||
290
server/storage.ts
Normal file
290
server/storage.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import {
|
||||
users,
|
||||
mediaOutlets,
|
||||
articles,
|
||||
predictionMarkets,
|
||||
auctions,
|
||||
bids,
|
||||
mediaOutletRequests,
|
||||
comments,
|
||||
type User,
|
||||
type UpsertUser,
|
||||
type MediaOutlet,
|
||||
type InsertMediaOutlet,
|
||||
type Article,
|
||||
type InsertArticle,
|
||||
type PredictionMarket,
|
||||
type InsertPredictionMarket,
|
||||
type Auction,
|
||||
type InsertAuction,
|
||||
type Bid,
|
||||
type InsertBid,
|
||||
type MediaOutletRequest,
|
||||
type InsertMediaOutletRequest,
|
||||
type Comment,
|
||||
type InsertComment,
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, desc, and, ilike, sql } from "drizzle-orm";
|
||||
|
||||
// Interface for storage operations
|
||||
export interface IStorage {
|
||||
// User operations (mandatory for Replit Auth)
|
||||
getUser(id: string): Promise<User | undefined>;
|
||||
upsertUser(user: UpsertUser): Promise<User>;
|
||||
|
||||
// Media outlet operations
|
||||
getMediaOutlets(category?: string): Promise<MediaOutlet[]>;
|
||||
getMediaOutletBySlug(slug: string): Promise<MediaOutlet | undefined>;
|
||||
createMediaOutlet(outlet: InsertMediaOutlet): Promise<MediaOutlet>;
|
||||
updateMediaOutlet(id: string, outlet: Partial<InsertMediaOutlet>): Promise<MediaOutlet>;
|
||||
|
||||
// Article operations
|
||||
getArticlesByOutlet(mediaOutletId: string): Promise<Article[]>;
|
||||
getArticleBySlug(slug: string): Promise<Article | undefined>;
|
||||
createArticle(article: InsertArticle): Promise<Article>;
|
||||
updateArticle(id: string, article: Partial<InsertArticle>): Promise<Article>;
|
||||
getFeaturedArticles(limit?: number): Promise<Article[]>;
|
||||
|
||||
// Prediction market operations
|
||||
getPredictionMarkets(articleId?: string): Promise<PredictionMarket[]>;
|
||||
createPredictionMarket(market: InsertPredictionMarket): Promise<PredictionMarket>;
|
||||
|
||||
// Auction operations
|
||||
getActiveAuctions(): Promise<Auction[]>;
|
||||
getAuctionById(id: string): Promise<Auction | undefined>;
|
||||
createAuction(auction: InsertAuction): Promise<Auction>;
|
||||
placeBid(bid: InsertBid): Promise<Bid>;
|
||||
|
||||
// Media outlet request operations
|
||||
getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]>;
|
||||
createMediaOutletRequest(request: InsertMediaOutletRequest): Promise<MediaOutletRequest>;
|
||||
updateMediaOutletRequestStatus(id: string, status: string, reviewerId: string): Promise<MediaOutletRequest>;
|
||||
|
||||
// Comment operations
|
||||
getCommentsByArticle(articleId: string): Promise<Comment[]>;
|
||||
createComment(comment: InsertComment): Promise<Comment>;
|
||||
|
||||
// Analytics operations
|
||||
getAnalytics(): Promise<{
|
||||
totalArticles: number;
|
||||
activePredictions: number;
|
||||
liveAuctions: number;
|
||||
totalRevenue: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
// User operations (mandatory for Replit Auth)
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||||
return user;
|
||||
}
|
||||
|
||||
async upsertUser(userData: UpsertUser): Promise<User> {
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values(userData)
|
||||
.onConflictDoUpdate({
|
||||
target: users.id,
|
||||
set: {
|
||||
...userData,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
// Media outlet operations
|
||||
async getMediaOutlets(category?: string): Promise<MediaOutlet[]> {
|
||||
if (category) {
|
||||
return await db.select().from(mediaOutlets).where(and(eq(mediaOutlets.isActive, true), eq(mediaOutlets.category, category)));
|
||||
}
|
||||
|
||||
return await db.select().from(mediaOutlets).where(eq(mediaOutlets.isActive, true));
|
||||
}
|
||||
|
||||
async getMediaOutletBySlug(slug: string): Promise<MediaOutlet | undefined> {
|
||||
const [outlet] = await db.select().from(mediaOutlets).where(eq(mediaOutlets.slug, slug));
|
||||
return outlet;
|
||||
}
|
||||
|
||||
async createMediaOutlet(outlet: InsertMediaOutlet): Promise<MediaOutlet> {
|
||||
const [newOutlet] = await db.insert(mediaOutlets).values(outlet).returning();
|
||||
return newOutlet;
|
||||
}
|
||||
|
||||
async updateMediaOutlet(id: string, outlet: Partial<InsertMediaOutlet>): Promise<MediaOutlet> {
|
||||
const [updated] = await db
|
||||
.update(mediaOutlets)
|
||||
.set({ ...outlet, updatedAt: new Date() })
|
||||
.where(eq(mediaOutlets.id, id))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
// Article operations
|
||||
async getArticlesByOutlet(mediaOutletId: string): Promise<Article[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(articles)
|
||||
.where(eq(articles.mediaOutletId, mediaOutletId))
|
||||
.orderBy(desc(articles.publishedAt));
|
||||
}
|
||||
|
||||
async getArticleBySlug(slug: string): Promise<Article | undefined> {
|
||||
const [article] = await db.select().from(articles).where(eq(articles.slug, slug));
|
||||
return article;
|
||||
}
|
||||
|
||||
async createArticle(article: InsertArticle): Promise<Article> {
|
||||
const [newArticle] = await db.insert(articles).values(article).returning();
|
||||
return newArticle;
|
||||
}
|
||||
|
||||
async updateArticle(id: string, article: Partial<InsertArticle>): Promise<Article> {
|
||||
const [updated] = await db
|
||||
.update(articles)
|
||||
.set({ ...article, updatedAt: new Date() })
|
||||
.where(eq(articles.id, id))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
async getFeaturedArticles(limit = 10): Promise<Article[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(articles)
|
||||
.where(eq(articles.isFeatured, true))
|
||||
.orderBy(desc(articles.publishedAt))
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
// Prediction market operations
|
||||
async getPredictionMarkets(articleId?: string): Promise<PredictionMarket[]> {
|
||||
if (articleId) {
|
||||
return await db.select().from(predictionMarkets).where(and(eq(predictionMarkets.isActive, true), eq(predictionMarkets.articleId, articleId)));
|
||||
}
|
||||
|
||||
return await db.select().from(predictionMarkets).where(eq(predictionMarkets.isActive, true)).orderBy(desc(predictionMarkets.createdAt));
|
||||
}
|
||||
|
||||
async createPredictionMarket(market: InsertPredictionMarket): Promise<PredictionMarket> {
|
||||
const [newMarket] = await db.insert(predictionMarkets).values(market).returning();
|
||||
return newMarket;
|
||||
}
|
||||
|
||||
// Auction operations
|
||||
async getActiveAuctions(): Promise<Auction[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(auctions)
|
||||
.where(and(eq(auctions.isActive, true), sql`end_date > NOW()`))
|
||||
.orderBy(auctions.endDate);
|
||||
}
|
||||
|
||||
async getAuctionById(id: string): Promise<Auction | undefined> {
|
||||
const [auction] = await db.select().from(auctions).where(eq(auctions.id, id));
|
||||
return auction;
|
||||
}
|
||||
|
||||
async createAuction(auction: InsertAuction): Promise<Auction> {
|
||||
const [newAuction] = await db.insert(auctions).values(auction).returning();
|
||||
return newAuction;
|
||||
}
|
||||
|
||||
async placeBid(bid: InsertBid): Promise<Bid> {
|
||||
const [newBid] = await db.insert(bids).values(bid).returning();
|
||||
|
||||
// Update auction with highest bid
|
||||
await db
|
||||
.update(auctions)
|
||||
.set({
|
||||
currentBid: bid.amount,
|
||||
highestBidderId: bid.bidderId,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(auctions.id, bid.auctionId));
|
||||
|
||||
return newBid;
|
||||
}
|
||||
|
||||
// Media outlet request operations
|
||||
async getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]> {
|
||||
const query = db.select().from(mediaOutletRequests);
|
||||
|
||||
if (status) {
|
||||
return await query.where(eq(mediaOutletRequests.status, status));
|
||||
}
|
||||
|
||||
return await query.orderBy(desc(mediaOutletRequests.createdAt));
|
||||
}
|
||||
|
||||
async createMediaOutletRequest(request: InsertMediaOutletRequest): Promise<MediaOutletRequest> {
|
||||
const [newRequest] = await db.insert(mediaOutletRequests).values(request).returning();
|
||||
return newRequest;
|
||||
}
|
||||
|
||||
async updateMediaOutletRequestStatus(id: string, status: string, reviewerId: string): Promise<MediaOutletRequest> {
|
||||
const [updated] = await db
|
||||
.update(mediaOutletRequests)
|
||||
.set({
|
||||
status,
|
||||
reviewedBy: reviewerId,
|
||||
reviewedAt: new Date()
|
||||
})
|
||||
.where(eq(mediaOutletRequests.id, id))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
// Comment operations
|
||||
async getCommentsByArticle(articleId: string): Promise<Comment[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(comments)
|
||||
.where(eq(comments.articleId, articleId))
|
||||
.orderBy(desc(comments.isPinned), desc(comments.createdAt));
|
||||
}
|
||||
|
||||
async createComment(comment: InsertComment): Promise<Comment> {
|
||||
const [newComment] = await db.insert(comments).values(comment).returning();
|
||||
return newComment;
|
||||
}
|
||||
|
||||
// Analytics operations
|
||||
async getAnalytics(): Promise<{
|
||||
totalArticles: number;
|
||||
activePredictions: number;
|
||||
liveAuctions: number;
|
||||
totalRevenue: number;
|
||||
}> {
|
||||
const [articleCount] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(articles);
|
||||
|
||||
const [predictionCount] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(predictionMarkets)
|
||||
.where(eq(predictionMarkets.isActive, true));
|
||||
|
||||
const [auctionCount] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(auctions)
|
||||
.where(and(eq(auctions.isActive, true), sql`end_date > NOW()`));
|
||||
|
||||
const [revenueSum] = await db
|
||||
.select({ sum: sql<number>`COALESCE(SUM(current_bid), 0)` })
|
||||
.from(auctions);
|
||||
|
||||
return {
|
||||
totalArticles: articleCount.count,
|
||||
activePredictions: predictionCount.count,
|
||||
liveAuctions: auctionCount.count,
|
||||
totalRevenue: revenueSum.sum
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
85
server/vite.ts
Normal file
85
server/vite.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createServer as createViteServer, createLogger } from "vite";
|
||||
import { type Server } from "http";
|
||||
import viteConfig from "../vite.config";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const viteLogger = createLogger();
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
console.log(`${formattedTime} [${source}] ${message}`);
|
||||
}
|
||||
|
||||
export async function setupVite(app: Express, server: Server) {
|
||||
const serverOptions = {
|
||||
middlewareMode: true,
|
||||
hmr: { server },
|
||||
allowedHosts: true as const,
|
||||
};
|
||||
|
||||
const vite = await createViteServer({
|
||||
...viteConfig,
|
||||
configFile: false,
|
||||
customLogger: {
|
||||
...viteLogger,
|
||||
error: (msg, options) => {
|
||||
viteLogger.error(msg, options);
|
||||
process.exit(1);
|
||||
},
|
||||
},
|
||||
server: serverOptions,
|
||||
appType: "custom",
|
||||
});
|
||||
|
||||
app.use(vite.middlewares);
|
||||
app.use("*", async (req, res, next) => {
|
||||
const url = req.originalUrl;
|
||||
|
||||
try {
|
||||
const clientTemplate = path.resolve(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"client",
|
||||
"index.html",
|
||||
);
|
||||
|
||||
// always reload the index.html file from disk incase it changes
|
||||
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
||||
template = template.replace(
|
||||
`src="/src/main.tsx"`,
|
||||
`src="/src/main.tsx?v=${nanoid()}"`,
|
||||
);
|
||||
const page = await vite.transformIndexHtml(url, template);
|
||||
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
||||
} catch (e) {
|
||||
vite.ssrFixStacktrace(e as Error);
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath = path.resolve(import.meta.dirname, "public");
|
||||
|
||||
if (!fs.existsSync(distPath)) {
|
||||
throw new Error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`,
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user