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:
jungwoo choi
2025-10-23 14:30:25 +09:00
commit 919afe56f2
1516 changed files with 64072 additions and 0 deletions

232
shared/schema.ts Normal file
View 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;