feat: SAPIENS Mobile App - Initial commit
React Native mobile application for SAPIENS news platform. Consolidated all previous history into single commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
232
shared/schema.ts
Normal file
232
shared/schema.ts
Normal file
@ -0,0 +1,232 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { pgTable, text, varchar, timestamp, integer, pgEnum, foreignKey, uniqueIndex, index } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
// Enum for reaction types
|
||||
export const reactionTypeEnum = pgEnum('reaction_type', ['like', 'dislike']);
|
||||
|
||||
// User schema
|
||||
export const users = pgTable("users", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
username: text("username").notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
preferredLanguage: text("preferred_language").default("en"),
|
||||
avatar: text("avatar"),
|
||||
});
|
||||
|
||||
// Media outlet schema
|
||||
export const mediaOutlets = pgTable("media_outlets", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
name: text("name").notNull(),
|
||||
description: text("description").notNull(),
|
||||
category: text("category").notNull(), // "people", "topics", "companies"
|
||||
focusSubject: text("focus_subject").notNull(), // "jacob", "ala", "ai", "crypto", etc.
|
||||
avatar: text("avatar"),
|
||||
profileImage: text("profile_image"),
|
||||
bio: text("bio").notNull(),
|
||||
fullBio: text("full_bio").array(),
|
||||
wikiProfile: text("wiki_profile"), // Detailed Wikipedia-style profile in HTML/Markdown format
|
||||
});
|
||||
|
||||
// Article schema - extended for web scraping
|
||||
export const articles = pgTable("articles", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
outletId: varchar("outlet_id").notNull().references(() => mediaOutlets.id),
|
||||
title: text("title").notNull(),
|
||||
summary: text("summary").notNull(),
|
||||
body: text("body").notNull(),
|
||||
thumbnail: text("thumbnail").notNull(),
|
||||
publishedAt: timestamp("published_at").notNull(),
|
||||
tags: text("tags").array(),
|
||||
viewCount: integer("view_count").notNull().default(0),
|
||||
// Scraped article fields
|
||||
sourceUrl: text("source_url"), // Original URL that was scraped
|
||||
author: text("author"), // Article author from scraped content
|
||||
originalImageUrl: text("original_image_url"), // Original image URL before processing
|
||||
scrapedAt: timestamp("scraped_at").default(sql`NOW()`), // When this article was scraped
|
||||
isScraped: integer("is_scraped").notNull().default(0), // 1 if scraped, 0 if manually created
|
||||
});
|
||||
|
||||
// Comments schema for YouTube-style commenting system
|
||||
export const comments = pgTable("comments", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
articleId: varchar("article_id").notNull().references(() => articles.id),
|
||||
content: text("content").notNull(),
|
||||
nickname: text("nickname").notNull(),
|
||||
avatar: text("avatar"), // Optional avatar URL or emoji
|
||||
parentId: varchar("parent_id"), // Parent comment ID (nullable for top-level comments)
|
||||
likesCount: integer("likes_count").notNull().default(0),
|
||||
dislikesCount: integer("dislikes_count").notNull().default(0),
|
||||
repliesCount: integer("replies_count").notNull().default(0),
|
||||
createdAt: timestamp("created_at").notNull().default(sql`NOW()`),
|
||||
updatedAt: timestamp("updated_at").notNull().default(sql`NOW()`),
|
||||
}, (table) => ({
|
||||
// Performance indexes
|
||||
articleParentCreatedIdx: index("comments_article_parent_created_idx").on(
|
||||
table.articleId,
|
||||
table.parentId,
|
||||
table.createdAt
|
||||
),
|
||||
}));
|
||||
|
||||
// Comment reactions (likes/dislikes) with deduplication constraint
|
||||
export const commentReactions = pgTable("comment_reactions", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
commentId: varchar("comment_id").notNull().references(() => comments.id, { onDelete: 'cascade' }),
|
||||
reactionType: text("reaction_type").notNull(), // Keep as text to avoid migration issues
|
||||
userIdentifier: text("user_identifier").notNull(), // Session ID, IP hash, or similar
|
||||
createdAt: timestamp("created_at").notNull().default(sql`NOW()`),
|
||||
}, (table) => ({
|
||||
// Unique constraint to prevent duplicate reactions from same user
|
||||
uniqueUserReaction: uniqueIndex("unique_user_reaction_per_comment_idx").on(
|
||||
table.commentId,
|
||||
table.userIdentifier
|
||||
),
|
||||
}));
|
||||
|
||||
// Bookmarks schema for saving articles
|
||||
export const bookmarks = pgTable("bookmarks", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
articleId: varchar("article_id").notNull().references(() => articles.id, { onDelete: 'cascade' }),
|
||||
userIdentifier: text("user_identifier").notNull(), // Session ID, IP hash, or similar
|
||||
createdAt: timestamp("created_at").notNull().default(sql`NOW()`),
|
||||
}, (table) => ({
|
||||
// Unique constraint to prevent duplicate bookmarks from same user
|
||||
uniqueUserBookmark: uniqueIndex("unique_user_bookmark_per_article_idx").on(
|
||||
table.articleId,
|
||||
table.userIdentifier
|
||||
),
|
||||
}));
|
||||
|
||||
// Prediction Markets schema for article-related betting events
|
||||
export const predictionMarkets = pgTable("prediction_markets", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
articleId: varchar("article_id").notNull().references(() => articles.id),
|
||||
question: text("question").notNull(),
|
||||
description: text("description"),
|
||||
marketType: text("market_type").notNull().default("binary"), // "binary" (yes/no) or "multiple_choice"
|
||||
// For binary markets
|
||||
yesPrice: integer("yes_price"), // Price in cents (0-100)
|
||||
noPrice: integer("no_price"), // Price in cents (0-100)
|
||||
// For multiple choice markets - JSON array of options
|
||||
// [{name: "Option A", price: 60, image: "/url"}, ...]
|
||||
options: text("options"), // JSON string
|
||||
totalVolume: integer("total_volume").notNull().default(0), // Total trading volume in dollars
|
||||
endDate: timestamp("end_date").notNull(),
|
||||
category: text("category"),
|
||||
isLive: integer("is_live").notNull().default(0),
|
||||
isResolved: integer("is_resolved").notNull().default(0),
|
||||
createdAt: timestamp("created_at").notNull().default(sql`NOW()`),
|
||||
}, (table) => ({
|
||||
articleIdx: index("prediction_markets_article_idx").on(table.articleId),
|
||||
}));
|
||||
|
||||
// Insert schemas
|
||||
export const insertUserSchema = createInsertSchema(users).pick({
|
||||
username: true,
|
||||
password: true,
|
||||
preferredLanguage: true,
|
||||
avatar: true,
|
||||
});
|
||||
|
||||
export const insertMediaOutletSchema = createInsertSchema(mediaOutlets).pick({
|
||||
name: true,
|
||||
description: true,
|
||||
category: true,
|
||||
focusSubject: true,
|
||||
avatar: true,
|
||||
profileImage: true,
|
||||
bio: true,
|
||||
fullBio: true,
|
||||
wikiProfile: true,
|
||||
});
|
||||
|
||||
export const insertArticleSchema = createInsertSchema(articles).pick({
|
||||
outletId: true,
|
||||
title: true,
|
||||
summary: true,
|
||||
body: true,
|
||||
thumbnail: true,
|
||||
publishedAt: true,
|
||||
tags: true,
|
||||
viewCount: true,
|
||||
sourceUrl: true,
|
||||
author: true,
|
||||
originalImageUrl: true,
|
||||
scrapedAt: true,
|
||||
isScraped: true,
|
||||
}).extend({
|
||||
publishedAt: z.preprocess((val) => {
|
||||
if (typeof val === 'string') return new Date(val);
|
||||
if (val instanceof Date) return val;
|
||||
return new Date();
|
||||
}, z.date()),
|
||||
});
|
||||
|
||||
export const insertCommentSchema = createInsertSchema(comments).pick({
|
||||
articleId: true,
|
||||
content: true,
|
||||
nickname: true,
|
||||
avatar: true,
|
||||
parentId: true,
|
||||
});
|
||||
|
||||
export const insertCommentReactionSchema = createInsertSchema(commentReactions).pick({
|
||||
commentId: true,
|
||||
reactionType: true,
|
||||
userIdentifier: true,
|
||||
}).extend({
|
||||
reactionType: z.enum(["like", "dislike"]),
|
||||
});
|
||||
|
||||
export const insertBookmarkSchema = createInsertSchema(bookmarks).pick({
|
||||
articleId: true,
|
||||
userIdentifier: true,
|
||||
});
|
||||
|
||||
export const insertPredictionMarketSchema = createInsertSchema(predictionMarkets).pick({
|
||||
articleId: true,
|
||||
question: true,
|
||||
description: true,
|
||||
marketType: true,
|
||||
yesPrice: true,
|
||||
noPrice: true,
|
||||
options: true,
|
||||
totalVolume: true,
|
||||
endDate: true,
|
||||
category: true,
|
||||
isLive: true,
|
||||
isResolved: true,
|
||||
});
|
||||
|
||||
// Types
|
||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type InsertMediaOutlet = z.infer<typeof insertMediaOutletSchema>;
|
||||
export type MediaOutlet = typeof mediaOutlets.$inferSelect;
|
||||
export type InsertArticle = z.infer<typeof insertArticleSchema>;
|
||||
export type Article = typeof articles.$inferSelect;
|
||||
export type InsertComment = z.infer<typeof insertCommentSchema>;
|
||||
export type Comment = typeof comments.$inferSelect;
|
||||
export type InsertCommentReaction = z.infer<typeof insertCommentReactionSchema>;
|
||||
export type CommentReaction = typeof commentReactions.$inferSelect;
|
||||
export type InsertBookmark = z.infer<typeof insertBookmarkSchema>;
|
||||
export type Bookmark = typeof bookmarks.$inferSelect;
|
||||
export type InsertPredictionMarket = z.infer<typeof insertPredictionMarketSchema>;
|
||||
export type PredictionMarket = typeof predictionMarkets.$inferSelect;
|
||||
|
||||
// Enums for categories
|
||||
export const CATEGORIES = ["people", "topics", "companies"] as const;
|
||||
export const FOCUS_SUBJECTS = ["jacob", "ala", "ai", "crypto", "polychain", "dcg"] as const;
|
||||
export const LANGUAGES = [
|
||||
{ code: "en", name: "English" },
|
||||
{ code: "ko", name: "한국어" },
|
||||
{ code: "ja", name: "日本語" },
|
||||
{ code: "zh_cn", name: "简体中文" },
|
||||
{ code: "zh_tw", name: "繁體中文" },
|
||||
{ code: "de", name: "Deutsch" },
|
||||
{ code: "fr", name: "Français" },
|
||||
{ code: "es", name: "Español" },
|
||||
{ code: "it", name: "Italiano" },
|
||||
] as const;
|
||||
Reference in New Issue
Block a user