project init

This commit is contained in:
2026-02-06 02:17:59 +03:00
commit b9d9b9ed17
129 changed files with 22835 additions and 0 deletions

View File

@ -0,0 +1,16 @@
{
"name": "@coursecraft/shared",
"version": "0.1.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist .turbo",
"dev": "tsc --watch"
},
"devDependencies": {
"@types/node": "^20.11.0",
"typescript": "^5.4.0"
}
}

View File

@ -0,0 +1,204 @@
// Default AI model for all tiers (can override in user settings on PRO)
const DEFAULT_AI_MODEL = 'openai/gpt-4o-mini';
// Subscription tier limits
export const SUBSCRIPTION_LIMITS = {
FREE: {
coursesPerMonth: 2,
defaultAiModel: DEFAULT_AI_MODEL,
},
PREMIUM: {
coursesPerMonth: 5,
defaultAiModel: DEFAULT_AI_MODEL,
},
PRO: {
coursesPerMonth: 15,
defaultAiModel: DEFAULT_AI_MODEL,
},
} as const;
// Prices in RUB for Russian locale (approximate)
const PRICE_RUB = {
FREE: 0,
PREMIUM: 999,
PRO: 2999,
} as const;
// Subscription plans for display
export const SUBSCRIPTION_PLANS = [
{
tier: 'FREE' as const,
name: 'Free',
nameRu: 'Бесплатный',
description: 'Perfect for trying out CourseCraft',
descriptionRu: 'Идеально для знакомства с CourseCraft',
price: 0,
priceRu: PRICE_RUB.FREE,
currency: 'USD',
features: [
'2 courses per month',
'Basic AI model',
'Standard support',
],
featuresRu: [
'2 курса в месяц',
'Базовая нейросеть',
'Стандартная поддержка',
],
stripePriceId: null,
},
{
tier: 'PREMIUM' as const,
name: 'Premium',
nameRu: 'Премиум',
description: 'For creators who need more',
descriptionRu: 'Для тех, кому нужно больше',
price: 9.99,
priceRu: PRICE_RUB.PREMIUM,
currency: 'USD',
features: [
'5 courses per month',
'Enhanced AI model',
'Priority support',
'Advanced editing tools',
],
featuresRu: [
'5 курсов в месяц',
'Улучшенная нейросеть',
'Приоритетная поддержка',
'Расширенные инструменты редактирования',
],
stripePriceId: process.env.STRIPE_PRICE_PREMIUM || null,
},
{
tier: 'PRO' as const,
name: 'Pro',
nameRu: 'Профессиональный',
description: 'For power users and teams',
descriptionRu: 'Для профессионалов и команд',
price: 29.99,
priceRu: PRICE_RUB.PRO,
currency: 'USD',
features: [
'15 courses per month',
'Best AI model available',
'Priority support 24/7',
'Advanced editing tools',
'Custom AI model selection',
'Export to PDF (coming soon)',
],
featuresRu: [
'15 курсов в месяц',
'Лучшая доступная нейросеть',
'Приоритетная поддержка 24/7',
'Расширенные инструменты редактирования',
'Выбор модели нейросети',
'Экспорт в PDF (скоро)',
],
stripePriceId: process.env.STRIPE_PRICE_PRO || null,
},
] as const;
export type SubscriptionPlan = (typeof SUBSCRIPTION_PLANS)[number];
/** Format plan price for display. Uses RUB when language is Russian. */
export function formatPlanPrice(
plan: { price: number; priceRu: number; currency: string },
language: string
): { amount: number; currency: string; formatted: string } {
const isRu = language === 'ru' || language.startsWith('ru');
if (isRu) {
const amount = plan.priceRu;
return {
amount,
currency: 'RUB',
formatted: amount === 0 ? 'Бесплатно' : `${amount.toLocaleString('ru-RU')}`,
};
}
return {
amount: plan.price,
currency: plan.currency,
formatted: plan.price === 0 ? 'Free' : `$${plan.price}`,
};
}
// Generation progress steps
export const GENERATION_STEPS = {
PENDING: { progress: 0, label: 'Waiting to start', labelRu: 'Ожидание запуска' },
ANALYZING: { progress: 10, label: 'Analyzing your request', labelRu: 'Анализ запроса' },
ASKING_QUESTIONS: { progress: 15, label: 'Preparing questions', labelRu: 'Подготовка вопросов' },
WAITING_FOR_ANSWERS: { progress: 20, label: 'Waiting for your answers', labelRu: 'Ожидание ваших ответов' },
RESEARCHING: { progress: 30, label: 'Researching the topic', labelRu: 'Исследование темы' },
GENERATING_OUTLINE: { progress: 50, label: 'Creating course structure', labelRu: 'Создание структуры курса' },
GENERATING_CONTENT: { progress: 70, label: 'Writing course content', labelRu: 'Написание содержания' },
COMPLETED: { progress: 100, label: 'Course completed!', labelRu: 'Курс готов!' },
FAILED: { progress: 0, label: 'Generation failed', labelRu: 'Ошибка генерации' },
CANCELLED: { progress: 0, label: 'Generation cancelled', labelRu: 'Генерация отменена' },
} as const;
// API Routes
export const API_ROUTES = {
AUTH: {
ME: '/auth/me',
CALLBACK: '/auth/callback',
LOGOUT: '/auth/logout',
},
USERS: {
PROFILE: '/users/profile',
SETTINGS: '/users/settings',
},
COURSES: {
LIST: '/courses',
CREATE: '/courses',
GET: (id: string) => `/courses/${id}`,
UPDATE: (id: string) => `/courses/${id}`,
DELETE: (id: string) => `/courses/${id}`,
},
CHAPTERS: {
LIST: (courseId: string) => `/courses/${courseId}/chapters`,
CREATE: (courseId: string) => `/courses/${courseId}/chapters`,
UPDATE: (courseId: string, chapterId: string) => `/courses/${courseId}/chapters/${chapterId}`,
DELETE: (courseId: string, chapterId: string) => `/courses/${courseId}/chapters/${chapterId}`,
REORDER: (courseId: string) => `/courses/${courseId}/chapters/reorder`,
},
LESSONS: {
GET: (courseId: string, lessonId: string) => `/courses/${courseId}/lessons/${lessonId}`,
UPDATE: (courseId: string, lessonId: string) => `/courses/${courseId}/lessons/${lessonId}`,
},
GENERATION: {
START: '/generation/start',
STATUS: (id: string) => `/generation/${id}/status`,
ANSWER: (id: string) => `/generation/${id}/answer`,
CANCEL: (id: string) => `/generation/${id}/cancel`,
},
SUBSCRIPTIONS: {
PLANS: '/subscriptions/plans',
CURRENT: '/subscriptions/current',
CHECKOUT: '/subscriptions/checkout',
PORTAL: '/subscriptions/portal',
},
SEARCH: {
COURSES: '/search/courses',
},
} as const;
// Validation
export const VALIDATION = {
COURSE: {
TITLE_MIN: 3,
TITLE_MAX: 200,
DESCRIPTION_MAX: 5000,
},
CHAPTER: {
TITLE_MIN: 2,
TITLE_MAX: 200,
},
LESSON: {
TITLE_MIN: 2,
TITLE_MAX: 200,
},
PROMPT: {
MIN: 10,
MAX: 2000,
},
} as const;

View File

@ -0,0 +1,4 @@
// Export all shared types and utilities
export * from './types';
export * from './constants';
export * from './utils';

View File

@ -0,0 +1,197 @@
// API Response types
export interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: unknown;
};
meta?: {
page?: number;
limit?: number;
total?: number;
totalPages?: number;
};
}
// User types
export interface UserProfile {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
subscriptionTier: SubscriptionTierType;
createdAt: string;
}
export type SubscriptionTierType = 'FREE' | 'PREMIUM' | 'PRO';
export interface UserSettings {
customAiModel: string | null;
emailNotifications: boolean;
marketingEmails: boolean;
theme: 'light' | 'dark' | 'system';
language: string;
}
// Course types
export interface CourseListItem {
id: string;
title: string;
description: string | null;
slug: string;
coverImage: string | null;
status: CourseStatusType;
chaptersCount: number;
lessonsCount: number;
createdAt: string;
updatedAt: string;
}
export type CourseStatusType = 'DRAFT' | 'GENERATING' | 'PUBLISHED' | 'ARCHIVED';
export interface CourseDetail extends CourseListItem {
chapters: ChapterWithLessons[];
author: {
id: string;
name: string | null;
avatarUrl: string | null;
};
}
export interface ChapterWithLessons {
id: string;
title: string;
description: string | null;
order: number;
lessons: LessonSummary[];
}
export interface LessonSummary {
id: string;
title: string;
order: number;
durationMinutes: number | null;
}
export interface LessonContent {
id: string;
title: string;
content: TipTapContent | null;
durationMinutes: number | null;
videoUrl: string | null;
}
// TipTap content type
export interface TipTapContent {
type: 'doc';
content: TipTapNode[];
}
export interface TipTapNode {
type: string;
attrs?: Record<string, unknown>;
content?: TipTapNode[];
marks?: TipTapMark[];
text?: string;
}
export interface TipTapMark {
type: string;
attrs?: Record<string, unknown>;
}
// Generation types
export type GenerationStatusType =
| 'PENDING'
| 'ANALYZING'
| 'ASKING_QUESTIONS'
| 'WAITING_FOR_ANSWERS'
| 'RESEARCHING'
| 'GENERATING_OUTLINE'
| 'GENERATING_CONTENT'
| 'COMPLETED'
| 'FAILED'
| 'CANCELLED';
export interface GenerationProgress {
id: string;
status: GenerationStatusType;
progress: number;
currentStep: string | null;
questions?: ClarifyingQuestion[];
generatedOutline?: CourseOutline;
errorMessage?: string;
}
export interface ClarifyingQuestion {
id: string;
question: string;
type: 'single_choice' | 'multiple_choice' | 'text';
options?: string[];
required: boolean;
}
export interface QuestionAnswer {
questionId: string;
answer: string | string[];
}
export interface CourseOutline {
title: string;
description: string;
chapters: {
title: string;
description: string;
lessons: {
title: string;
estimatedMinutes: number;
}[];
}[];
estimatedTotalHours: number;
difficulty: 'beginner' | 'intermediate' | 'advanced';
tags: string[];
}
// Subscription types
export interface SubscriptionPlan {
tier: SubscriptionTierType;
name: string;
description: string;
price: number;
currency: string;
features: string[];
limits: {
coursesPerMonth: number;
defaultAiModel: string;
};
stripePriceId: string | null;
}
export interface CurrentSubscription {
tier: SubscriptionTierType;
status: string;
currentPeriodEnd: string | null;
cancelAtPeriodEnd: boolean;
coursesCreatedThisMonth: number;
coursesLimit: number;
}
// Marketplace types (future)
export interface MarketplaceCourse extends CourseListItem {
price: number | null;
currency: string;
author: {
id: string;
name: string | null;
avatarUrl: string | null;
};
averageRating: number | null;
enrollmentCount: number;
category: {
id: string;
name: string;
slug: string;
} | null;
}

View File

@ -0,0 +1,128 @@
/**
* Generate a URL-friendly slug from a string
*/
export function generateSlug(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
/**
* Generate a unique slug by appending a random suffix
*/
export function generateUniqueSlug(text: string): string {
const baseSlug = generateSlug(text);
const suffix = Math.random().toString(36).substring(2, 8);
return `${baseSlug}-${suffix}`;
}
/**
* Format a price for display
*/
export function formatPrice(amount: number, currency: string = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount);
}
/**
* Format a date for display
*/
export function formatDate(date: string | Date, locale: string = 'ru-RU'): string {
const d = typeof date === 'string' ? new Date(date) : date;
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(d);
}
/**
* Format a relative time (e.g., "2 days ago")
*/
export function formatRelativeTime(date: string | Date, locale: string = 'ru-RU'): string {
const d = typeof date === 'string' ? new Date(date) : date;
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - d.getTime()) / 1000);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
if (diffInSeconds < 60) {
return rtf.format(-diffInSeconds, 'second');
} else if (diffInSeconds < 3600) {
return rtf.format(-Math.floor(diffInSeconds / 60), 'minute');
} else if (diffInSeconds < 86400) {
return rtf.format(-Math.floor(diffInSeconds / 3600), 'hour');
} else if (diffInSeconds < 2592000) {
return rtf.format(-Math.floor(diffInSeconds / 86400), 'day');
} else if (diffInSeconds < 31536000) {
return rtf.format(-Math.floor(diffInSeconds / 2592000), 'month');
} else {
return rtf.format(-Math.floor(diffInSeconds / 31536000), 'year');
}
}
/**
* Truncate text to a maximum length
*/
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - 3) + '...';
}
/**
* Format duration in minutes to human readable
*/
export function formatDuration(minutes: number, locale: string = 'ru'): string {
if (minutes < 60) {
return locale === 'ru' ? `${minutes} мин` : `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (locale === 'ru') {
return remainingMinutes > 0
? `${hours} ч ${remainingMinutes} мин`
: `${hours} ч`;
}
return remainingMinutes > 0
? `${hours}h ${remainingMinutes}m`
: `${hours}h`;
}
/**
* Calculate estimated reading time from content
*/
export function calculateReadingTime(text: string, wordsPerMinute: number = 200): number {
const words = text.trim().split(/\s+/).length;
return Math.ceil(words / wordsPerMinute);
}
/**
* Validate email format
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Sleep utility for async operations
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Chunk an array into smaller arrays
*/
export function chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}