Feat: Landingpages centralized, new app news integrated

This commit is contained in:
Till-JS 2025-11-25 18:20:17 +01:00
parent 36b85fc8a0
commit 865d74ff37
91 changed files with 8242 additions and 610 deletions

View file

@ -0,0 +1,15 @@
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';
import { resolve } from 'path';
// Load .env from monorepo root
config({ path: resolve(__dirname, '../../.env') });
export default defineConfig({
schema: './src/schema/index.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL || 'postgresql://news:news_dev_password@localhost:5434/news_hub',
},
});

View file

@ -0,0 +1,43 @@
{
"name": "@manacore/news-database",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./schema": {
"types": "./dist/schema/index.d.ts",
"import": "./dist/schema/index.js",
"require": "./dist/schema/index.js",
"default": "./dist/schema/index.js"
}
},
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"prepare": "pnpm build",
"db:generate": "dotenv -- drizzle-kit generate",
"db:migrate": "dotenv -- drizzle-kit migrate",
"db:push": "dotenv -- drizzle-kit push --force",
"db:studio": "dotenv -- drizzle-kit studio",
"type-check": "tsc --noEmit"
},
"dependencies": {
"drizzle-orm": "^0.36.0",
"postgres": "^3.4.5"
},
"devDependencies": {
"dotenv-cli": "^7.4.0",
"drizzle-kit": "^0.28.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0",
"@types/node": "^22.0.0"
}
}

View file

@ -0,0 +1,19 @@
import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
// Re-export schema and types
export * from './schema';
export { sql, eq, and, or, desc, asc, isNull, isNotNull, inArray } from 'drizzle-orm';
// Export schema for use in drizzle initialization
export { schema };
// Type for the database instance with schema
export type Database = PostgresJsDatabase<typeof schema>;
// Helper to create a new database connection
export function createDb(url: string): Database {
const client = postgres(url);
return drizzle(client, { schema });
}

View file

@ -0,0 +1,64 @@
import { pgTable, uuid, text, timestamp, boolean, integer, real, pgEnum, index } from 'drizzle-orm/pg-core';
import { users } from './users';
import { categories } from './categories';
export const articleTypeEnum = pgEnum('article_type', ['feed', 'summary', 'in_depth', 'saved']);
export const articleSourceEnum = pgEnum('article_source', ['ai', 'user_saved']);
export const summaryPeriodEnum = pgEnum('summary_period', ['morning', 'noon', 'evening', 'night']);
export const articles = pgTable('articles', {
id: uuid('id').primaryKey().defaultRandom(),
// Core fields
type: articleTypeEnum('type').notNull(),
sourceOrigin: articleSourceEnum('source_origin').default('ai').notNull(),
title: text('title').notNull(),
content: text('content').notNull(),
summary: text('summary'),
// For user-saved articles
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
originalUrl: text('original_url'),
parsedContent: text('parsed_content'),
isArchived: boolean('is_archived').default(false),
// Metadata
categoryId: uuid('category_id').references(() => categories.id),
sourceUrl: text('source_url'),
sourceName: text('source_name'),
sourceDomain: text('source_domain'),
author: text('author'),
imageUrl: text('image_url'),
// AI-generated metadata
aiTags: text('ai_tags').array(),
sentimentScore: real('sentiment_score'),
// Reading metrics
readingTimeMinutes: integer('reading_time_minutes'),
wordCount: integer('word_count'),
// Summary-specific fields
summaryDate: timestamp('summary_date'),
summaryPeriod: summaryPeriodEnum('summary_period'),
includedArticleIds: uuid('included_article_ids').array(),
// In-depth specific fields
keyInsights: text('key_insights'), // JSON string
dataVisualizations: text('data_visualizations'), // JSON string
relatedArticleIds: uuid('related_article_ids').array(),
// Timestamps
publishedAt: timestamp('published_at').defaultNow().notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => [
index('articles_type_idx').on(table.type),
index('articles_user_idx').on(table.userId),
index('articles_source_origin_idx').on(table.sourceOrigin),
index('articles_published_at_idx').on(table.publishedAt),
index('articles_category_idx').on(table.categoryId),
]);
export type Article = typeof articles.$inferSelect;
export type NewArticle = typeof articles.$inferInsert;

View file

@ -0,0 +1,47 @@
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core';
import { users } from './users';
// Better Auth Sessions
export const sessions = pgTable('sessions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
token: text('token').notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Better Auth Accounts (for OAuth providers)
export const accounts = pgTable('accounts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
providerId: text('provider_id').notNull(), // 'credential', 'google', 'apple', etc.
accountId: text('account_id').notNull(), // Provider's user ID or email for credential
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'), // Hashed, only for credential provider
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Better Auth Verification Tokens
export const verifications = pgTable('verifications', {
id: uuid('id').primaryKey().defaultRandom(),
identifier: text('identifier').notNull(), // email or other identifier
value: text('value').notNull(), // the token
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export type Session = typeof sessions.$inferSelect;
export type NewSession = typeof sessions.$inferInsert;
export type Account = typeof accounts.$inferSelect;
export type NewAccount = typeof accounts.$inferInsert;
export type Verification = typeof verifications.$inferSelect;
export type NewVerification = typeof verifications.$inferInsert;

View file

@ -0,0 +1,16 @@
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core';
export const categories = pgTable('categories', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull().unique(),
displayName: text('display_name').notNull(),
description: text('description'),
icon: text('icon'),
color: text('color'),
priority: integer('priority').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export type Category = typeof categories.$inferSelect;
export type NewCategory = typeof categories.$inferInsert;

View file

@ -0,0 +1,5 @@
export * from './users';
export * from './categories';
export * from './articles';
export * from './interactions';
export * from './auth';

View file

@ -0,0 +1,31 @@
import { pgTable, uuid, timestamp, boolean, real, integer, index, unique } from 'drizzle-orm/pg-core';
import { users } from './users';
import { articles } from './articles';
export const userArticleInteractions = pgTable('user_article_interactions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
articleId: uuid('article_id').references(() => articles.id, { onDelete: 'cascade' }).notNull(),
// Interaction states
isRead: boolean('is_read').default(false).notNull(),
isSaved: boolean('is_saved').default(false).notNull(),
readProgress: real('read_progress').default(0), // 0.0 to 1.0
rating: integer('rating'), // 1-5
shareCount: integer('share_count').default(0).notNull(),
// Timestamps
openedAt: timestamp('opened_at'),
readAt: timestamp('read_at'),
savedAt: timestamp('saved_at'),
ratedAt: timestamp('rated_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => [
unique('user_article_unique').on(table.userId, table.articleId),
index('interactions_user_idx').on(table.userId),
index('interactions_article_idx').on(table.articleId),
]);
export type UserArticleInteraction = typeof userArticleInteractions.$inferSelect;
export type NewUserArticleInteraction = typeof userArticleInteractions.$inferInsert;

View file

@ -0,0 +1,29 @@
import { pgTable, uuid, text, timestamp, boolean, pgEnum } from 'drizzle-orm/pg-core';
export const userTierEnum = pgEnum('user_tier', ['free', 'premium', 'enterprise']);
export const readingSpeedEnum = pgEnum('reading_speed', ['slow', 'normal', 'fast']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name'),
avatarUrl: text('avatar_url'),
emailVerified: boolean('email_verified').default(false).notNull(),
// Preferences
tier: userTierEnum('tier').default('free').notNull(),
readingSpeed: readingSpeedEnum('reading_speed').default('normal').notNull(),
preferredCategories: text('preferred_categories').array(),
blockedSources: text('blocked_sources').array(),
// Settings
onboardingCompleted: boolean('onboarding_completed').default(false).notNull(),
notificationSettings: text('notification_settings'), // JSON string
// Timestamps
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}