From 7f5b79f94bf2f903857fc48f4e138b0ff353705b Mon Sep 17 00:00:00 2001 From: Ho Phuoc Hoan Date: Fri, 2 Jan 2026 20:23:42 +0700 Subject: [PATCH 1/9] feat: enhance seed data with rich content, update docker setup, and polish docs --- Context/Final project Guidelines.md | 58 +++ Dockerfile | 5 +- README.md | 166 +++++---- apps/api/package.json | 9 +- apps/api/src/scripts/check-users.ts | 29 -- apps/api/src/scripts/create-admin.ts | 67 ---- apps/api/src/scripts/reset-db.ts | 27 ++ apps/api/src/scripts/seed-from-sql.ts | 38 ++ apps/api/src/scripts/seed-moderation.ts | 78 ----- apps/api/src/scripts/seed-test-data.ts | 97 ----- apps/web/package.json | 4 +- database/README.md | 79 +++++ database/schema.sql | 277 +++++++++++++++ database/seed.sql | 448 ++++++++++++++++++++++++ pnpm-lock.yaml | 32 +- 15 files changed, 1044 insertions(+), 370 deletions(-) create mode 100644 Context/Final project Guidelines.md delete mode 100644 apps/api/src/scripts/check-users.ts delete mode 100644 apps/api/src/scripts/create-admin.ts create mode 100644 apps/api/src/scripts/reset-db.ts create mode 100644 apps/api/src/scripts/seed-from-sql.ts delete mode 100644 apps/api/src/scripts/seed-moderation.ts delete mode 100644 apps/api/src/scripts/seed-test-data.ts create mode 100644 database/README.md create mode 100644 database/schema.sql create mode 100644 database/seed.sql diff --git a/Context/Final project Guidelines.md b/Context/Final project Guidelines.md new file mode 100644 index 0000000..6adfca0 --- /dev/null +++ b/Context/Final project Guidelines.md @@ -0,0 +1,58 @@ +# Submission Guidelines + +Your submission is required to encompass the following components: + +* Source Folder: + * Organize each project into distinct folders (e.g., front end, back end, API). + * Include comprehensive instructions detailing the deployment process for your project. +* Database Folder: + * House the database data along with the migration script for the data structure. + * Provide clear instructions on how to import the migration script. +* Reports + * Teamwork Report.pdf: + * Include a comprehensive report detailing how your team collaborated. + * Append screenshots of Git commits + * Jira Roadmap and Jira task reports as evidence (optional). + * Final Project Report.pdf: + * System Description: Provide an overview of your project. + * Team Information: Introduce your team members. + * Project Plan Tracker: Outline your project plan and progress. + * Functionalities Analysis: Describe the functionalities of your project. + * Database Design: Detail the structure and design of your database. + * UI/UX Design: Include Layout Design and Implementation details. + * Guideline: Offer instructions for web usage and deployment (both local and internet). + * Project Self-assessment Report.pdf: + * Grade each criterion based on the established scale. + * Provide evidence and proof for each self-assigned score. + * Accumulate and present the final project grade. + * Utilize a Pie chart to illustrate the contribution percentage for all team members. + * Offer a detailed breakdown of each member's contributions. +* Video Demo + * Team Introduction: + * Briefly introduce your team. + * Highlight member contributions with evidence: + * GitHub Contribution Report + * Commits + * Jira entries + * Documents + * Reports + * Project Introduction: + * Provide a concise overview of your project. + * If the project is published on a public website, demonstrate with the public website. + * Each member must showcase their contributions + +Ensure all documentation is clear, organized, and follows the specified guidelines. This comprehensive submission will facilitate a thorough understanding of your project, team dynamics, and individual contributions. + +# Oral defense guidelines + +1. **Project Self-assessment Report** + * Complete the report, print it, and present it to the teacher during the defense. +2. **GitHub Repository & Contributors Report** + * Share your GitHub repository with the teacher. + * Highlight the Contributors section to show who worked on which tasks. + * Explain your team's collaboration process and each member's responsibilities. +3. **Project Showcase** + * Present your project to the teacher, demonstrating its features and functionality. +4. **Grade Sheet Signature** + * Sign the class grade sheet as part of the final evaluation process. + diff --git a/Dockerfile b/Dockerfile index 8e96795..8b6093b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,9 +45,10 @@ RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=installer --chown=nextjs:nodejs /app . +COPY --chown=nextjs:nodejs database ./database -# Install serve globally for static site serving (web) -RUN npm install -g serve +# Install serve globally for static site serving (web) and pnpm for dev scripts +RUN npm install -g serve pnpm USER nextjs diff --git a/README.md b/README.md index b108697..5147658 100644 --- a/README.md +++ b/README.md @@ -18,117 +18,131 @@ Online learning platform built with React 19, NestJS 11, and Turborepo. - **Github Repo**: - **Vercel**: -## Structure +## Demo Accounts (Evaluator) + +Use these accounts to evaluate the platform in different roles. **Password for all: `Password@123`** + +| Role | Email | Features to Test | +| ---------- | ------------------------ | ------------------------------------------------------------- | +| Admin | `admin@learnix.edu` | Course moderation, user management, system stats | +| Instructor | `instructor@learnix.edu` | Create courses, manage lessons, generate AI quizzes | +| Student | `student@learnix.edu` | Enroll in courses, watch lessons, use Embedded IDE, quizzes | + +## Project Structure ```text -apps/web/ # React + Vite frontend -apps/api/ # NestJS backend -e2e/ # Playwright tests -api/ # Vercel serverless adapter +├── apps/ +│ ├── web/ # Frontend (React 19 + Vite) +│ └── api/ # Backend (NestJS 11) +├── database/ # Database migrations & seed data +│ ├── schema.sql # Complete PostgreSQL schema +│ ├── seed.sql # Sample data for development +│ └── README.md # Database import instructions +├── api/ # Vercel serverless adapter +├── e2e/ # End-to-end tests (Playwright) +├── docker-compose.yml # Local development containers +└── Makefile # Common development commands ``` -## Quick Start +### Folder Details -### Prerequisites +| Folder | Description | +| ---------- | ------------------------------------------------------------------- | +| `apps/web` | React frontend with Vite, TailwindCSS, TanStack Query, CodeMirror | +| `apps/api` | NestJS backend with TypeORM, Passport.js, WebSockets, Gemini AI | +| `database` | PostgreSQL schema and seed data with import instructions | +| `api` | Vercel serverless function adapter for backend deployment | +| `e2e` | Playwright E2E tests for critical user flows | -- Node.js >= 24 -- pnpm >= 10 -- Docker (for database) +## Quick Start for Evaluators (Docker - Recommended) -### Setup +If you are a lecturer or evaluator, the easiest way to launch the **entire platform** (Frontend, Backend, and Database) is using Docker. -1. **Install Dependencies** +### Prerequisites (Docker Setup) - ```bash - pnpm install - ``` +- Docker & Docker Compose installed -2. **Environment Variables** - Copy `.env.example` to `.env` in `apps/web` and `apps/api`. +### Launch Stack - ```bash - cp apps/web/.env.example apps/web/.env - cp apps/api/.env.example apps/api/.env - ``` +```bash +# Start all services (PostgreSQL, NestJS API, React Frontend) +docker compose up -d --build +``` -3. **Start Database** +### Initialize & Seed Database - ```bash - make db - ``` +Wait about 10 seconds for the containers to be ready, then run: -4. **Start Development Servers** +```bash +# This reset script will apply the schema and comprehensive seed data +docker exec -it learnix-backend pnpm --filter @repo/api db:reset +docker exec -it learnix-backend pnpm --filter @repo/api db:seed +``` - ```bash - make dev - # Web: http://localhost:5173 - # API: http://localhost:3000 - ``` +### Access URLs -## Quality Gates & Testing +- **Frontend (Student/Instructor/Admin Portal)**: [http://localhost:5173](http://localhost:5173) +- **Backend API Documentation**: [http://localhost:3000/api](http://localhost:3000/api) -This project enforces strict quality gates. All checks must pass with zero errors and zero warnings. +--- -```bash -# Run all pre-commit checks (Format, Lint, Typecheck, Unit Tests) -make pre +## Setup for Developers (Local Development) -# Run End-to-End Tests (Playwright) -make e2e +If you want to contribute to the code, use the following steps to run services locally. -# Build all packages -make build -``` +### Prerequisites (Local Setup) + +- Node.js >= 24, pnpm >= 10 +- PostgreSQL (Local or Docker) -### Individual Commands +### Initial Setup ```bash -pnpm dev # Development mode -pnpm build # Build for production -pnpm test # Unit/Integration tests (Vitest/Jest) -pnpm test:e2e # E2E tests (Playwright) -pnpm lint # Lint (ESLint) -pnpm typecheck # TypeScript check -pnpm format # Formatter (Prettier) +pnpm install +cp apps/web/.env.example apps/web/.env +cp apps/api/.env.example apps/api/.env ``` -## Deployment +### Start Database -- **Frontend**: Automatically deployed to Vercel on push to `main`. -- **Backend**: Deployed as Serverless Functions on Vercel. -- **Database**: Uses Aiven Cloud PostgreSQL. +If using the provided Docker PostgreSQL: -### Docker - -To run the application with Docker Compose: +```bash +docker compose up -d postgres +# Then reset and seed (using local node) +cd apps/api +pnpm db:reset +pnpm db:seed +``` -1. Ensure `.env` files are set up in `apps/web` and `apps/api`. -2. Run the compose file +### Run Development Servers ```bash -docker compose up -d --build +# From project root +pnpm dev ``` -- **Frontend**: -- **Backend API**: -- **Database**: `postgres` service (internal) +- Frontend: [http://localhost:5173](http://localhost:5173) +- API: [http://localhost:3000](http://localhost:3000) -To stop: +--- -```bash -docker compose down -``` +## Evaluation Accounts -## Tech Stack +**Password for all: `Password@123`** + +| Role | Email | Key Features | +| ---------- | ------------------------ | ----------------------------------------------------- | +| Admin | `admin@learnix.edu` | Course moderation, user management, system stats | +| Instructor | `instructor@learnix.edu` | Dashboard, course/lesson creation, AI quiz generation | +| Student | `student@learnix.edu` | Enrollment, learning viewer, embedded IDE, quizzes | -| Frontend | Backend | DevOps | -| -------------- | ----------------------- | -------------- | -| React 19 | NestJS 11 | Turborepo 2.7 | -| Vite 7 | TypeORM + PostgreSQL | GitHub Actions | -| TailwindCSS 4 | Passport.js (JWT/OAuth) | Playwright | -| TanStack Query | Gemini AI (Quizzes) | Vercel | -| CodeMirror 6 | Piston API (IDE) | Docker | +--- + +## Tech Stack -## License +- **Frontend**: React 19, Vite 7, TailwindCSS 4, TanStack Query, CodeMirror 6 +- **Backend**: NestJS 11, TypeORM, PostgreSQL, Passport.js (JWT/OAuth), Gemini AI +- **DevOps**: Turborepo 2.7, Docker, GitHub Actions, Vercel -UNLICENSED +For detailed database details, see [database/README.md](database/README.md). diff --git a/apps/api/package.json b/apps/api/package.json index 80ece16..66612e6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -18,7 +18,9 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", - "seed:admin": "ts-node -r tsconfig-paths/register src/scripts/create-admin.ts" + "seed:admin": "ts-node -r tsconfig-paths/register src/scripts/create-admin.ts", + "db:reset": "ts-node -r tsconfig-paths/register src/scripts/reset-db.ts", + "db:seed": "ts-node -r tsconfig-paths/register src/scripts/seed-from-sql.ts" }, "dependencies": { "@google/genai": "^1.34.0", @@ -79,11 +81,12 @@ "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-security": "^3.0.1", - "globals": "^16.5.0", + "globals": "^17.0.0", "jest": "^30.2.0", "supertest": "^7.1.4", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3", "typescript-eslint": "^8.51.0" }, @@ -104,4 +107,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/apps/api/src/scripts/check-users.ts b/apps/api/src/scripts/check-users.ts deleted file mode 100644 index 1017287..0000000 --- a/apps/api/src/scripts/check-users.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NestFactory } from '@nestjs/core'; - -import { AppModule } from '../app.module'; -import { UsersService } from '../users/users.service'; - -async function bootstrap(): Promise { - const app = await NestFactory.createApplicationContext(AppModule); - const usersService = app.get(UsersService); - - const emails = ['admin@example.com', 'instructor_mod_js@example.com']; - - for (const email of emails) { - const user = await usersService.findByEmail(email); - if (user) { - console.log(`User ${email}:`); - console.log(` ID: ${user.id}`); - console.log(` Role: ${user.role}`); - console.log(` Active: ${user.isActive}`); - console.log(` Verified: ${user.isEmailVerified}`); - console.log(` Password Hash: ${user.password ? 'Present' : 'Missing'}`); - } else { - console.log(`User ${email} NOT FOUND`); - } - } - - await app.close(); -} - -void bootstrap(); diff --git a/apps/api/src/scripts/create-admin.ts b/apps/api/src/scripts/create-admin.ts deleted file mode 100644 index 4e72902..0000000 --- a/apps/api/src/scripts/create-admin.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import * as bcrypt from 'bcrypt'; -import { type Repository } from 'typeorm'; - -import { AppModule } from '../app.module'; -import { User } from '../users/entities/user.entity'; -import { UserRole } from '../users/enums/user-role.enum'; -import { UsersService } from '../users/users.service'; - -async function bootstrap(): Promise { - const app = await NestFactory.createApplicationContext(AppModule); - const usersService = app.get(UsersService); - const usersRepository = app.get>(getRepositoryToken(User)); - - const email = 'admin@example.com'; - const password = 'Password123!'; - const fullName = 'Admin User'; - - console.warn(`Checking for user with email: ${email}`); - - let user = await usersService.findByEmail(email); - - if (!user) { - console.warn('User not found. Creating new admin user...'); - - try { - const result = await usersService.createWithActivationToken({ - email, - fullName, - password, - }); - - ({ user } = result); - console.warn(`User created with ID: ${user.id}`); - } catch (error) { - console.error('Error creating user:', error); - process.exit(1); - } - } else { - console.warn(`User found with ID: ${user.id}. Updating to admin...`); - } - - await usersService.updateRole(user.id, UserRole.ADMIN); - console.warn('Role updated to ADMIN'); - - // Force update password to ensure consistency with tests - const hashedPassword = await bcrypt.hash(password, 10); - await usersRepository - .createQueryBuilder() - .update(User) - .set({ password: hashedPassword }) - .where('id = :id', { id: user.id }) - .execute(); - console.warn('Password reset'); - - await usersService.activateUser(user.id); - console.warn('User activated'); - - console.warn('Admin user setup complete.'); - console.warn(`Email: ${email}`); - console.warn(`Password: ${password}`); - - await app.close(); -} - -void bootstrap(); diff --git a/apps/api/src/scripts/reset-db.ts b/apps/api/src/scripts/reset-db.ts new file mode 100644 index 0000000..bee57bd --- /dev/null +++ b/apps/api/src/scripts/reset-db.ts @@ -0,0 +1,27 @@ +import { NestFactory } from '@nestjs/core'; +import { DataSource } from 'typeorm'; +import { AppModule } from '../app.module'; + +async function bootstrap() { + console.log('Starting database reset...'); + const app = await NestFactory.createApplicationContext(AppModule); + const dataSource = app.get(DataSource); + + try { + console.log('Dropping database...'); + await dataSource.dropDatabase(); + console.log('Database dropped successfully.'); + + console.log('Synchronizing database...'); + await dataSource.synchronize(); + console.log('Database synchronized successfully.'); + + console.log('Database reset complete.'); + } catch (error) { + console.error('Error resetting database:', error); + } finally { + await app.close(); + } +} + +bootstrap(); diff --git a/apps/api/src/scripts/seed-from-sql.ts b/apps/api/src/scripts/seed-from-sql.ts new file mode 100644 index 0000000..dcc9cee --- /dev/null +++ b/apps/api/src/scripts/seed-from-sql.ts @@ -0,0 +1,38 @@ +import { NestFactory } from '@nestjs/core'; +import { DataSource } from 'typeorm'; +import * as fs from 'fs'; +import * as path from 'path'; +import { AppModule } from '../app.module'; + +async function bootstrap() { + console.log('Starting seed from SQL script...'); + const app = await NestFactory.createApplicationContext(AppModule); + const dataSource = app.get(DataSource); + + try { + const seedFilePath = path.join(process.cwd(), '../../database/seed.sql'); + console.log(`Reading seed file from: ${seedFilePath}`); + + if (!fs.existsSync(seedFilePath)) { + throw new Error(`Seed file not found at ${seedFilePath}`); + } + + const sql = fs.readFileSync(seedFilePath, 'utf8'); + + console.log('Executing SQL seed script...'); + + // We can't easily split by ; because of potential strings containing ; + // However, TypeORM's query method can execute multiple statements in one call in some PG drivers + // If not, we might need a better way to parse. + // For PostgreSQL, running the whole block often works if the driver supports it. + await dataSource.query(sql); + + console.log('Seed data applied successfully.'); + } catch (error) { + console.error('Error applying seed data:', error); + } finally { + await app.close(); + } +} + +bootstrap(); diff --git a/apps/api/src/scripts/seed-moderation.ts b/apps/api/src/scripts/seed-moderation.ts deleted file mode 100644 index 6aa5d16..0000000 --- a/apps/api/src/scripts/seed-moderation.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { type Repository } from 'typeorm'; - -import { AppModule } from '../app.module'; -import { User } from '../users/entities/user.entity'; -import { UserRole } from '../users/enums/user-role.enum'; -import { UsersService } from '../users/users.service'; - -async function bootstrap(): Promise { - const app = await NestFactory.createApplicationContext(AppModule); - const usersService = app.get(UsersService); - const usersRepository = app.get>(getRepositoryToken(User)); - - const instructorEmail = 'instructor_mod_js@example.com'; - const instructorPass = 'Password123!'; - - const instructor = await usersService.findByEmail(instructorEmail); - if (!instructor) { - console.warn('Creating instructor...'); - await usersService.create({ - email: instructorEmail, - password: instructorPass, - fullName: 'Test Instructor JS', - }); - } else { - console.warn('Instructor exists.'); - } - - console.warn('Updating instructor status and password...'); - const bcrypt = require('bcrypt'); - const hashedPassword = await bcrypt.hash(instructorPass, 10); - - // Force update verify status, role, and password - await usersRepository - .createQueryBuilder() - .update(User) - .set({ - isEmailVerified: true, - isActive: true, - role: UserRole.INSTRUCTOR, - password: hashedPassword, - }) - .where('email = :email', { email: instructorEmail }) - .execute(); - - console.warn('Instructor updated: Verified, Active, and Password reset.'); - - // Seed Student - const studentEmail = 'student_test@example.com'; - const studentPass = 'Password123!'; - const student = await usersService.findByEmail(studentEmail); - if (!student) { - console.warn('Creating student...'); - await usersService.create({ - email: studentEmail, - password: studentPass, - fullName: 'Test Student', - }); - } - - await usersRepository - .createQueryBuilder() - .update(User) - .set({ - isEmailVerified: true, - isActive: true, - role: UserRole.STUDENT, - password: hashedPassword, - }) - .where('email = :email', { email: studentEmail }) - .execute(); - - console.warn('Student updated: Verified, Active, and Password reset.'); - await app.close(); -} - -void bootstrap(); diff --git a/apps/api/src/scripts/seed-test-data.ts b/apps/api/src/scripts/seed-test-data.ts deleted file mode 100644 index 6e6fcb0..0000000 --- a/apps/api/src/scripts/seed-test-data.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import * as bcrypt from 'bcrypt'; -import { type Repository } from 'typeorm'; - -import { AppModule } from '../app.module'; -import { User } from '../users/entities/user.entity'; -import { UserRole } from '../users/enums/user-role.enum'; -import { UsersService } from '../users/users.service'; - -async function seedUser( - usersService: UsersService, - usersRepository: Repository, - email: string, - pass: string, - fullName: string, - role: UserRole, -) { - let user = await usersService.findByEmail(email); - if (!user) { - console.warn(`Creating ${role}: ${email}...`); - user = await usersService.create({ - email, - password: pass, - fullName, - }); - } else { - console.warn(`${role} exists: ${email}.`); - } - - const hashedPassword = await bcrypt.hash(pass, 10); - - // Force update verify status, role, and password - await usersRepository - .createQueryBuilder() - .update(User) - .set({ - isEmailVerified: true, - isActive: true, - role: role, - password: hashedPassword, - }) - .where('id = :id', { id: user.id }) - .execute(); - - console.warn(`${role} updated and verified.`); -} - -async function bootstrap(): Promise { - let app; - try { - app = await NestFactory.createApplicationContext(AppModule); - const usersService = app.get(UsersService); - const usersRepository = app.get(getRepositoryToken(User)); - - const defaultPass = 'Password123!'; - - // 1. Instructor - await seedUser( - usersService, - usersRepository, - 'instructor_mod_js@example.com', - defaultPass, - 'Test Instructor JS', - UserRole.INSTRUCTOR, - ); - - // 2. Admin - await seedUser( - usersService, - usersRepository, - 'admin@example.com', - defaultPass, - 'Test Admin', - UserRole.ADMIN, - ); - - // 3. Student - await seedUser( - usersService, - usersRepository, - 'student_test@example.com', - defaultPass, - 'Test Student', - UserRole.STUDENT, - ); - - console.log('Seeding completed successfully.'); - } catch (error) { - console.error('Seeding failed:', error); - process.exit(1); - } finally { - if (app) await app.close(); - } -} - -void bootstrap(); diff --git a/apps/web/package.json b/apps/web/package.json index 97e174a..2447ddf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -54,7 +54,7 @@ "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "tiptap-markdown": "^0.9.0", - "zod": "^4.3.2" + "zod": "^4.3.4" }, "devDependencies": { "@eslint/js": "^9.39.2", @@ -78,7 +78,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-security": "^3.0.1", - "globals": "^16.5.0", + "globals": "^17.0.0", "jsdom": "^27.4.0", "react-hook-form": "^7.69.0", "tw-animate-css": "^1.4.0", diff --git a/database/README.md b/database/README.md new file mode 100644 index 0000000..59bc026 --- /dev/null +++ b/database/README.md @@ -0,0 +1,79 @@ +# Learnix Database + +This folder contains the database schema and seed data for the Learnix online learning platform. + +## Prerequisites + +- PostgreSQL 18+ (or compatible version) +- Docker (optional, for containerized setup) + +## Files + +| File | Description | +| ------------ | ------------------------------------------------------------------------------- | +| `schema.sql` | Complete database schema with all tables, enums, indexes, and constraints | +| `seed.sql` | Comprehensive seed data with 5 complete courses, quizzes, and user interactions | + +## Quick Start (Using NestJS Scripts) + +```bash +cd apps/api +pnpm db:reset +pnpm db:seed +``` + +## Evaluation Accounts + +Use these accounts to test the platform. **Password for all: `Password@123`** + +| Email | Password | Role | +| ------------------------ | ------------ | ---------- | +| | Password@123 | Admin | +| | Password@123 | Instructor | +| | Password@123 | Student | + +## Connection Configuration + +Set these environment variables in `apps/api/.env`: + +```env +# Local Development +DB_HOST=localhost +DB_PORT=5432 +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_NAME=learnix +DB_SSL=false + +# Or use DATABASE_URL +DATABASE_URL=postgresql://user:password@host:5432/learnix +``` + +## Schema Overview + +```text +users # User accounts (admin, instructor, student) + └── external_auth # OAuth providers (Google, GitHub) + +courses # Course catalog + └── course_sections # Course sections/modules + └── lessons # Individual lessons + └── lesson_resources # Attachments/links + +enrollments # Student-course relationships +quizzes # Course quizzes + └── questions # Quiz questions +quiz_submissions # Student quiz attempts +payments # Course purchases +notifications # User notifications +``` + +## Reset Database + +To completely reset the database: + +```bash +cd apps/api +pnpm db:reset +pnpm db:seed +``` diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..f4b27dd --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,277 @@ +-- ============================================================================== +-- Learnix Database Schema +-- PostgreSQL Migration Script +-- Generated from TypeORM entities +-- ============================================================================== + +-- Drop tables if they exist (in reverse order of dependencies) +DROP TABLE IF EXISTS quiz_submissions CASCADE; +DROP TABLE IF EXISTS questions CASCADE; +DROP TABLE IF EXISTS quizzes CASCADE; +DROP TABLE IF EXISTS payments CASCADE; +DROP TABLE IF EXISTS notifications CASCADE; +DROP TABLE IF EXISTS lesson_resources CASCADE; +DROP TABLE IF EXISTS lessons CASCADE; +DROP TABLE IF EXISTS enrollments CASCADE; +DROP TABLE IF EXISTS course_sections CASCADE; +DROP TABLE IF EXISTS courses CASCADE; +DROP TABLE IF EXISTS external_auth CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +-- Drop types if they exist +DROP TYPE IF EXISTS user_role CASCADE; +DROP TYPE IF EXISTS course_status CASCADE; +DROP TYPE IF EXISTS course_level CASCADE; +DROP TYPE IF EXISTS quiz_status CASCADE; +DROP TYPE IF EXISTS payment_status CASCADE; +DROP TYPE IF EXISTS auth_provider CASCADE; +DROP TYPE IF EXISTS notification_type CASCADE; +DROP TYPE IF EXISTS lesson_type CASCADE; +DROP TYPE IF EXISTS resource_type CASCADE; + +-- ============================================================================== +-- ENUM TYPES +-- ============================================================================== + +CREATE TYPE user_role AS ENUM ('student', 'instructor', 'admin'); +CREATE TYPE course_status AS ENUM ('draft', 'pending', 'published', 'rejected'); +CREATE TYPE course_level AS ENUM ('beginner', 'intermediate', 'advanced'); +CREATE TYPE quiz_status AS ENUM ('draft', 'ai_generated', 'approved'); +CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed'); +CREATE TYPE auth_provider AS ENUM ('google', 'github'); +CREATE TYPE notification_type AS ENUM ('enrollment', 'payment_success', 'payment_failed', 'course_approved', 'course_rejected', 'course_submitted', 'course_completed', 'course_unenrollment', 'quiz_submitted', 'system'); +CREATE TYPE lesson_type AS ENUM ('standard', 'quiz'); +CREATE TYPE resource_type AS ENUM ('file', 'link'); + +-- ============================================================================== +-- TABLES +-- ============================================================================== + +-- ----------------------------------------------------------------------------- +-- Users Table +-- ----------------------------------------------------------------------------- +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255), + "fullName" VARCHAR(255), + "avatarUrl" VARCHAR(500), + "oauthAvatarUrl" VARCHAR(500), + role user_role, + "isActive" BOOLEAN NOT NULL DEFAULT TRUE, + "isEmailVerified" BOOLEAN NOT NULL DEFAULT FALSE, + "activationToken" VARCHAR(255), + "activationTokenExpiry" TIMESTAMPTZ, + "passwordResetToken" VARCHAR(255), + "passwordResetTokenExpiry" TIMESTAMPTZ, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ----------------------------------------------------------------------------- +-- External Auth Table (OAuth providers) +-- ----------------------------------------------------------------------------- +CREATE TABLE external_auth ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + provider auth_provider NOT NULL, + provider_id VARCHAR(255) NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + "accessToken" VARCHAR(500), + "refreshToken" VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (provider, provider_id) +); + +-- ----------------------------------------------------------------------------- +-- Courses Table +-- ----------------------------------------------------------------------------- +CREATE TABLE courses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + "thumbnailUrl" VARCHAR(500), + price DECIMAL(10, 2) NOT NULL DEFAULT 0, + level course_level NOT NULL DEFAULT 'beginner', + status course_status NOT NULL DEFAULT 'draft', + "isPublished" BOOLEAN NOT NULL DEFAULT FALSE, + tags TEXT, + instructor_id UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_courses_instructor ON courses(instructor_id); +CREATE INDEX idx_courses_status ON courses(status); +CREATE INDEX idx_courses_level ON courses(level); + +-- ----------------------------------------------------------------------------- +-- Course Sections Table +-- ----------------------------------------------------------------------------- +CREATE TABLE course_sections ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + "orderIndex" INTEGER NOT NULL DEFAULT 0, + course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_course_sections_course ON course_sections(course_id); + +-- ----------------------------------------------------------------------------- +-- Lessons Table +-- ----------------------------------------------------------------------------- +CREATE TABLE lessons ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + type lesson_type NOT NULL DEFAULT 'standard', + content JSONB, + "ideConfig" JSONB, + "durationSeconds" INTEGER NOT NULL DEFAULT 0, + "isFreePreview" BOOLEAN NOT NULL DEFAULT FALSE, + "orderIndex" INTEGER NOT NULL DEFAULT 0, + section_id UUID NOT NULL REFERENCES course_sections(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_lessons_section ON lessons(section_id); + +-- ----------------------------------------------------------------------------- +-- Lesson Resources Table +-- ----------------------------------------------------------------------------- +CREATE TABLE lesson_resources ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + type resource_type NOT NULL, + url VARCHAR(500) NOT NULL, + "fileSize" INTEGER, + "lessonId" UUID NOT NULL REFERENCES lessons(id) ON DELETE CASCADE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_lesson_resources_lesson ON lesson_resources("lessonId"); + +-- ----------------------------------------------------------------------------- +-- Enrollments Table +-- ----------------------------------------------------------------------------- +CREATE TABLE enrollments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + "completedLessonIds" TEXT, + "completedAt" TIMESTAMPTZ, + is_archived BOOLEAN NOT NULL DEFAULT FALSE, + archived_at TIMESTAMPTZ, + enrolled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, course_id) +); + +CREATE INDEX idx_enrollments_user ON enrollments(user_id); +CREATE INDEX idx_enrollments_course ON enrollments(course_id); + +-- ----------------------------------------------------------------------------- +-- Quizzes Table +-- ----------------------------------------------------------------------------- +CREATE TABLE quizzes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + description TEXT, + course_id UUID, + lesson_id UUID, + status quiz_status NOT NULL DEFAULT 'draft', + created_by UUID NOT NULL REFERENCES users(id), + ai_generated BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_quizzes_course ON quizzes(course_id); +CREATE INDEX idx_quizzes_lesson ON quizzes(lesson_id); +CREATE INDEX idx_quizzes_creator ON quizzes(created_by); + +-- ----------------------------------------------------------------------------- +-- Questions Table +-- ----------------------------------------------------------------------------- +CREATE TABLE questions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + quiz_id UUID NOT NULL REFERENCES quizzes(id) ON DELETE CASCADE, + "questionText" TEXT NOT NULL, + image_url TEXT, + options JSONB NOT NULL, + "correctAnswer" VARCHAR(10) NOT NULL, + explanation TEXT, + points INTEGER NOT NULL DEFAULT 1, + position INTEGER NOT NULL DEFAULT 0, + type VARCHAR(50) NOT NULL DEFAULT 'multiple_choice', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_questions_quiz ON questions(quiz_id); + +-- ----------------------------------------------------------------------------- +-- Quiz Submissions Table +-- ----------------------------------------------------------------------------- +CREATE TABLE quiz_submissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + quiz_id UUID NOT NULL REFERENCES quizzes(id), + user_id UUID NOT NULL REFERENCES users(id), + score REAL, + total_points REAL, + percentage REAL, + responses JSONB NOT NULL, + completed_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_quiz_submissions_quiz ON quiz_submissions(quiz_id); +CREATE INDEX idx_quiz_submissions_user ON quiz_submissions(user_id); + +-- ----------------------------------------------------------------------------- +-- Payments Table +-- ----------------------------------------------------------------------------- +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + course_id UUID NOT NULL REFERENCES courses(id), + amount DECIMAL(10, 2) NOT NULL, + currency VARCHAR(10) NOT NULL DEFAULT 'USD', + status payment_status NOT NULL DEFAULT 'pending', + transaction_id VARCHAR(255), + method VARCHAR(50) NOT NULL DEFAULT 'credit_card', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_payments_user ON payments(user_id); +CREATE INDEX idx_payments_course ON payments(course_id); +CREATE INDEX idx_payments_status ON payments(status); + +-- ----------------------------------------------------------------------------- +-- Notifications Table +-- ----------------------------------------------------------------------------- +CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + message TEXT NOT NULL, + type VARCHAR(20) NOT NULL DEFAULT 'info', + "notificationType" notification_type, + "userId" UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + "isRead" BOOLEAN NOT NULL DEFAULT FALSE, + link VARCHAR(500), + metadata JSONB, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_notifications_user_read ON notifications("userId", "isRead"); +CREATE INDEX idx_notifications_user_type ON notifications("userId", "notificationType"); + +-- ============================================================================== +-- END OF SCHEMA +-- ============================================================================== diff --git a/database/seed.sql b/database/seed.sql new file mode 100644 index 0000000..381e186 --- /dev/null +++ b/database/seed.sql @@ -0,0 +1,448 @@ +-- ============================================================================== +-- Learnix COMPREHENSIVE SEED DATA - Proper Tiptap Format +-- All Tables | Proper JSON Structure | Complete Curricula for All Courses +-- ============================================================================== + +TRUNCATE quiz_submissions, questions, quizzes, payments, notifications, + lesson_resources, lessons, enrollments, course_sections, courses, + external_auth, users CASCADE; + +-- Password for all: Password@123 +-- Hash: $2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri + +-- ============================================================================== +-- 1. USERS +-- ============================================================================== + +INSERT INTO users (id, email, password, "fullName", "avatarUrl", role, "isActive", "isEmailVerified", "createdAt", "updatedAt") +VALUES + ('00000000-0000-0000-0000-000000000001', 'admin@learnix.edu', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Admin User', 'https://api.dicebear.com/7.x/avataaars/svg?seed=admin', 'admin', TRUE, TRUE, NOW(), NOW()), + ('00000000-0000-0000-0000-000000000002', 'instructor@learnix.edu', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Instructor User', 'https://api.dicebear.com/7.x/avataaars/svg?seed=instructor', 'instructor', TRUE, TRUE, NOW(), NOW()), + ('00000000-0000-0000-0000-000000000003', 'student@learnix.edu', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Student User', 'https://api.dicebear.com/7.x/avataaars/svg?seed=student', 'student', TRUE, TRUE, NOW(), NOW()), + ('10000000-0000-0000-0000-000000000001', 'dr.smith@learnix.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Dr. Alan Smith', 'https://api.dicebear.com/7.x/avataaars/svg?seed=alan', 'instructor', TRUE, TRUE, NOW() - INTERVAL '8 months', NOW()), + ('10000000-0000-0000-0000-000000000002', 'prof.chen@learnix.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Prof. Sarah Chen', 'https://api.dicebear.com/7.x/avataaars/svg?seed=sarah', 'instructor', TRUE, TRUE, NOW() - INTERVAL '7 months', NOW()), + ('10000000-0000-0000-0000-000000000003', 'eng.kumar@learnix.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Eng. Raj Kumar', 'https://api.dicebear.com/7.x/avataaars/svg?seed=raj', 'instructor', TRUE, TRUE, NOW() - INTERVAL '6 months', NOW()), + ('20000000-0000-0000-0000-000000000001', 'alice.wonder@student.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Alice Wonderland', 'https://api.dicebear.com/7.x/avataaars/svg?seed=alice', 'student', TRUE, TRUE, NOW() - INTERVAL '5 months', NOW()), + ('20000000-0000-0000-0000-000000000002', 'bob.builder@student.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Bob Builder', 'https://api.dicebear.com/7.x/avataaars/svg?seed=bob', 'student', TRUE, TRUE, NOW() - INTERVAL '4 months', NOW()), + ('20000000-0000-0000-0000-000000000003', 'charlie.brown@student.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Charlie Brown', 'https://api.dicebear.com/7.x/avataaars/svg?seed=charlie', 'student', TRUE, TRUE, NOW() - INTERVAL '3 months', NOW()), + ('20000000-0000-0000-0000-000000000004', 'diana.prince@student.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Diana Prince', 'https://api.dicebear.com/7.x/avataaars/svg?seed=diana', 'student', TRUE, TRUE, NOW() - INTERVAL '2 months', NOW()), + ('20000000-0000-0000-0000-000000000005', 'ethan.hunt@student.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Ethan Hunt', 'https://api.dicebear.com/7.x/avataaars/svg?seed=ethan', 'student', TRUE, TRUE, NOW() - INTERVAL '1 month', NOW()), + ('20000000-0000-0000-0000-000000000006', 'fiona.apple@student.com', '$2b$10$0l/2jh5vOJVkkgjBbkfgveGpAYkGXLHiaVwI7AtMQHgzbkRlOt2Ri', 'Fiona Apple', 'https://api.dicebear.com/7.x/avataaars/svg?seed=fiona', 'student', TRUE, TRUE, NOW() - INTERVAL '3 weeks', NOW()); + +-- ============================================================================== +-- 2. EXTERNAL AUTH +-- ============================================================================== + +INSERT INTO external_auth (id, provider, provider_id, user_id, "accessToken", "refreshToken", created_at, updated_at) +VALUES + (gen_random_uuid(), 'google', 'google_alice_123456', '20000000-0000-0000-0000-000000000001', 'ya29.mock_access_token_alice', 'mock_refresh_token_alice', NOW() - INTERVAL '5 months', NOW()), + (gen_random_uuid(), 'github', 'github_bob_789012', '20000000-0000-0000-0000-000000000002', 'gho_mock_access_token_bob', 'mock_refresh_token_bob', NOW() - INTERVAL '4 months', NOW()), + (gen_random_uuid(), 'google', 'google_charlie_345678', '20000000-0000-0000-0000-000000000003', 'ya29.mock_access_token_charlie', 'mock_refresh_token_charlie', NOW() - INTERVAL '3 months', NOW()); + +-- ============================================================================== +-- 3. COURSES (5 Complete Courses) +-- ============================================================================== + +INSERT INTO courses (id, title, description, "thumbnailUrl", price, level, status, "isPublished", tags, instructor_id, created_at, updated_at) +VALUES + ('c0000001-0000-0000-0000-000000000001', 'Complete Fullstack Web Development with React & Node.js', + 'Master modern web development from frontend to backend. Build production-ready applications using React 19, Node.js, TypeScript, and PostgreSQL.', + 'https://images.unsplash.com/photo-1627398242454-45a1465c2479?w=800', + 129.99, 'beginner', 'published', TRUE, 'web,react,nodejs,typescript', + '00000000-0000-0000-0000-000000000002', NOW() - INTERVAL '6 months', NOW()), + + ('c0000001-0000-0000-0000-000000000002', 'Artificial Intelligence & Machine Learning with Python', + 'Dive deep into AI and ML concepts. Master TensorFlow, PyTorch, neural networks, NLP, and computer vision.', + 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + 179.99, 'advanced', 'published', TRUE, 'ai,python,machinelearning', + '10000000-0000-0000-0000-000000000001', NOW() - INTERVAL '5 months', NOW()), + + ('c0000001-0000-0000-0000-000000000003', 'DevOps Mastery: Docker, Kubernetes & AWS', + 'Complete DevOps journey covering containerization, orchestration, CI/CD pipelines, and cloud deployment.', + 'https://images.unsplash.com/photo-1667372393086-9d4001d51cf1?w=800', + 149.99, 'intermediate', 'published', TRUE, 'devops,docker,kubernetes,aws', + '10000000-0000-0000-0000-000000000003', NOW() - INTERVAL '4 months', NOW()), + + ('c0000001-0000-0000-0000-000000000004', 'Modern UI/UX Design with Figma', + 'Learn professional UI/UX design principles, user research, wireframing, and building scalable design systems.', + 'https://images.unsplash.com/photo-1561070791-2526d30994b5?w=800', + 89.99, 'beginner', 'published', TRUE, 'design,uiux,figma', + '10000000-0000-0000-0000-000000000002', NOW() - INTERVAL '3 months', NOW()), + + ('c0000001-0000-0000-0000-000000000005', 'Data Science & Analytics with Python', + 'Master data analysis, visualization, statistical modeling, and predictive analytics with pandas, NumPy, and scikit-learn.', + 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800', + 139.99, 'intermediate', 'published', TRUE, 'datascience,python,analytics', + '10000000-0000-0000-0000-000000000001', NOW() - INTERVAL '5 months', NOW()); + +-- ============================================================================== +-- 4. COURSE SECTIONS +-- ============================================================================== + +INSERT INTO course_sections (id, title, "orderIndex", course_id) VALUES +-- Course 1 +('a1000001-0000-0000-0000-000000000001', 'Getting Started & Fundamentals', 0, 'c0000001-0000-0000-0000-000000000001'), +('a1000001-0000-0000-0000-000000000002', 'Frontend with React', 1, 'c0000001-0000-0000-0000-000000000001'), +('a1000001-0000-0000-0000-000000000003', 'Backend with Node.js', 2, 'c0000001-0000-0000-0000-000000000001'), +-- Course 2 +('a1000002-0000-0000-0000-000000000001', 'Python & ML Basics', 0, 'c0000001-0000-0000-0000-000000000002'), +('a1000002-0000-0000-0000-000000000002', 'Neural Networks', 1, 'c0000001-0000-0000-0000-000000000002'), +-- Course 3 +('a1000003-0000-0000-0000-000000000001', 'Docker & Containers', 0, 'c0000001-0000-0000-0000-000000000003'), +('a1000003-0000-0000-0000-000000000002', 'Kubernetes', 1, 'c0000001-0000-0000-0000-000000000003'), +-- Course 4 +('a1000004-0000-0000-0000-000000000001', 'Design Principles', 0, 'c0000001-0000-0000-0000-000000000004'), +('a1000004-0000-0000-0000-000000000002', 'Figma Mastery', 1, 'c0000001-0000-0000-0000-000000000004'), +-- Course 5 +('a1000005-0000-0000-0000-000000000001', 'Data Analysis', 0, 'c0000001-0000-0000-0000-000000000005'), +('a1000005-0000-0000-0000-000000000002', 'Visualization', 1, 'c0000001-0000-0000-0000-000000000005'); + +-- ============================================================================== +-- 5. LESSONS (Proper Tiptap JSON Format) +-- ============================================================================== + +INSERT INTO lessons (id, title, type, content, "ideConfig", "durationSeconds", "isFreePreview", "orderIndex", section_id) VALUES +-- Course 1 Lessons +('b1000001-0000-0000-0000-000000000001', 'Welcome to Fullstack Development', 'standard', + '[{"id":"intro1","type":"text","content":"# Welcome to Fullstack Web Development\n\nIn this comprehensive course, you will learn to build **production-ready web applications** from scratch.\n\n### What You Will Learn:\n\n- Modern React 19 with Server Components\n- TypeScript for type safety\n- Node.js and Express backend\n- PostgreSQL database design","orderIndex":0},{"id":"introimg","type":"image","content":"https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=800","metadata":{"caption":"Modern web development workspace"},"orderIndex":1},{"id":"vid1","type":"video","content":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","orderIndex":2}]', + NULL, 720, TRUE, 0, 'a1000001-0000-0000-0000-000000000001'), + +('b1000001-0000-0000-0000-000000000002', 'HTML5 and CSS3 Fundamentals', 'standard', + '[{"id":"html1","type":"text","content":"## Modern HTML and CSS\n\nLearn the building blocks of the web:\n\n- HTML5 Semantic Elements\n- CSS3 Flexbox and Grid\n- Responsive Design","orderIndex":0},{"id":"htmlcode","type":"code","content":"
\n \n
\n
\n
\n

Article Title

\n

Content here...

\n
\n
","metadata":{"language":"html"},"orderIndex":1}]', + NULL, 1200, TRUE, 1, 'a1000001-0000-0000-0000-000000000001'), + +('b1000001-0000-0000-0000-000000000003', 'JavaScript ES6+ Essentials', 'standard', + '[{"id":"js1","type":"text","content":"## Modern JavaScript\n\nMaster ES6+ features:\n\n- Arrow Functions\n- Destructuring\n- Async/Await\n- Modules","orderIndex":0}]', + NULL, 1800, FALSE, 2, 'a1000001-0000-0000-0000-000000000001'), + +('b1000001-0000-0000-0000-000000000004', 'React Components and Props', 'standard', + '[{"id":"react1","type":"text","content":"# Building React Components\n\nComponents are the building blocks of React applications. Learn to create **reusable** and **composable** components.\n\n### Functional Components","orderIndex":0},{"id":"reactcode","type":"code","content":"function Welcome({ name }) {\n return

Hello, {name}!

;\n}\n\n// Usage\n","metadata":{"language":"javascript"},"orderIndex":1},{"id":"reactimg","type":"image","content":"https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800","metadata":{"caption":"React component architecture"},"orderIndex":2}]', + NULL, 2100, FALSE, 0, 'a1000001-0000-0000-0000-000000000002'), + +('b1000001-0000-0000-0000-000000000005', 'State Management with Hooks', 'standard', + '[{"id":"hooks1","type":"text","content":"## React Hooks\n\nHooks let you use state in functional components:\n\n- useState - Manage state\n- useEffect - Side effects\n- useContext - Access context","orderIndex":0}]', + NULL, 2400, FALSE, 1, 'a1000001-0000-0000-0000-000000000002'), + +('b1000001-0000-0000-0000-000000000006', 'Node.js and Express Fundamentals', 'standard', + '[{"id":"node1","type":"text","content":"# Backend with Node.js\n\nNode.js allows you to run JavaScript on the server. Express is a minimal web framework.","orderIndex":0}]', + NULL, 2700, FALSE, 0, 'a1000001-0000-0000-0000-000000000003'), + +-- Course 2 Lessons +('b1000002-0000-0000-0000-000000000001', 'Introduction to Machine Learning', 'standard', + '[{"id":"ml1","type":"text","content":"# Welcome to AI and Machine Learning\n\nDiscover the world of artificial intelligence.\n\n- Supervised Learning\n- Unsupervised Learning\n- Deep Learning","orderIndex":0},{"id":"mlimg","type":"image","content":"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800","metadata":{"caption":"AI and machine learning visualization"},"orderIndex":1},{"id":"mlcode","type":"code","content":"from sklearn.model_selection import train_test_split\nfrom sklearn.linear_model import LogisticRegression\n\n# Split data\nX_train, X_test, y_train, y_test = train_test_split(X, y)\n\n# Train model\nmodel = LogisticRegression()\nmodel.fit(X_train, y_train)\n\n# Evaluate\nscore = model.score(X_test, y_test)\nprint(f''Accuracy: {score}'')","metadata":{"language":"python"},"orderIndex":2}]', + NULL, 900, TRUE, 0, 'a1000002-0000-0000-0000-000000000001'), + +('b1000002-0000-0000-0000-000000000002', 'Python for Data Science', 'standard', + '[{"id":"py1","type":"text","content":"## Python Libraries for ML\n\nMaster essential libraries:\n\n- NumPy - Numerical computing\n- Pandas - Data manipulation\n- Scikit-learn - ML algorithms","orderIndex":0}]', + NULL, 1800, TRUE, 1, 'a1000002-0000-0000-0000-000000000001'), + +('b1000002-0000-0000-0000-000000000003', 'Neural Networks Architecture', 'standard', + '[{"id":"nn1","type":"text","content":"# Understanding Neural Networks\n\nLearn how neural networks work.","orderIndex":0}]', + NULL, 2400, FALSE, 0, 'a1000002-0000-0000-0000-000000000002'), + +-- Course 3 Lessons +('b1000003-0000-0000-0000-000000000001', 'Docker Fundamentals', 'standard', + '[{"id":"docker1","type":"text","content":"# Introduction to Docker\n\nLearn containerization with Docker. Containers package your application with all its dependencies.","orderIndex":0},{"id":"dockerimg","type":"image","content":"https://images.unsplash.com/photo-1605745341112-85968b19335b?w=800","metadata":{"caption":"Docker containers architecture"},"orderIndex":1},{"id":"dockercode","type":"code","content":"FROM node:20-alpine\n\nWORKDIR /app\n\nCOPY package*.json ./\nRUN npm install\n\nCOPY . .\n\nEXPOSE 3000\n\nCMD [\"npm\", \"start\"]","metadata":{"language":"dockerfile"},"orderIndex":2}]', + NULL, 1800, TRUE, 0, 'a1000003-0000-0000-0000-000000000001'), + +('b1000003-0000-0000-0000-000000000002', 'Kubernetes Basics', 'standard', + '[{"id":"k8s1","type":"text","content":"## Kubernetes Orchestration\n\nMaster container orchestration.","orderIndex":0}]', + NULL, 2100, FALSE, 0, 'a1000003-0000-0000-0000-000000000002'), + +-- Course 4 Lessons +('b1000004-0000-0000-0000-000000000001', 'Design Principles', 'standard', + '[{"id":"design1","type":"text","content":"# UI/UX Design Fundamentals\n\nLearn core design principles:\n\n- **Contrast** - Make important elements stand out\n- **Alignment** - Create visual connections\n- **Repetition** - Build consistency\n- **Proximity** - Group related items","orderIndex":0},{"id":"designimg","type":"image","content":"https://images.unsplash.com/photo-1561070791-2526d30994b5?w=800","metadata":{"caption":"UI/UX design principles in action"},"orderIndex":1}]', + NULL, 1500, TRUE, 0, 'a1000004-0000-0000-0000-000000000001'), + +('b1000004-0000-0000-0000-000000000002', 'Figma Essentials', 'standard', + '[{"id":"figma1","type":"text","content":"## Mastering Figma\n\nFigma is the leading collaborative design tool.","orderIndex":0}]', + NULL, 1800, TRUE, 1, 'a1000004-0000-0000-0000-000000000002'), + +-- Course 5 Lessons +('b1000005-0000-0000-0000-000000000001', 'Pandas for Data Analysis', 'standard', + '[{"id":"pandas1","type":"text","content":"# Data Analysis with Pandas\n\nPandas is essential for data manipulation in Python. Work with DataFrames and Series to analyze data efficiently.","orderIndex":0},{"id":"pandascode","type":"code","content":"import pandas as pd\n\n# Read CSV file\ndf = pd.read_csv(''data.csv'')\n\n# Display first rows\nprint(df.head())\n\n# Get summary statistics\nprint(df.describe())\n\n# Filter data\nfiltered = df[df[''age''] > 25]\n\n# Group and aggregate\ngrouped = df.groupby(''category'').mean()","metadata":{"language":"python"},"orderIndex":1},{"id":"pandasimg","type":"image","content":"https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800","metadata":{"caption":"Data analysis visualization"},"orderIndex":2}]', + NULL, 2000, TRUE, 0, 'a1000005-0000-0000-0000-000000000001'), + +('b1000005-0000-0000-0000-000000000002', 'Data Visualization', 'standard', + '[{"id":"viz1","type":"text","content":"## Visualizing Data\n\nCreate compelling visualizations with Matplotlib.","orderIndex":0}]', + NULL, 1600, FALSE, 0, 'a1000005-0000-0000-0000-000000000002'); + +-- ============================================================================== +-- 6. LESSON RESOURCES +-- ============================================================================== + +INSERT INTO lesson_resources (id, title, type, url, "fileSize", "lessonId", "createdAt", "updatedAt") VALUES +(gen_random_uuid(), 'React Documentation', 'link', 'https://react.dev', NULL, 'b1000001-0000-0000-0000-000000000004', NOW(), NOW()), +(gen_random_uuid(), 'Node.js Best Practices', 'link', 'https://github.com/goldbergyoni/nodebestpractices', NULL, 'b1000001-0000-0000-0000-000000000006', NOW(), NOW()), +(gen_random_uuid(), 'TensorFlow Docs', 'link', 'https://www.tensorflow.org/learn', NULL, 'b1000002-0000-0000-0000-000000000003', NOW(), NOW()), +(gen_random_uuid(), 'Docker Documentation', 'link', 'https://docs.docker.com', NULL, 'b1000003-0000-0000-0000-000000000001', NOW(), NOW()), +(gen_random_uuid(), 'Figma Community', 'link', 'https://www.figma.com/community', NULL, 'b1000004-0000-0000-0000-000000000002', NOW(), NOW()), +(gen_random_uuid(), 'Pandas Documentation', 'link', 'https://pandas.pydata.org/docs', NULL, 'b1000005-0000-0000-0000-000000000001', NOW(), NOW()); + +-- ============================================================================== +-- 7. QUIZZES +-- ============================================================================== + +INSERT INTO quizzes (id, title, description, course_id, lesson_id, status, created_by, ai_generated, created_at, updated_at) VALUES +('d1000001-0000-0000-0000-000000000001', 'Fullstack Fundamentals Quiz', 'Test your web development knowledge', 'c0000001-0000-0000-0000-000000000001', NULL, 'approved', '00000000-0000-0000-0000-000000000002', FALSE, NOW(), NOW()), +('d1000002-0000-0000-0000-000000000001', 'Machine Learning Basics', 'ML fundamentals assessment', 'c0000001-0000-0000-0000-000000000002', NULL, 'approved', '10000000-0000-0000-0000-000000000001', FALSE, NOW(), NOW()), +('d1000003-0000-0000-0000-000000000001', 'Docker and Kubernetes Quiz', 'Container orchestration check', 'c0000001-0000-0000-0000-000000000003', NULL, 'approved', '10000000-0000-0000-0000-000000000003', FALSE, NOW(), NOW()), +('d1000004-0000-0000-0000-000000000001', 'UI/UX Design Principles', 'Design fundamentals', 'c0000001-0000-0000-0000-000000000004', NULL, 'approved', '10000000-0000-0000-0000-000000000002', FALSE, NOW(), NOW()), +('d1000005-0000-0000-0000-000000000001', 'Data Science Fundamentals', 'Python data analysis', 'c0000001-0000-0000-0000-000000000005', NULL, 'approved', '10000000-0000-0000-0000-000000000001', FALSE, NOW(), NOW()); + +-- ============================================================================== +-- 8. QUESTIONS +-- ============================================================================== + +INSERT INTO questions (id, quiz_id, "questionText", image_url, options, "correctAnswer", explanation, points, position, type, created_at) VALUES +('e1000001-0000-0000-0000-000000000001', 'd1000001-0000-0000-0000-000000000001', + 'Which HTML5 element is used for navigation?', NULL, + '["