Implements a search feature by adding a search modal, API endpoint, and storage logic for querying articles and media outlets. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 069d4324-6c40-4355-955e-c714a50de1ea Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/069d4324-6c40-4355-955e-c714a50de1ea/gVirbWH
431 lines
13 KiB
TypeScript
431 lines
13 KiB
TypeScript
import {
|
|
users,
|
|
mediaOutlets,
|
|
articles,
|
|
predictionMarkets,
|
|
auctions,
|
|
bids,
|
|
mediaOutletRequests,
|
|
comments,
|
|
predictionBets,
|
|
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,
|
|
type PredictionBet,
|
|
type InsertPredictionBet,
|
|
} 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>;
|
|
getAuctionByMediaOutlet(mediaOutletId: string): Promise<Auction | undefined>;
|
|
createAuction(auction: InsertAuction): Promise<Auction>;
|
|
placeBid(bid: InsertBid): Promise<Bid>;
|
|
getBidsByAuctionId(auctionId: string): 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>;
|
|
|
|
// Prediction bet operations
|
|
createPredictionBet(bet: InsertPredictionBet): Promise<PredictionBet>;
|
|
getPredictionBetsByMarket(marketId: string): Promise<PredictionBet[]>;
|
|
getPredictionBetsByUser(userId: string): Promise<PredictionBet[]>;
|
|
|
|
// Comment operations
|
|
getCommentsByArticle(articleId: string): Promise<Comment[]>;
|
|
createComment(comment: InsertComment): Promise<Comment>;
|
|
|
|
// Analytics operations
|
|
getAnalytics(): Promise<{
|
|
totalArticles: number;
|
|
activePredictions: number;
|
|
liveAuctions: number;
|
|
totalRevenue: number;
|
|
}>;
|
|
|
|
// Search operations
|
|
search(query: string): Promise<{
|
|
outlets: MediaOutlet[];
|
|
articles: Article[];
|
|
}>;
|
|
}
|
|
|
|
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.email,
|
|
set: {
|
|
firstName: userData.firstName,
|
|
lastName: userData.lastName,
|
|
profileImageUrl: userData.profileImageUrl,
|
|
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 getAuctionByMediaOutlet(mediaOutletId: string): Promise<Auction | undefined> {
|
|
const [auction] = await db.select().from(auctions).where(
|
|
and(eq(auctions.mediaOutletId, mediaOutletId), eq(auctions.isActive, true))
|
|
);
|
|
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> {
|
|
// First, get the auction to validate
|
|
const auction = await this.getAuctionById(bid.auctionId);
|
|
if (!auction) {
|
|
throw new Error("Auction not found");
|
|
}
|
|
|
|
// Validate auction status
|
|
if (!auction.isActive) {
|
|
throw new Error("Auction is not active");
|
|
}
|
|
|
|
// Validate auction end date
|
|
if (new Date() > auction.endDate) {
|
|
throw new Error("Auction has ended");
|
|
}
|
|
|
|
// Validate bid amount
|
|
const currentBidAmount = parseFloat(auction.currentBid || "0");
|
|
const bidAmount = parseFloat(bid.amount.toString());
|
|
|
|
if (bidAmount <= currentBidAmount) {
|
|
throw new Error(`Bid amount must be higher than current bid of ${currentBidAmount}`);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
async getBidsByAuctionId(auctionId: string): Promise<Bid[]> {
|
|
return await db
|
|
.select()
|
|
.from(bids)
|
|
.where(eq(bids.auctionId, auctionId))
|
|
.orderBy(desc(bids.createdAt));
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Prediction bet operations
|
|
async createPredictionBet(bet: InsertPredictionBet): Promise<PredictionBet> {
|
|
// Get the current market to determine the price
|
|
const market = await this.getPredictionMarketById(bet.marketId);
|
|
if (!market) {
|
|
throw new Error("Prediction market not found");
|
|
}
|
|
|
|
// Use current market price for the bet
|
|
const price = bet.side === "yes" ? market.yesPrice : market.noPrice;
|
|
|
|
const [newBet] = await db.insert(predictionBets).values({
|
|
...bet,
|
|
price: price.toString()
|
|
}).returning();
|
|
|
|
// Update market volume and bet count
|
|
await db
|
|
.update(predictionMarkets)
|
|
.set({
|
|
totalVolume: sql`total_volume + ${bet.amount}`,
|
|
totalBets: sql`total_bets + 1`,
|
|
updatedAt: new Date()
|
|
})
|
|
.where(eq(predictionMarkets.id, bet.marketId));
|
|
|
|
return newBet;
|
|
}
|
|
|
|
async getPredictionBetsByMarket(marketId: string): Promise<PredictionBet[]> {
|
|
return await db
|
|
.select()
|
|
.from(predictionBets)
|
|
.where(eq(predictionBets.marketId, marketId))
|
|
.orderBy(desc(predictionBets.createdAt));
|
|
}
|
|
|
|
async getPredictionBetsByUser(userId: string): Promise<PredictionBet[]> {
|
|
return await db
|
|
.select()
|
|
.from(predictionBets)
|
|
.where(eq(predictionBets.userId, userId))
|
|
.orderBy(desc(predictionBets.createdAt));
|
|
}
|
|
|
|
// 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
|
|
};
|
|
}
|
|
|
|
// Search operations
|
|
async search(query: string): Promise<{
|
|
outlets: MediaOutlet[];
|
|
articles: Article[];
|
|
}> {
|
|
const searchTerm = query.toLowerCase();
|
|
|
|
// Search media outlets
|
|
const foundOutlets = await db
|
|
.select()
|
|
.from(mediaOutlets)
|
|
.where(eq(mediaOutlets.isActive, true))
|
|
.limit(50);
|
|
|
|
const filteredOutlets = foundOutlets.filter(outlet =>
|
|
outlet.name.toLowerCase().includes(searchTerm) ||
|
|
(outlet.description && outlet.description.toLowerCase().includes(searchTerm))
|
|
).slice(0, 10);
|
|
|
|
// Search articles
|
|
const foundArticles = await db
|
|
.select()
|
|
.from(articles)
|
|
.orderBy(desc(articles.publishedAt))
|
|
.limit(100);
|
|
|
|
const filteredArticles = foundArticles.filter(article =>
|
|
article.title.toLowerCase().includes(searchTerm) ||
|
|
(article.content && article.content.toLowerCase().includes(searchTerm)) ||
|
|
(article.excerpt && article.excerpt.toLowerCase().includes(searchTerm))
|
|
).slice(0, 20);
|
|
|
|
return {
|
|
outlets: filteredOutlets,
|
|
articles: filteredArticles
|
|
};
|
|
}
|
|
}
|
|
|
|
export const storage = new DatabaseStorage();
|