diff --git a/.cursor/rules/00-core.mdc b/.cursor/rules/00-core.mdc
new file mode 100644
index 00000000..e7524b7a
--- /dev/null
+++ b/.cursor/rules/00-core.mdc
@@ -0,0 +1,21 @@
+---
+description: Core context & output contract for AiM
+globs:
+ - "**/*"
+alwaysApply: true
+---
+
+# Core Project Context (AiM)
+STACK: Next.js 15, React 19, TS, Prisma + Postgres, NextAuth, shadcn/ui, Zod.
+DOMAINS: Organization, Campaign, Content, Asset, Schedule, AnalyticsEvent.
+
+## OUTPUT CONTRACT (include in every response)
+1) CHANGES — file list + brief diff
+2) COMMANDS — install/migrate/dev/test
+3) TESTS/RESULTS — build/typecheck/lint/health logs
+4) GIT — branch name, commit SHAs, PR link (if cloud)
+
+MUST READ (if present): docs/SPEC.md, docs/data-model.md, docs/api/*.md
+DO NOT:
+- Touch real `.env`; only update `.env.example`
+- Add dependencies without explicit approval
diff --git a/.cursor/rules/01-guardrails.mdc b/.cursor/rules/01-guardrails.mdc
new file mode 100644
index 00000000..a4be43c0
--- /dev/null
+++ b/.cursor/rules/01-guardrails.mdc
@@ -0,0 +1,22 @@
+---
+description: Guardrails — permissions, limits, safety
+globs:
+ - "app/**"
+ - "features/**"
+ - "prisma/**"
+ - "app/api/**"
+ - "lib/**"
+alwaysApply: false
+---
+
+# Guardrails
+WRITE ALLOWED: app/**, features/**, lib/**, prisma/**, app/api/**
+ASK FIRST: package.json, pnpm-lock.yaml, next.config.ts, .github/workflows/**
+
+SECURITY:
+- Mọi route phải check session + RBAC
+- Validate input bằng Zod; trả JSON { ok, data | error }
+
+LIMITS:
+- ≤ 20 files / ≤ 800 lines diff / PR; vượt ngưỡng → dừng và hỏi
+- Không ghi logs nhạy cảm (PII)
diff --git a/.cursor/rules/02-repo-map.mdc b/.cursor/rules/02-repo-map.mdc
new file mode 100644
index 00000000..abd6ca44
--- /dev/null
+++ b/.cursor/rules/02-repo-map.mdc
@@ -0,0 +1,16 @@
+---
+description: Repo map & module boundaries for AiM
+globs:
+ - "**/*"
+alwaysApply: false
+---
+
+# Repo Map & Boundaries
+FEATURES: features/{auth,orgs,campaigns,content,assets,calendar,analytics,settings}
+API: app/api/[orgId]/{campaigns,content,assets,schedules,analytics}/...
+DATA: prisma/schema.prisma (source of truth)
+UI: components/layout/*, components/ui/*, components/dashboards/*
+
+RULE:
+- Tôn trọng boundaries: service/repo/UI tách lớp
+- Không gọi Prisma trực tiếp từ UI client
\ No newline at end of file
diff --git a/.cursor/rules/03-sop-ai-sse.mdc b/.cursor/rules/03-sop-ai-sse.mdc
new file mode 100644
index 00000000..be465e56
--- /dev/null
+++ b/.cursor/rules/03-sop-ai-sse.mdc
@@ -0,0 +1,20 @@
+---
+description: SOP for AI-SSE — how to deliver a PR
+globs:
+ - "docs/tasks/**"
+ - "app/**"
+ - "features/**"
+ - "app/api/**"
+ - "prisma/**"
+alwaysApply: false
+---
+
+# SOP for AI-SSE (per PR)
+ACCEPTANCE (check all):
+- Feature chạy sau khi `pnpm dev`
+- Inputs validated (Zod); Auth + RBAC: 401/403 đúng chỗ
+- Có migration + seed + rollback note (dev)
+- CI pass (build, typecheck, lint, /api/health)
+
+RESPONSE FORMAT = theo OUTPUT CONTRACT.
+Nếu chạm schema/kiến trúc → tạo docs/adr/####-*.md
diff --git a/.cursor/rules/04-api-style.mdc b/.cursor/rules/04-api-style.mdc
new file mode 100644
index 00000000..4041fd2a
--- /dev/null
+++ b/.cursor/rules/04-api-style.mdc
@@ -0,0 +1,16 @@
+---
+description: API style & contracts for route handlers
+globs:
+ - "app/api/**"
+ - "lib/**"
+alwaysApply: false
+---
+
+# API Style (Next.js Route Handlers)
+RESPONSE SHAPE:
+{ "ok": true, "data": ... } | { "ok": false, "error": { "code", "message", "details?" } }
+
+VALIDATION: Zod per route → 400 invalid; 404 not found.
+ORG SCOPING: Lấy orgId từ path; cấm cross-org access.
+PAGINATION: ?page=&limit= → { items, total, page, limit }
+ERRORS: Dùng mã ngắn (e.g., E_VALIDATION, E_FORBIDDEN)
diff --git a/.cursor/rules/05-rbac-security.mdc b/.cursor/rules/05-rbac-security.mdc
new file mode 100644
index 00000000..6a1b5f21
--- /dev/null
+++ b/.cursor/rules/05-rbac-security.mdc
@@ -0,0 +1,17 @@
+---
+description: RBAC roles & security checklist
+globs:
+ - "app/api/**"
+ - "lib/**"
+ - "middleware.ts"
+alwaysApply: false
+---
+
+# RBAC & Security
+ROLES: ADMIN, BRAND_OWNER, CREATOR (mặc định deny-by-default).
+HELPERS: lib/rbac.ts — kiểm tra quyền theo orgId + vai trò.
+
+CHECKLIST:
+- [ ] Session bắt buộc cho routes không public
+- [ ] Sanitize input/output; strip secrets khỏi logs
+- [ ] Rate limit (nếu có hành vi ghi/nhạy cảm)
diff --git a/.cursor/rules/06-ci-quality.mdc b/.cursor/rules/06-ci-quality.mdc
new file mode 100644
index 00000000..ec959043
--- /dev/null
+++ b/.cursor/rules/06-ci-quality.mdc
@@ -0,0 +1,18 @@
+---
+description: CI pipeline & quality gates
+globs:
+ - ".github/workflows/**"
+ - "**/*"
+alwaysApply: false
+---
+
+# CI & Quality Gates
+CI MUST:
+- build, typecheck, lint
+- curl /api/health trả 200
+
+PR CHECKLIST (đi vào PR template):
+- [ ] Zod validation
+- [ ] Auth + RBAC checks
+- [ ] Migration/seed + rollback note
+- [ ] CI xanh
diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..54aed853
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,10 @@
+# Database (Postgres)
+DATABASE_URL="postgresql://postgres:postgres@localhost:5432/circle_dev?schema=public"
+
+# NextAuth
+NEXTAUTH_URL="http://localhost:3000"
+NEXTAUTH_SECRET="changeme-in-local"
+
+# Dev-only credentials login
+DEV_LOGIN_PASSWORD="dev"
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..3bf624f0
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,49 @@
+name: CI
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ branches: [ main, develop ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Lint
+ run: pnpm run lint
+
+ - name: Typecheck
+ run: pnpm run typecheck
+
+ - name: Run unit tests
+ run: pnpm run test
+
+ - name: Build
+ run: pnpm run build
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps
+
+ - name: Run E2E tests
+ run: pnpm run test:e2e
+
+ - name: Health check
+ run: |
+ pnpm run start &
+ sleep 10
+ curl -f http://localhost:3000/api/health || exit 1
+ pkill -f "next start" || true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4f9a9e88..2e91c277 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,10 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
-.env*
+.env
+.env.local
+.env.production
+# .env.example is allowed to be committed
# vercel
.vercel
diff --git a/.obsidian/app.json b/.obsidian/app.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/.obsidian/app.json
@@ -0,0 +1 @@
+{}
diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/.obsidian/appearance.json
@@ -0,0 +1 @@
+{}
diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json
new file mode 100644
index 00000000..aa7bd982
--- /dev/null
+++ b/.obsidian/core-plugins.json
@@ -0,0 +1,33 @@
+{
+ "file-explorer": true,
+ "global-search": true,
+ "switcher": true,
+ "graph": true,
+ "backlink": true,
+ "canvas": true,
+ "outgoing-link": true,
+ "tag-pane": true,
+ "footnotes": false,
+ "properties": false,
+ "page-preview": true,
+ "daily-notes": true,
+ "templates": true,
+ "note-composer": true,
+ "command-palette": true,
+ "slash-command": false,
+ "editor-status": true,
+ "bookmarks": true,
+ "markdown-importer": false,
+ "zk-prefixer": false,
+ "random-note": false,
+ "outline": true,
+ "word-count": true,
+ "slides": false,
+ "audio-recorder": false,
+ "workspaces": false,
+ "file-recovery": true,
+ "publish": false,
+ "sync": true,
+ "bases": true,
+ "webviewer": false
+}
diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json
new file mode 100644
index 00000000..ec1b1390
--- /dev/null
+++ b/.obsidian/workspace.json
@@ -0,0 +1,211 @@
+{
+ "main": {
+ "id": "893e4335666bb73c",
+ "type": "split",
+ "children": [
+ {
+ "id": "11c40342203930f7",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "b1d29e7c72ab1df3",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "docs/ui/campaigns.md",
+ "mode": "source",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "campaigns"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "vertical"
+ },
+ "left": {
+ "id": "72e3f419536dccb2",
+ "type": "split",
+ "children": [
+ {
+ "id": "15caff196190004c",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "5c458420e26c3b03",
+ "type": "leaf",
+ "state": {
+ "type": "file-explorer",
+ "state": {
+ "sortOrder": "alphabetical",
+ "autoReveal": false
+ },
+ "icon": "lucide-folder-closed",
+ "title": "Files"
+ }
+ },
+ {
+ "id": "53b843ad39ed6c0f",
+ "type": "leaf",
+ "state": {
+ "type": "search",
+ "state": {
+ "query": "",
+ "matchingCase": false,
+ "explainSearch": false,
+ "collapseAll": false,
+ "extraContext": false,
+ "sortOrder": "alphabetical"
+ },
+ "icon": "lucide-search",
+ "title": "Search"
+ }
+ },
+ {
+ "id": "96501c8e7e5e3dce",
+ "type": "leaf",
+ "state": {
+ "type": "bookmarks",
+ "state": {},
+ "icon": "lucide-bookmark",
+ "title": "Bookmarks"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "width": 242.5
+ },
+ "right": {
+ "id": "291dec6b9bbe827e",
+ "type": "split",
+ "children": [
+ {
+ "id": "c54902261a13dcf7",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "650593ff38043448",
+ "type": "leaf",
+ "state": {
+ "type": "backlink",
+ "state": {
+ "file": "docs/ui/campaigns.md",
+ "collapseAll": false,
+ "extraContext": false,
+ "sortOrder": "alphabetical",
+ "showSearch": false,
+ "searchQuery": "",
+ "backlinkCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-coming-in",
+ "title": "Backlinks for campaigns"
+ }
+ },
+ {
+ "id": "2d9a526162c43ded",
+ "type": "leaf",
+ "state": {
+ "type": "outgoing-link",
+ "state": {
+ "file": "docs/ui/campaigns.md",
+ "linksCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-going-out",
+ "title": "Outgoing links from campaigns"
+ }
+ },
+ {
+ "id": "5092e30ea33d8ccd",
+ "type": "leaf",
+ "state": {
+ "type": "tag",
+ "state": {
+ "sortOrder": "frequency",
+ "useHierarchy": true,
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-tags",
+ "title": "Tags"
+ }
+ },
+ {
+ "id": "35aaf7d0cc18ec7f",
+ "type": "leaf",
+ "state": {
+ "type": "outline",
+ "state": {
+ "file": "docs/ui/campaigns.md",
+ "followCursor": false,
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-list",
+ "title": "Outline of campaigns"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "width": 300,
+ "collapsed": true
+ },
+ "left-ribbon": {
+ "hiddenItems": {
+ "switcher:Open quick switcher": false,
+ "graph:Open graph view": false,
+ "canvas:Create new canvas": false,
+ "daily-notes:Open today's daily note": false,
+ "templates:Insert template": false,
+ "command-palette:Open command palette": false,
+ "bases:Create new base": false
+ }
+ },
+ "active": "5c458420e26c3b03",
+ "lastOpenFiles": [
+ "node_modules/@testing-library",
+ "node_modules/@playwright",
+ "node_modules/@eslint",
+ "node_modules/@tailwindcss",
+ "node_modules/@kayron013",
+ "components/campaigns/content/index.ts",
+ "components/campaigns/analytics/index.ts",
+ "components/campaigns/members/index.ts",
+ "app/api/[orgId]/campaigns/[id]/tasks/[taskId]/route.ts",
+ "app/api/[orgId]/campaigns/[id]/tasks/[taskId]",
+ "app/api/[orgId]/campaigns/[id]/contents/route.ts",
+ "docs/tasks/PR-002-campaigns-content-crud.md",
+ "docs/tasks/PR-001-auth-prisma.md",
+ "docs/ui/campaigns.md",
+ "docs/ui/README.md",
+ "docs/ui/dashboards.md",
+ "docs/ui/content-editor.md",
+ "docs/ui/auth.md",
+ "docs/ui/dashboard-layout.md",
+ "docs/ui/content-layout.md",
+ "docs/ui/campaigns-layout.md",
+ "docs/ui/schedule.md",
+ "docs/adr/0002-schedule-system-architecture.md",
+ "docs/design/schedule/README.md",
+ "docs/tasks/PR-012-documentation-deployment.md",
+ "docs/tasks/PR-011-testing-quality.md",
+ "docs/tasks/PR-010-performance-optimization.md",
+ "docs/tasks/PR-009-settings-management.md",
+ "docs/tasks/PR-008-analytics-reporting.md",
+ "docs/tasks/PR-007-scheduling-calendar.md",
+ "docs/tasks/README.md",
+ "docs/tasks/PR-006-ai-integration.md",
+ "docs/tasks/PR-005-dashboards.md",
+ "docs/tasks/PR-004-assets-upload.md",
+ "docs/tasks/PR-003-rbac-navigation.md",
+ "docs/playbooks/rollback.md"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..1b525590
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome against localhost",
+ "url": "http://localhost:3000",
+ "webRoot": "${workspaceFolder}"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..eb66b02f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "chatgpt.openOnStartup": true
+}
diff --git a/AI_INTEGRATION_README.md b/AI_INTEGRATION_README.md
new file mode 100644
index 00000000..3026adfc
--- /dev/null
+++ b/AI_INTEGRATION_README.md
@@ -0,0 +1,246 @@
+# AI Services Integration for AiM
+
+This document describes the AI services integration implemented in the AiM (AI Marketing) platform.
+
+## Overview
+
+The AI integration provides the following services:
+
+- **Content Generation**: Generate new content based on prompts
+- **Content Summarization**: Summarize long-form content
+- **Content Translation**: Translate content to different languages
+- **Idea Generation**: Generate creative ideas for content topics
+
+## Setup
+
+### Environment Variables
+
+Add the following to your `.env` file:
+
+```env
+# OpenAI
+OPENAI_API_KEY="your-openai-api-key-here"
+```
+
+### Dependencies
+
+The following packages are required:
+
+- `openai`: OpenAI SDK for API integration
+
+Install with:
+
+```bash
+pnpm add openai
+```
+
+## API Endpoints
+
+### Content Generation
+
+**POST** `/api/[orgId]/content/generate`
+
+Generates new content using AI based on a prompt.
+
+**Request Body:**
+
+```json
+{
+ "prompt": "Write a social media post about summer fashion",
+ "campaignId": "campaign-uuid"
+}
+```
+
+**Response:**
+
+```json
+{
+ "id": "content-uuid",
+ "title": "AI Generated: Write a social media post about summer fashion...",
+ "body": "Generated content here...",
+ "campaignId": "campaign-uuid",
+ "createdAt": "2024-01-01T00:00:00.000Z"
+}
+```
+
+### Content Summarization
+
+**POST** `/api/[orgId]/content/summarize`
+
+Summarizes provided content.
+
+**Request Body:**
+
+```json
+{
+ "content": "Long text to summarize...",
+ "length": "brief" | "detailed"
+}
+```
+
+**Response:**
+
+```json
+{
+ "summary": "Summarized content here..."
+}
+```
+
+### Content Translation
+
+**POST** `/api/[orgId]/content/translate`
+
+Translates content to a target language.
+
+**Request Body:**
+
+```json
+{
+ "content": "Text to translate",
+ "targetLanguage": "Spanish",
+ "sourceLanguage": "English" // optional
+}
+```
+
+**Response:**
+
+```json
+{
+ "translatedContent": "Texto traducido"
+}
+```
+
+### Idea Generation
+
+**POST** `/api/[orgId]/content/ideas`
+
+Generates creative ideas for content topics.
+
+**Request Body:**
+
+```json
+{
+ "topic": "social media marketing",
+ "count": 5,
+ "type": "general" | "titles" | "hashtags" | "campaigns"
+}
+```
+
+**Response:**
+
+```json
+{
+ "ideas": ["Idea 1", "Idea 2", "Idea 3"]
+}
+```
+
+## UI Integration
+
+### Creator Dashboard
+
+The AI features are integrated into the Creator Dashboard with three main sections:
+
+1. **AI Assistant Tab**: Contains forms for all AI services
+2. **Content Studio**: AI-powered content creation dialog
+3. **Idea Generation**: Generate content ideas based on topics
+
+### Features
+
+- **Real-time AI Generation**: Generate content instantly
+- **Campaign Integration**: Associate generated content with campaigns
+- **Multi-language Support**: Translate content to various languages
+- **Idea Brainstorming**: Generate multiple ideas for content topics
+- **Content Optimization**: Summarize and improve existing content
+
+## Usage Examples
+
+### Generating Content
+
+```typescript
+import { generateContent } from '@/lib/openai';
+
+const content = await generateContent({
+ prompt: 'Write a blog post about AI in marketing',
+ type: 'blog',
+ tone: 'professional',
+ length: 'medium',
+});
+```
+
+### Summarizing Content
+
+```typescript
+import { summarizeContent } from '@/lib/openai';
+
+const summary = await summarizeContent({
+ content: 'Long article text...',
+ length: 'brief',
+});
+```
+
+### Translating Content
+
+```typescript
+import { translateContent } from '@/lib/openai';
+
+const translation = await translateContent({
+ content: 'Hello world',
+ targetLanguage: 'Spanish',
+});
+```
+
+### Generating Ideas
+
+```typescript
+import { generateIdeas } from '@/lib/openai';
+
+const ideas = await generateIdeas({
+ topic: 'content marketing',
+ count: 5,
+ type: 'general',
+});
+```
+
+## Error Handling
+
+All AI functions include proper error handling:
+
+- API key validation
+- Network error handling
+- Rate limiting considerations
+- Fallback responses for failed requests
+
+## Security
+
+- API keys are stored as environment variables
+- All endpoints require authentication
+- RBAC permissions are enforced
+- Input validation using Zod schemas
+
+## Testing
+
+Run the AI integration tests:
+
+```typescript
+import { runAITests } from '@/lib/ai-test';
+
+// Run all tests
+const results = await runAITests();
+```
+
+## Future Enhancements
+
+- Support for additional AI models
+- Batch processing for multiple content items
+- Advanced content optimization features
+- Integration with other AI services
+- Custom AI model training
+
+## Support
+
+For issues or questions about the AI integration:
+
+1. Check the API key is valid and has sufficient credits
+2. Verify network connectivity
+3. Review error logs in the console
+4. Ensure proper permissions are set for the user
diff --git a/AiM_Migration_Plan_vi.md b/AiM_Migration_Plan_vi.md
new file mode 100644
index 00000000..a209ae15
--- /dev/null
+++ b/AiM_Migration_Plan_vi.md
@@ -0,0 +1,586 @@
+---
+title: 'Kế hoạch Phân tích & Migration Toàn Diện cho **AiM Platform**'
+tags: [AiM, Migration, Architecture, Next.js, Prisma, NextAuth, Postgres, RBAC, Obsidian]
+date: 2025-09-02
+---
+
+# Kế hoạch Phân tích & Migration Toàn Diện cho **AiM Platform**
+
+## 1) Tóm tắt điều hành (**Executive Summary**)
+
+Repository hiện tại (**ioqwfoihas**, tên **Circle**) là một ứng dụng **Next.js** lấy cảm hứng từ công cụ quản lý dự án **Linear**. Ứng dụng theo dõi _issues, projects, teams_ với UI hiện đại, _responsive_. `package.json` cho thấy đây là **client-only stack** với các dependencies như **Next.js 15**, **React 19**, **Tailwind CSS**, **Radix UI**, **Zustand**, **Recharts** và **Zod**. Chưa có **database** hay **API layer**; dữ liệu đang nằm ở **local state**.
+
+**AiM** hướng tới chuyển hoá codebase này thành một **AI‑powered content platform** phục vụ **Creators** và **Brands**. Hệ thống tương lai cần hỗ trợ **role‑based dashboards**, **campaign planning**, **AI‑assisted content creation**, **scheduling**, **asset management**, **analytics** và **administrative controls**. Báo cáo này phân tích repo, _map_ năng lực hiện tại sang yêu cầu **AiM**, xác định **gaps/risks**, đề xuất **target architecture** và **data model**, đồng thời cung cấp **migration roadmap** với **150+** nhiệm vụ khả dụng ngay.
+
+---
+
+## 2) Kiểm kê Repo & Tech Stack (**Repo Inventory & Tech Stack**)
+
+### Cây thư mục (ước lượng, depth ≤ 3)
+
+> Ghi chú: do không có listing đầy đủ, một số đường dẫn được suy luận theo pattern **Linear/Next.js** phổ biến.
+
+```text
+ioqwfoihas/
+├─ README.md
+├─ package.json
+├─ tsconfig.json
+├─ next.config.ts
+├─ app/
+│ ├─ page.tsx
+│ ├─ layout.tsx
+│ ├─ lndev-ui/
+│ │ ├─ team/[team]/[view]/
+│ │ │ ├─ page.tsx
+│ │ │ └─ layout.tsx
+│ │ ├─ settings/…
+│ │ └─ …
+│ └─ (auth)/
+├─ components/
+│ ├─ ui/
+│ ├─ board/
+│ ├─ modal/
+│ ├─ chart/
+│ └─ navigation/
+├─ lib/
+│ ├─ store/
+│ ├─ hooks/
+│ └─ utils.ts
+├─ public/
+├─ styles/
+├─ .husky/
+└─ .eslintrc, .prettierrc
+```
+
+### Công nghệ & phiên bản (**Tech & Versions**)
+
+| Khu vực | Chi tiết |
+| ---------------------- | ---------------------------------------------------- |
+| **Framework** | **Next.js 15** (App Router, React Server Components) |
+| **Ngôn ngữ** | **TypeScript** (strict) |
+| **UI Library** | **shadcn/ui** (trên nền **Radix UI**) |
+| **Styling** | **Tailwind CSS v4** + `tailwindcss-animate` |
+| **State Mgmt** | **Zustand** cho client state |
+| **Forms & Validation** | **React Hook Form** + **Zod** |
+| **Charts** | **Recharts** |
+| **Utilities** | `clsx`, `date-fns`, `uuid`, … |
+| **Testing** | Chưa có **Jest/Vitest/Cypress** |
+| **CI & Scripts** | **Husky**, **lint-staged**, **ESLint**, **Prettier** |
+| **Database/ORM** | Chưa có (no **Prisma/ORM**) |
+
+---
+
+## 3) Đánh giá kiến trúc (**Architecture Assessment**)
+
+- **Single-app**: cấu trúc **Next.js** thuần, không **monorepo/workspaces**.
+- **Client‑only**: không có API routes/server logic; dữ liệu nằm ở **Zustand** → _không phù hợp cho AiM_ (cần persistence).
+- **UI/UX**: responsive, dark/light, sidebars, modals; dùng **shadcn/ui** + **Radix**; có command palette (**cmdk**) và **Recharts** có thể tái sử dụng.
+- **Routing**: **App Router**; root page redirect sang board; dynamic routes theo `team/view`.
+- **State**: **Zustand** cho local data; để lên **AiM** cần **React Query** (data fetching/cache) + backend.
+- **Auth**: Chưa có **authentication/roles**.
+- **Config/Env**: Tối thiểu; chưa có `.env.example` hay secret management.
+- **Quality/DX**: Có lint/format cơ bản; chưa có tests/CI đầy đủ.
+- **Security**: Không có ranh giới bảo mật (no auth, no server validation).
+
+---
+
+## 4) Mapping sang **AiM** (**Reuse / Extend / Replace / Remove**)
+
+| Module/Screen hiện có | Mapping sang AiM | Action | Lý do |
+| ------------------------------ | ----------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------- |
+| UI Shell (layout, navigation) | Base dashboard cho mọi role | **Reuse & Extend** | Giữ responsive & theme; mở rộng navigation: **Campaigns, Content Studio, Calendar, Assets, Analytics, Settings** |
+| Team/Project Boards | **Campaign Kanban** | **Extend** | Chuyển issue card → campaign card (**Draft/Active/Completed**), drag‑drop persist |
+| List/Issue Views | **Content lists** trong campaign | **Replace** | Thay issue list → content items; thêm trạng thái AI (**Draft/AI‑generated/Approved**) |
+| **Zustand Stores** (mock data) | Server data qua **React Query** | **Replace** | **Zustand** chỉ giữ UI state; data persist qua **Postgres/API** |
+| Modals & Forms | Wizards tạo **Campaign/Content** | **Reuse & Extend** | Reuse modal patterns + multi‑step, **zod** validation, AI suggestions |
+| Charts (**Recharts**) | **Campaign/Content analytics** | **Reuse & Extend** | Nối analytics API, hiển thị metrics (impressions/CTR/ROI) |
+| Command palette | **AI assistant** & global search | **Extend** | Palette nhận prompt AI, thực thi lệnh (“Generate LinkedIn post for X”) |
+| Auth/RBAC (missing) | **Auth** & role‑based access | **Add** | Thêm **NextAuth** + **Prisma**, roles: **Creator/Brand/Admin** (+**Reviewer/Publisher** nếu cần) |
+| API & ORM (missing) | API endpoints/**tRPC** + **Prisma** | **Add** | CRUD cho campaigns/contents/schedules/assets/analytics |
+| Tests/CI (missing) | Testing & CI pipelines | **Add** | Unit/integration/e2e; **GitHub Actions**: lint, test, build, health check |
+| Assets (missing) | Asset storage | **Add** | Upload → **S3/UploadThing**; Asset Library UI/API |
+| Analytics (missing) | Event tracking & dashboards | **Add** | Thu thập events (view/click/conv), tổng hợp hiển thị |
+| AI (missing) | AI content generation | **Add** | Server route gọi AI provider; UI để generate/refine/translate |
+| i18n (missing) | Đa ngôn ngữ | **Extend** | Thêm **next-intl** (English/Vietnamese) |
+| Settings (missing) | Settings (profile/org/billing/AI) | **Add** | Trang cấu hình user/org, API keys, preferences, … |
+
+---
+
+## 5) Gaps, Risks & Mitigations
+
+### Gaps
+
+- **Persistence**: chưa có DB → **Prisma + Postgres** (schema cho **Users/Orgs/Campaign/Content/Asset/Schedule/Analytics**).
+- **Auth & RBAC**: thiếu → **NextAuth** + role enum/middleware.
+- **API layer**: thiếu CRUD → API routes hoặc **tRPC** + **zod** validation.
+- **Testing/CI**: thiếu → thêm **Jest/Vitest**, **Playwright**, **GitHub Actions**.
+- **Security**: chưa có validation, rate limit, secrets → thêm server validation, rate limiting, secret mgmt.
+- **Observability**: thiếu logs/monitoring → **pino/winston**, **Sentry**.
+- **i18n**: English‑only → **next-intl**.
+- **Scalability**: dễ thành monolith → module boundaries, cân nhắc packages sau.
+
+### Risks & Mitigations
+
+| Risk | Tác động | Xác suất | Giảm thiểu |
+| --------------------------------------- | ---------------- | -------- | ------------------------------------------------------- |
+| Migration phức tạp | Dễ phát sinh bug | Cao | Chia nhỏ PR, giữ trạng thái chạy được, test tự động |
+| Dependencies chưa ổn định (**Next 15**) | Compatibility | TB | Pin version, thử nghiệm trước khi merge |
+| Security (auth/upload) | Lỗ hổng | TB | Lib an toàn, OWASP, rate limit, file‑type validation |
+| Hiệu năng (board nhiều items) | Lag | TB | Pagination/virtualization, lazy load, React Query cache |
+| Adoption/training | Chậm triển khai | TB | Tài liệu, code samples, workshop |
+| Compliance (GDPR, …) | Rủi ro pháp lý | Thấp | Privacy by design, retention policy, consent |
+
+---
+
+## 6) Kiến trúc mục tiêu (**Target Architecture for AiM**)
+
+```mermaid
+flowchart TD
+ subgraph Client
+ U[Creators/Brands/Admins] -->|Browser| FE[Next.js 15 App]
+ FE -->|Fetch| API
+ FE -->|Auth Hooks| AuthClient
+ end
+ subgraph Server
+ API --> DB[(PostgreSQL via Prisma)]
+ API --> AIService[(External AI API)]
+ API --> Storage[(Asset Storage: S3/UploadThing)]
+ API --> Analytics[(Analytics Service)]
+ API --> RBAC[(RBAC Middleware)]
+ AuthService[(NextAuth.js)] --> DB
+ RBAC --> AuthService
+ end
+ Analytics --> DB
+ FE -.->|WebSockets| Notifications[(Real-time Notifications)]
+```
+
+**Mô tả ngắn**
+
+- **Frontend (Next.js)**: server/client components; **Zustand** cho UI state; **React Query** để fetch/cache.
+- **API layer (Next API routes/tRPC)**: endpoints cho campaign/content/asset/schedule/analytics/ai; **zod** + RBAC middleware.
+- **Auth**: **NextAuth.js**, session lưu DB.
+- **DB**: **PostgreSQL** qua **Prisma** (migrations).
+- **AI Service**: proxy tới provider (**OpenAI/…**).
+- **Asset Storage**: **S3/UploadThing**, metadata trong DB.
+- **Analytics**: thu thập & tổng hợp events.
+- **Real‑time**: **WebSockets/Pusher** (tuỳ chọn).
+
+### Data model đề xuất (**Proposed Data Model**)
+
+| Entity | Fields chính | Quan hệ |
+| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| **User** | `id, name, email (unique), passwordHash, role (creator/brand/admin/reviewer/publisher), organizationId, timestamps` | Thuộc **Organization**; tạo **Campaign/Content/Schedule/Asset/AnalyticsEvent** |
+| **Organization** | `id, name, slug, timestamps` | Có nhiều **Users/Campaigns/Assets** |
+| **Campaign** | `id, organizationId, createdById, name, description, status (draft/active/completed), budget, startDate, endDate, timestamps` | Thuộc **Organization/User**; có **Contents/Schedules/AnalyticsEvents** |
+| **Content** | `id, campaignId, authorId, assetId?, title, body, type, aiGenerated, status, metadata (JSON), timestamps` | Thuộc **Campaign**; tham chiếu **Asset**; có **Schedules/AnalyticsEvents** |
+| **Asset** | `id, organizationId, uploadedById, fileName, fileType, size, url, timestamps` | Thuộc **Organization**; liên kết nhiều **Contents** |
+| **Schedule** | `id, contentId, campaignId, scheduledAt, platform, status, createdById` | Lịch đăng cho **Content** trong **Campaign** |
+| **AnalyticsEvent** | `id, contentId, userId?, eventType, timestamp, metadata` | Log tương tác, dùng để tổng hợp |
+| **Role** _(tuỳ chọn)_ | `id, name, permissions (JSON)` | Many‑to‑many với **User** nếu cần RBAC chi tiết |
+
+### UI shell & điều hướng theo role (**Role‑Based Navigation**)
+
+- **Creator**: Dashboard nhanh vào _Campaigns, Content Studio, Calendar, Assets, Analytics_; có AI assistant; hiển thị deadlines/assignments.
+- **Brand**: Tổng quan campaigns, approval queue, budget/ROI; duyệt nội dung, review assets, giám sát lịch.
+- **Admin**: Quản trị organizations/users/roles/settings; cấu hình billing/AI quotas.
+
+---
+
+## 7) Kế hoạch Delta theo file (**Delta Plan – File‑level**)
+
+> Bảng **ADD/MODIFY/REMOVE** chi tiết — xem đầy đủ trong bản gốc; giữ nguyên danh sách đường dẫn và mô tả như đã đề xuất (**Prisma schema**, **API routes**, **pages**, **components**, **workflows CI**, **Docker**, **scripts**…).
+
+**ADD**:
+`prisma/schema.prisma`, `prisma/migrations/*`, `lib/prisma.ts`, `app/api/auth/[...nextauth]/route.ts`, `app/api/auth/register/route.ts`,
+`app/api/campaigns/*`, `app/api/contents/*`, `app/api/assets/*`, `app/api/schedules/*`, `app/api/analytics/*`,
+`app/api/ai/generate/route.ts`, `app/middleware.ts`, `lib/rbac.ts`, `lib/hooks/useCurrentUser.ts`,
+`(auth)/login`, `(auth)/register`, `(dashboard)/creator`, `(dashboard)/brand`, `(dashboard)/admin`,
+`campaigns/*`, `content/*`, `calendar/page.tsx`, `assets/page.tsx`, `settings/page.tsx`,
+các components `forms/ai/navigation/dashboard…`, `.env.example`, `.github/workflows/*.yml`, `Dockerfile`, `docker-compose.yml`, `scripts/publish-worker.ts`…
+
+**MODIFY**:
+`package.json`, `tsconfig.json`, `next.config.ts`, `app/layout.tsx`, `app/page.tsx`, `components/board/*`, `components/navigation/*`, `styles/globals.css`…
+
+**REMOVE**:
+mock data trong `lib/store`, routes cũ dưới `lndev-ui` không dùng, assets placeholder.
+
+---
+
+## 8) Lộ trình 30/60/90 ngày (**Roadmap**)
+
+- **0–30 ngày – Nền tảng**: **Prisma/Postgres**, **Auth + RBAC**, **API** cơ bản, **Dashboard shell**, CI tối thiểu, Docs.
+- **31–60 ngày – Tính năng chính**: **Campaign Kanban + Wizard**, **Content Studio + AI**, **Calendar & Scheduling**, **Asset Library**, **Analytics** cơ bản, **i18n**, **Testing**.
+- **61–90 ngày – Nâng cao & polishing**: RBAC chi tiết, **Realtime notifications**, **Observability**, **Performance**, **Collaboration**, **Deployment**, **Extensibility**.
+
+> Chi tiết từng hạng mục đã được giữ nguyên và diễn giải tiếng Việt ở phần **Backlog**.
+
+---
+
+## 9) Backlog – To‑Do chi tiết (≥ 150 tasks)
+
+Giữ nguyên cấu trúc **Epic → Story → Task** như bản gốc. Nội dung dưới đây được dịch phần diễn giải, giữ thuật ngữ gốc trong ngoặc cho dễ đọc.
+Mỗi task có: **Acceptance Criteria**, **Files touched**, **Effort (S/M/L)**, **Priority (P0–P3)**.
+
+### Epic: **Authentication & RBAC**
+
+**Story: Cài đặt & cấu hình NextAuth.js**
+
+- Cài dependencies Auth… (như bản gốc; acceptance/files/effort/priority giữ nguyên). _Gợi ý_: dùng **NextAuth** (credentials provider), **bcrypt**, **JWT**.
+- Tạo route `app/api/auth/[...nextauth]/route.ts` (credentials) với callbacks (session/JWT)…
+- Hash password (`lib/auth.ts`) bằng **bcrypt**…
+- Endpoint **register** tạo **User + Organization**…
+- Bọc ``…
+- Trang **login** (zod validation, `signIn()`)…
+- Trang **register** (tạo org, role mặc định)…
+- **Logout** (`signOut()`)…
+- `/api/auth/session` trả user hiện tại…
+
+**Story: Quản lý vai trò (**Role Management & RBAC**)**
+
+- `lib/roles.ts` khai báo enum roles…
+- Thêm field **role** vào **User** (**Prisma enum**)…
+- `lib/rbac.ts` helpers `hasRole/hasPermission` + tests…
+- `middleware.ts` bảo vệ route theo session/role…
+- Gán role khi register (first user = **admin**)…
+- Role switcher (dev only)…
+- **Role‑based navigation** (Sidebar)…
+- **Role‑based API checks**…
+
+### Epic: **Database & Prisma**
+
+**Story: Định nghĩa schema**
+
+- `npx prisma init`…
+- Models **User/Organization**…
+- **Campaign**…
+- **Content**…
+- **Asset**…
+- **Schedule**…
+- **AnalyticsEvent**…
+- _(Tuỳ chọn)_ **Role/UserRole**…
+
+**Story: Migrations & Seeding**
+
+- `prisma migrate dev --name init`…
+- `scripts/seed.ts` (roles, admin, org mẫu, campaigns, content)…
+- `pnpm seed`…
+- Document DB setup trong **README**…
+
+### Epic: **API Layer**
+
+**Story: Campaign Endpoints**
+
+- `GET /campaigns` (pagination/search, RBAC)…
+- `POST /campaigns` (zod, `createdBy`, status `draft`)…
+- `GET /campaigns/[id]` (details + relations)…
+- `PUT /campaigns/[id]`…
+- `DELETE /campaigns/[id]` (cascade theo rule)…
+- `GET /campaigns/[id]/analytics` (summary)…
+- `POST /campaigns/[id]/duplicate`…
+
+**Story: Content Endpoints**
+
+- `GET /contents` (filter status/type)…
+- `POST /contents` (create)…
+- `GET /contents/[id]` (asset/author/schedules)…
+- `PUT /contents/[id]` (restrictions by role/status)…
+- `DELETE /contents/[id]`…
+- `POST /contents/[id]/submit` (submit for approval)…
+- `POST /contents/[id]/approve|reject` (**Brand** role)…
+- `POST /ai/generate` (**OpenAI**; cache; audit)…
+- `POST /ai/translate`…
+- `POST /ai/summarise`…
+
+**Story: Asset Endpoints**
+
+- `POST /assets` (**UploadThing/S3**; validate type/size; metadata)…
+- `GET /assets` (org scope; pagination/search)…
+- `DELETE /assets/[id]` (prevent delete if in‑use)…
+- `GET /assets/upload-url` (pre‑signed **S3**)…
+
+**Story: Schedule Endpoints**
+
+- `GET /schedules` (filters)…
+- `POST /schedules` (validate future; content must be approved)…
+- `PUT /schedules/[id]`…
+- `POST /schedules/[id]/cancel`…
+- `scripts/publish-worker.ts` (cron publish + analytics + notifications)…
+
+**Story: Analytics Endpoints**
+
+- `POST /analytics` (record event)…
+- `GET /analytics` (filters)…
+- `GET /analytics/summary` (aggregate)…
+- `GET /analytics/export` (CSV/Excel; admin/brand only)…
+
+### Epic: **Frontend – Global Layout & Navigation**
+
+**Story: Providers & Setup**
+
+- **React Query** provider (`QueryClientProvider`, `Hydrate`)…
+- **next-intl** (en/vi) + ``…
+- **Theme provider** (dark/light)…
+
+**Story: Sidebar & Topbar**
+
+- Sidebar (role‑specific menus)…
+- Topbar (search, notifications, user menu)…
+- **Command palette** (global search + AI commands)…
+- **Notifications panel** (**Radix UI** + realtime)…
+
+### Epic: **Dashboards**
+
+**Story: Creator Dashboard**
+
+- Summary cards (active campaigns, drafts, scheduled, impressions)…
+- Campaign list…
+- Quick actions…
+- Draft reminders…
+- AI suggestions widget…
+
+**Story: Brand Dashboard**
+
+- Brand metrics cards (budget/ROI)…
+- Approval queue…
+- Budget vs ROI chart…
+- Creator leaderboard…
+- Campaign health summary…
+
+**Story: Admin Dashboard**
+
+- `UserTable` (enable/disable)…
+- `OrgTable` (rename/delete cascade)…
+- Role manager (nếu dùng Role model)…
+- Audit logs…
+- Feature flags…
+
+### Epic: **Campaign Module**
+
+**Story: Campaign List & Board**
+
+- Campaign table (sort/filter/search)…
+- Pagination/infinite scroll…
+- Board view (**Draft/Active/Completed**; drag‑drop persist)…
+- Campaign detail (**Overview/Content/Schedule/Analytics/Settings**)…
+- Create wizard (multi‑step)…
+- Duplicate…
+- Delete confirm…
+- Search debounce…
+- Permissions UI…
+
+### Epic: **Content Module**
+
+**Story: Content Editor**
+
+- Rich text editor (**TipTap**)…
+- **AI assistant** panel (prompt, rewrite, tone)…
+- Auto‑save drafts…
+- Asset picker…
+- Preview…
+- **Translate & summarise**…
+- Status management (**Draft/Submitted/Approved/Published/Rejected**)…
+- Comments & versioning…
+
+**Story: Content List & Cards**
+
+- Content list (trong **Campaign**)…
+- Content card (badge, quick actions)…
+- Filtering & search…
+- Bulk actions…
+
+### Epic: **Calendar & Scheduling**
+
+**Story: Calendar View**
+
+- Calendar page (**react-day-picker**) — color‑code theo platform…
+- Schedule form modal (platform/date/time/timezone)…
+- Timezone support (user preference, store **UTC**)…
+- Recurring schedules (**RRULE** model)…
+
+### Epic: **Assets Module**
+
+**Story: Asset Library UI**
+
+- Assets page (search/filter/thumbnail)…
+- Upload dialog (multi files, progress)…
+- Asset detail drawer (preview, metadata, usage)…
+- Asset usage indicator…
+
+### Epic: **Analytics Module**
+
+**Story: Analytics Dashboards**
+
+- Overview chart (7/30/90)…
+- Campaign analytics tab (line/bar/pie)…
+- Content analytics tab…
+- Creator performance…
+- Export analytics UI…
+
+### Epic: **Settings Module**
+
+**Story: User & Organization Settings**
+
+- Profile settings (name/email/password; AI usage)…
+- Organization settings (slug/logo/default language)…
+- Notification preferences…
+- Billing & quotas (**Stripe**)…
+- API keys management…
+
+### Epic: **Internationalisation**
+
+**Story: i18n**
+
+- Cài **next-intl** (en/vi)…
+- Tạo translation files…
+- Refactor components dùng `t('key')`…
+- Language switcher…
+
+### Epic: **Testing & Quality**
+
+**Story: Unit & Integration Tests**
+
+- **Jest/Vitest** setup…
+- Tests cho utilities (auth, rbac, utils)…
+- Tests forms & components…
+- Tests API routes…
+
+**Story: End‑to‑End Tests**
+
+- **Playwright** setup…
+- e2e login…
+- e2e create campaign…
+- e2e content workflow…
+- e2e schedule & publish…
+
+### Epic: **CI/CD & Observability**
+
+**Story: Continuous Integration**
+
+- `ci.yml` (lint/build/test)…
+- `e2e.yml` (**Playwright** artifacts)…
+- Health check (`/api/health`)…
+
+**Story: Delivery & Deployment**
+
+- `Dockerfile` (**Node 20**; `prisma migrate deploy`)…
+- `docker-compose.yml` (app + postgres + mailhog)…
+- `deploy.yml` (push image, deploy staging/prod)…
+
+**Story: Observability & Monitoring**
+
+- Logging (**pino/winston**)…
+- Error tracking (**Sentry**)…
+- `/api/health`…
+- `/api/metrics` (**Prometheus prom-client**)…
+
+### Epic: **AI Integration**
+
+**Story: AI Services**
+
+- Chọn AI provider (đánh giá cost/perf/language)…
+- AI proxy endpoint (`/api/ai/generate`) — prompt engineering, rate limit, cache…
+- Prompt library (`lib/prompts.ts`)…
+- AI usage tracking (model **AIUsage** hoặc gắn vào **Analytics**)…
+- AI tone tuning (formal/friendly/professional)…
+- Content idea generator (`/api/ai/ideas`)…
+
+### Epic: **Performance & Scalability**
+
+**Story: Optimisations**
+
+- Bundle analysis (`next build --profile`, dynamic imports)…
+- Image optimization (` `, lazy)…
+- Virtualization (lists > 100 rows)…
+- Caching & SWR (**React Query** strategies)…
+
+> ⚠️ **Tổng số mục checklist vượt 150** (đủ chuẩn để tạo **GitHub Issues** ngay).
+
+---
+
+## 10) CI/CD & Quality Gates
+
+- **Static Analysis**: **ESLint/Prettier**, **Husky** pre‑commit.
+- **Type Checking**: `tsc --noEmit` trong CI.
+- **Unit & Integration Tests**: ≥ 80% cho core logic.
+- **E2E Tests**: **Playwright** trên PR.
+- **Build & Health Check**: ping `/api/health`.
+- **Code Review**: branch protection + **Conventional Commits**.
+- **Secrets Management**: **GitHub Secrets**, `.env.example`.
+- **CD Pipeline**: deploy **staging** tự động; smoke test; promote to **prod**.
+
+---
+
+## 11) `.env` & xử lý secrets (**Secrets Handling**)
+
+Tạo **.env.example** với các biến:
+
+```env
+DATABASE_URL="postgresql://aim_user:password@localhost:5432/aim_db"
+NEXTAUTH_SECRET="set-a-random-secret-here"
+NEXTAUTH_URL="http://localhost:3000"
+S3_BUCKET="aim-assets"
+S3_REGION="ap-southeast-1"
+S3_ACCESS_KEY_ID=""
+S3_SECRET_ACCESS_KEY=""
+AI_API_KEY=""
+PUBLIC_SITE_URL="http://localhost:3000"
+DEFAULT_LANGUAGE="en"
+
+# Optional
+SMTP_HOST=""
+SMTP_PORT=""
+SMTP_USER=""
+SMTP_PASS=""
+```
+
+- Dev sao chép `.env.example` → `.env` (được `.gitignore`).
+- Prod dùng **platform secrets** (**Vercel/AWS/GitHub Actions**).
+- Tránh log secrets; **rotate** định kỳ; **least privilege**.
+
+---
+
+## 12) Câu hỏi mở (**Open Questions**)
+
+- **Stack Upgrade Tolerance**: Giữ **Next.js 15** hay cân nhắc **Next.js 14 LTS** để ổn định?
+- **Roles bổ sung**: Có cần **Reviewer/Publisher** không?
+- **Compliance**: Có yêu cầu **GDPR/ISO** cụ thể? (ảnh hưởng đến retention, consent, audit).
+
+---
+
+## 13) Phụ lục (**Appendix**)
+
+### Scripts/Commands đề xuất
+
+```bash
+pnpm prisma generate
+pnpm prisma migrate dev --name
+pnpm seed
+pnpm dev
+pnpm build
+pnpm test
+pnpm e2e
+node scripts/publish-worker.js
+```
+
+### Quy ước thư mục (**Folder Conventions**)
+
+```
+app/ # App Router; groups theo role
+components/ # UI tái sử dụng theo domain
+lib/ # Prisma/RBAC/logger/metrics/prompts/hooks
+prisma/ # schema + migrations
+public/ # assets
+scripts/ # seed/cron/test data
+tests/ # unit/integration/e2e
+```
+
+### Coding Guidelines
+
+- **TypeScript** nghiêm ngặt, tránh `any`.
+- **Conventional Commits**.
+- **ESLint + Prettier** (Next preset).
+- Ưu tiên **Server Components**; **Client Components** khi cần interactivity.
+- **React Query** cho data fetching/cache.
+- **zod** validate ở boundary (API/forms).
+- **A11y** với **Radix/ARIA**.
+- Viết tests cho components/APIs (ưu tiên critical paths).
diff --git a/DEPLOYMENT_README.md b/DEPLOYMENT_README.md
new file mode 100644
index 00000000..21a09a46
--- /dev/null
+++ b/DEPLOYMENT_README.md
@@ -0,0 +1,57 @@
+# Deployment and Monitoring Guide
+
+## Containerization
+
+### Docker
+
+- Build: `docker build -t aim-platform .`
+- Run: `docker run -p 3000:3000 aim-platform`
+
+### Docker Compose (Development)
+
+- Start: `docker-compose up`
+- Stop: `docker-compose down`
+
+## Deployment
+
+### Vercel
+
+1. Connect repository to Vercel
+2. Set environment variables in Vercel dashboard:
+ - `DATABASE_URL`
+ - `NEXTAUTH_SECRET`
+ - `NEXTAUTH_URL`
+ - `OPENAI_API_KEY`
+3. Deploy
+
+### AWS Amplify
+
+1. Connect repository to Amplify
+2. Configure build settings (amplify.yml provided)
+3. Set environment variables
+4. Deploy
+
+## Environment Variables
+
+### Production (.env.production)
+
+- Update with production values
+- Use secure secrets for sensitive data
+
+## Monitoring
+
+### Health Check
+
+- Endpoint: `/api/health`
+- Returns status of services (database, OpenAI)
+- Use for load balancer health checks
+
+### Logs
+
+- Application logs available in deployment platform
+- Database logs via PostgreSQL
+
+### Performance
+
+- Next.js analytics in production
+- Monitor API response times
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..4da00ea1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+# Use the official Node.js 20 image as the base image
+FROM node:20-alpine
+
+# Set the working directory inside the container
+WORKDIR /app
+
+# Copy package.json and package-lock.json (or pnpm-lock.yaml)
+COPY package.json pnpm-lock.yaml ./
+
+# Install dependencies
+RUN npm install -g pnpm && pnpm install --frozen-lockfile
+
+# Copy the rest of the application code
+COPY . .
+
+# Generate Prisma client
+RUN pnpm run db:generate
+
+# Build the Next.js application
+RUN pnpm run build
+
+# Expose the port the app runs on
+EXPOSE 3000
+
+# Set environment variables
+ENV NODE_ENV=production
+
+# Start the application
+CMD ["pnpm", "start"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 5e731633..df216969 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,166 @@
-# Circle
+# AiM Platform
-
-
-
-
+**AI-powered Marketing Platform** - Hệ thống quản lý nội dung và chiến dịch marketing được hỗ trợ bởi AI.
-
-
+## 🚀 Features
-Project management interface inspired by Linear. Built with Next.js and shadcn/ui, this application allows tracking of issues, projects and teams with a modern, responsive UI.
+- **🎨 Content Creation**: AI-assisted content generation với approval workflow
+- **📅 Smart Scheduling**: Multi-view calendar (Day/Week/Month) với drag & drop
+- **📊 Campaign Management**: End-to-end campaign lifecycle management
+- **🔐 Role-based Access**: Creator, Brand Owner, Admin roles với permissions
+- **📱 Multi-platform**: Support Facebook, Instagram, Twitter, YouTube, LinkedIn, TikTok
+- **📈 Analytics**: Event tracking và performance monitoring
-## 🛠️ Technologies
+## 🛠️ Tech Stack
-- **Framework**: [Next.js](https://nextjs.org/)
-- **Langage**: [TypeScript](https://www.typescriptlang.org/)
-- **UI Components**: [shadcn/ui](https://ui.shadcn.com/)
-- **Styling**: [Tailwind CSS](https://tailwindcss.com/)
+- **Frontend**: Next.js 15 (App Router), React 19, TypeScript 5
+- **UI Components**: shadcn/ui, Radix UI, Tailwind CSS 4
+- **Backend**: Next.js API Routes, Prisma 6 ORM
+- **Database**: PostgreSQL
+- **Authentication**: NextAuth.js 5
+- **State Management**: Zustand, React Query
+- **AI Integration**: OpenAI API
+- **File Storage**: UploadThing/S3
-### 📦 Installation
+## 📚 Documentation
-```shell
-git clone https://github.com/ln-dev7/circle.git
-cd circle
-```
+### 🏗️ Architecture & Design
+
+- [**AiM Architecture**](./AiM_Architecture.md) - Overall system architecture và design decisions
+- [**Product Specification**](./docs/SPEC.md) - Product features, user roles, và MVP scope
+- [**Data Model**](./docs/data-model.md) - Database schema, entities, và relationships
+
+### 🔌 API Reference
+
+- [**Campaigns API**](./docs/api/campaigns.md) - Campaign management endpoints
+- [**Content API**](./docs/api/content.md) - Content creation và management
+- [**Schedules API**](./docs/api/schedules.md) - Content scheduling với timezone support
+- [**Assets API**](./docs/api/assets.md) - File upload và management
+- [**Analytics API**](./docs/api/analytics.md) - Event tracking và metrics
+
+### 🎨 User Interface
+
+- [**Schedule UI Guide**](./docs/ui/schedule.md) - Calendar interface, drag & drop, draft panel
+- [**Design System**](./docs/ui/design-system.md) - Component library và design tokens
+
+### 🔒 Security & Operations
+
+- [**Security Guide**](./docs/SECURITY.md) - Authentication, authorization, data protection
+- [**Contributing Guide**](./docs/CONTRIBUTING.md) - Development setup và contribution guidelines
+- [**Rollback Playbook**](./docs/playbooks/rollback.md) - Emergency procedures và rollback steps
+- [**Observability Guide**](./docs/playbooks/observability.md) - Monitoring, logging, alerting
+
+### 📋 Development Tasks
+
+- [**Task Templates**](./docs/tasks/) - PR templates và development guidelines
+- [**Migration Plan**](./AiM_Migration_Plan_vi.md) - Technical migration roadmap
-### Install dependencies
+## 🚀 Quick Start
-```shell
+### Prerequisites
+
+- Node.js 18+
+- PostgreSQL 15+
+- pnpm (recommended)
+
+### Installation
+
+```bash
+# Clone repository
+git clone
+cd aim-platform
+
+# Install dependencies
pnpm install
-```
-### Start the development server
+# Setup environment
+cp .env.example .env
+# Edit .env with your configuration
-```shell
+# Setup database
+pnpm db:generate
+pnpm db:push
+pnpm db:seed
+
+# Start development server
pnpm dev
```
-## Star History
+### Environment Variables
+
+```bash
+# Database
+DATABASE_URL="postgresql://user:password@localhost:5432/aim_db"
+
+# Authentication
+NEXTAUTH_SECRET="your-secret-key"
+NEXTAUTH_URL="http://localhost:3000"
+
+# AI Integration
+OPENAI_API_KEY="your-openai-key"
+
+# File Storage
+UPLOADTHING_SECRET="your-uploadthing-secret"
+UPLOADTHING_APP_ID="your-uploadthing-app-id"
+```
+
+## 🧪 Testing
+
+```bash
+# Unit tests
+pnpm test
+
+# E2E tests
+pnpm test:e2e
+
+# Test coverage
+pnpm test:coverage
+```
+
+## 📁 Project Structure
+
+```
+aim-platform/
+├── app/ # Next.js App Router
+│ ├── [orgId]/ # Organization-scoped routes
+│ │ ├── schedule/ # Schedule management
+│ │ ├── campaigns/ # Campaign management
+│ │ ├── content/ # Content management
+│ │ └── analytics/ # Analytics dashboard
+│ ├── api/ # API routes
+│ └── auth/ # Authentication pages
+├── components/ # Reusable UI components
+├── features/ # Feature-specific components
+│ └── calendar/ # Schedule calendar components
+├── lib/ # Utility functions và configurations
+├── prisma/ # Database schema và migrations
+├── docs/ # Documentation
+└── tests/ # Test files
+```
+
+## 🤝 Contributing
+
+Please read our [Contributing Guide](./docs/CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
+
+### Development Workflow
+
+1. Create feature branch từ `main`
+2. Implement changes với tests
+3. Update documentation nếu cần
+4. Submit PR với detailed description
+5. Code review và approval
+6. Merge vào `main`
+
+## 📄 License
+
+This project is licensed under the MIT License - see the [LICENSE](./LICENSE.md) file for details.
+
+## 🆘 Support
+
+- **Documentation**: Check the [docs/](./docs/) directory
+- **Issues**: Create GitHub issue với detailed description
+- **Discussions**: Use GitHub Discussions cho questions và ideas
+
+---
-
-
-
-
-
-
-
+_Built with ❤️ by the AiM Team_
diff --git a/__mocks__/next-intl.ts b/__mocks__/next-intl.ts
new file mode 100644
index 00000000..c09b69e1
--- /dev/null
+++ b/__mocks__/next-intl.ts
@@ -0,0 +1,26 @@
+export const useTranslations = () => (key: string) => {
+ const translations: Record = {
+ admin_title: 'Admin Dashboard',
+ admin_description: 'Manage your organization and users',
+ total_users: 'Total Users',
+ active_users: 'active',
+ active_campaigns: 'Active Campaigns',
+ total_campaigns: 'of',
+ total: 'total',
+ content_pieces: 'Content Pieces',
+ pending_approvals: 'Pending Approvals',
+ user_management: 'User Management',
+ organization: 'Organization',
+ system: 'System',
+ system_settings: 'System Settings',
+ };
+
+ return translations[key] || key;
+};
+
+export const useFormatter = () => ({
+ dateTime: (date: Date) => date.toISOString(),
+ number: (num: number) => num.toString(),
+});
+
+export const NextIntlClientProvider = ({ children }: { children: React.ReactNode }) => children;
diff --git a/amplify.yml b/amplify.yml
new file mode 100644
index 00000000..a5d35f30
--- /dev/null
+++ b/amplify.yml
@@ -0,0 +1,19 @@
+version: 1
+frontend:
+ phases:
+ preBuild:
+ commands:
+ - npm install -g pnpm
+ - pnpm install
+ - pnpm run db:generate
+ build:
+ commands:
+ - pnpm run build
+ artifacts:
+ baseDirectory: .next
+ files:
+ - '**/*'
+ cache:
+ paths:
+ - node_modules/**/*
+ - .next/cache/**/*
\ No newline at end of file
diff --git a/app/[orgId]/assets/page.tsx b/app/[orgId]/assets/page.tsx
new file mode 100644
index 00000000..f4d5bad0
--- /dev/null
+++ b/app/[orgId]/assets/page.tsx
@@ -0,0 +1,17 @@
+import { AssetLibrary } from '@/components/assets/asset-library';
+
+interface AssetsPageProps {
+ params: {
+ orgId: string;
+ };
+}
+
+export default function AssetsPage({ params }: AssetsPageProps) {
+ const { orgId } = params;
+
+ return (
+
+ );
+}
diff --git a/app/[orgId]/campaigns/[id]/analytics/page.tsx b/app/[orgId]/campaigns/[id]/analytics/page.tsx
new file mode 100644
index 00000000..1eeabe35
--- /dev/null
+++ b/app/[orgId]/campaigns/[id]/analytics/page.tsx
@@ -0,0 +1,31 @@
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { getCampaign } from '@/lib/campaigns';
+import { CampaignAnalyticsPage } from '@/components/campaigns/analytics/campaign-analytics-page';
+
+interface CampaignAnalyticsPageProps {
+ params: {
+ orgId: string;
+ id: string;
+ };
+}
+
+export default async function CampaignAnalyticsPage({ params }: CampaignAnalyticsPageProps) {
+ const campaign = await getCampaign(params.orgId, params.id);
+
+ if (!campaign) {
+ notFound();
+ }
+
+ return (
+
+ Loading analytics...
}>
+
+
+
+ );
+}
diff --git a/app/[orgId]/campaigns/[id]/content/page.tsx b/app/[orgId]/campaigns/[id]/content/page.tsx
new file mode 100644
index 00000000..bdbf96eb
--- /dev/null
+++ b/app/[orgId]/campaigns/[id]/content/page.tsx
@@ -0,0 +1,27 @@
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { getCampaign } from '@/lib/campaigns';
+import { CampaignContentPage } from '@/components/campaigns/content/campaign-content-page';
+
+interface CampaignContentPageProps {
+ params: {
+ orgId: string;
+ id: string;
+ };
+}
+
+export default async function CampaignContentPage({ params }: CampaignContentPageProps) {
+ const campaign = await getCampaign(params.orgId, params.id);
+
+ if (!campaign) {
+ notFound();
+ }
+
+ return (
+
+ Loading content management...
}>
+
+
+
+ );
+}
diff --git a/app/[orgId]/campaigns/[id]/members/page.tsx b/app/[orgId]/campaigns/[id]/members/page.tsx
new file mode 100644
index 00000000..f9006074
--- /dev/null
+++ b/app/[orgId]/campaigns/[id]/members/page.tsx
@@ -0,0 +1,27 @@
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { getCampaign } from '@/lib/campaigns';
+import { CampaignMembersPage } from '@/components/campaigns/members/campaign-members-page';
+
+interface CampaignMembersPageProps {
+ params: {
+ orgId: string;
+ id: string;
+ };
+}
+
+export default async function CampaignMembersPage({ params }: CampaignMembersPageProps) {
+ const campaign = await getCampaign(params.orgId, params.id);
+
+ if (!campaign) {
+ notFound();
+ }
+
+ return (
+
+ Loading member management...
}>
+
+
+
+ );
+}
diff --git a/app/[orgId]/campaigns/[id]/page.tsx b/app/[orgId]/campaigns/[id]/page.tsx
new file mode 100644
index 00000000..7ca23e58
--- /dev/null
+++ b/app/[orgId]/campaigns/[id]/page.tsx
@@ -0,0 +1,28 @@
+import { Suspense } from 'react';
+import { CampaignDetail } from '@/components/campaigns/campaign-detail';
+import { notFound } from 'next/navigation';
+import { getCampaign } from '@/lib/campaigns';
+import { CampaignDetailWrapper } from '@/components/campaigns/campaign-detail-wrapper';
+
+interface CampaignPageProps {
+ params: {
+ orgId: string;
+ id: string;
+ };
+}
+
+export default async function CampaignPage({ params }: CampaignPageProps) {
+ const campaign = await getCampaign(params.orgId, params.id);
+
+ if (!campaign) {
+ notFound();
+ }
+
+ return (
+
+ Loading campaign...
}>
+
+
+
+ );
+}
diff --git a/app/[orgId]/campaigns/[id]/tasks/page.tsx b/app/[orgId]/campaigns/[id]/tasks/page.tsx
new file mode 100644
index 00000000..1585e42f
--- /dev/null
+++ b/app/[orgId]/campaigns/[id]/tasks/page.tsx
@@ -0,0 +1,27 @@
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { getCampaign } from '@/lib/campaigns';
+import { TaskManagementPage } from '@/components/campaigns/task-management/task-management-page';
+
+interface CampaignTasksPageProps {
+ params: {
+ orgId: string;
+ id: string;
+ };
+}
+
+export default async function CampaignTasksPage({ params }: CampaignTasksPageProps) {
+ const campaign = await getCampaign(params.orgId, params.id);
+
+ if (!campaign) {
+ notFound();
+ }
+
+ return (
+
+ Loading task management...
}>
+
+
+
+ );
+}
diff --git a/app/[orgId]/campaigns/new/page.tsx b/app/[orgId]/campaigns/new/page.tsx
new file mode 100644
index 00000000..cefe084b
--- /dev/null
+++ b/app/[orgId]/campaigns/new/page.tsx
@@ -0,0 +1,14 @@
+import { CampaignForm } from '@/components/campaigns/campaign-form';
+import { PageHeader } from '@/components/ui/page-header';
+
+export default function CreateCampaignPage() {
+ return (
+
+ );
+}
diff --git a/app/[orgId]/campaigns/page.tsx b/app/[orgId]/campaigns/page.tsx
new file mode 100644
index 00000000..9fda5fa5
--- /dev/null
+++ b/app/[orgId]/campaigns/page.tsx
@@ -0,0 +1,11 @@
+import MainLayout from '@/components/layout/main-layout';
+import Header from '@/components/layout/headers/campaigns/header';
+import Campaigns from '@/components/common/campaigns/campaigns';
+
+export default function CampaignsPage() {
+ return (
+ }>
+
+
+ );
+}
diff --git a/app/[orgId]/content/[id]/page.tsx b/app/[orgId]/content/[id]/page.tsx
new file mode 100644
index 00000000..bcdc094c
--- /dev/null
+++ b/app/[orgId]/content/[id]/page.tsx
@@ -0,0 +1,27 @@
+import { Suspense } from 'react';
+import { ContentDetail } from '@/components/content/content-detail';
+import { notFound } from 'next/navigation';
+import { getContent } from '@/lib/content';
+
+interface ContentPageProps {
+ params: {
+ orgId: string;
+ id: string;
+ };
+}
+
+export default async function ContentPage({ params }: ContentPageProps) {
+ const content = await getContent(params.orgId, params.id);
+
+ if (!content) {
+ notFound();
+ }
+
+ return (
+
+ Loading content...
}>
+
+
+
+ );
+}
diff --git a/app/[orgId]/content/new/page.tsx b/app/[orgId]/content/new/page.tsx
new file mode 100644
index 00000000..0d4a36b2
--- /dev/null
+++ b/app/[orgId]/content/new/page.tsx
@@ -0,0 +1,22 @@
+import { ContentEditor } from '@/components/content/content-editor';
+import { PageHeader } from '@/components/ui/page-header';
+
+interface CreateContentPageProps {
+ params: {
+ orgId: string;
+ };
+}
+
+export default function CreateContentPage({ params }: CreateContentPageProps) {
+ const { orgId } = params;
+
+ return (
+
+ );
+}
diff --git a/app/[orgId]/content/page.tsx b/app/[orgId]/content/page.tsx
new file mode 100644
index 00000000..213d7d1a
--- /dev/null
+++ b/app/[orgId]/content/page.tsx
@@ -0,0 +1,28 @@
+import { Suspense } from 'react';
+import { ContentList } from '@/components/content/content-list';
+import { CreateContentButton } from '@/components/content/create-content-button';
+import { PageHeader } from '@/components/ui/page-header';
+
+interface ContentPageProps {
+ params: {
+ orgId: string;
+ };
+}
+
+export default function ContentPage({ params }: ContentPageProps) {
+ const { orgId } = params;
+
+ return (
+
+
}
+ />
+
+
Loading content... }>
+
+
+
+ );
+}
diff --git a/app/[orgId]/page.tsx b/app/[orgId]/page.tsx
index afd7d1bf..e6841772 100644
--- a/app/[orgId]/page.tsx
+++ b/app/[orgId]/page.tsx
@@ -1,5 +1,55 @@
+import { Suspense } from 'react';
+import { getUserRole } from '@/lib/rbac';
+import { CreatorDashboard } from '@/components/dashboards/creator-dashboard';
+import { BrandDashboard } from '@/components/dashboards/brand-dashboard';
+import { AdminDashboard } from '@/components/dashboards/admin-dashboard';
import { redirect } from 'next/navigation';
+import { OrgRole } from '@prisma/client';
-export default function OrgIdPage() {
- redirect('lndev-ui/team/CORE/all');
+interface OrgIdPageProps {
+ params: Promise<{
+ orgId: string;
+ }>;
+}
+
+async function DashboardContent({ orgId }: { orgId: string }) {
+ const userRole = await getUserRole(orgId);
+
+ if (!userRole) {
+ // User doesn't have access to this organization
+ redirect('/auth/signin');
+ }
+
+ switch (userRole) {
+ case OrgRole.CREATOR:
+ return ;
+ case OrgRole.BRAND_OWNER:
+ return ;
+ case OrgRole.ADMIN:
+ return ;
+ default:
+ redirect('/auth/signin');
+ }
+}
+
+export default async function OrgIdPage({ params }: OrgIdPageProps) {
+ const resolvedParams = await params;
+
+ return (
+
+
+
+
Loading dashboard...
+
+ Please wait while we set up your workspace
+
+
+
+ }
+ >
+
+
+ );
}
diff --git a/app/[orgId]/schedule/page.tsx b/app/[orgId]/schedule/page.tsx
new file mode 100644
index 00000000..d87d77fc
--- /dev/null
+++ b/app/[orgId]/schedule/page.tsx
@@ -0,0 +1,27 @@
+import { Suspense } from 'react';
+import { ScheduleShell } from '@/features/calendar/ui/ScheduleShell';
+
+interface SchedulePageProps {
+ params: {
+ orgId: string;
+ };
+}
+
+export default function SchedulePage({ params }: SchedulePageProps) {
+ const { orgId } = params;
+
+ return (
+
+
+
Content Scheduling
+
+ Schedule your content for publication and manage your posting calendar.
+
+
+
+
Loading calendar... }>
+
+
+
+ );
+}
diff --git a/app/[orgId]/schedules/page.tsx b/app/[orgId]/schedules/page.tsx
new file mode 100644
index 00000000..2972be9b
--- /dev/null
+++ b/app/[orgId]/schedules/page.tsx
@@ -0,0 +1,27 @@
+import { Suspense } from 'react';
+import { ScheduleCalendar } from '@/components/schedules/schedule-calendar';
+
+interface SchedulesPageProps {
+ params: {
+ orgId: string;
+ };
+}
+
+export default function SchedulesPage({ params }: SchedulesPageProps) {
+ const { orgId } = params;
+
+ return (
+
+
+
Content Scheduling
+
+ Schedule your content for publication and manage your posting calendar.
+
+
+
+
Loading calendar... }>
+
+
+
+ );
+}
diff --git a/app/api/[orgId]/analytics/events/route.ts b/app/api/[orgId]/analytics/events/route.ts
new file mode 100644
index 00000000..79d1b24a
--- /dev/null
+++ b/app/api/[orgId]/analytics/events/route.ts
@@ -0,0 +1,66 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { createAnalyticsEventSchema } from '@/lib/schemas';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.VIEW_ANALYTICS);
+
+ const events = await (prisma as any).analyticsEvent.findMany({
+ where: { userId: session.user.id },
+ orderBy: { createdAt: 'desc' },
+ });
+
+ return NextResponse.json(events);
+ } catch (error) {
+ console.error('Error fetching analytics events:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.VIEW_ANALYTICS);
+
+ const body = await request.json();
+ const validatedData = createAnalyticsEventSchema.parse(body);
+
+ const event = await (prisma as any).analyticsEvent.create({
+ data: {
+ ...validatedData,
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ });
+
+ return NextResponse.json(event, { status: 201 });
+ } catch (error) {
+ console.error('Error creating analytics event:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/analytics/metrics/route.ts b/app/api/[orgId]/analytics/metrics/route.ts
new file mode 100644
index 00000000..03f0e45f
--- /dev/null
+++ b/app/api/[orgId]/analytics/metrics/route.ts
@@ -0,0 +1,88 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.VIEW_ANALYTICS);
+
+ // Get metrics for the org - count events by type
+ const eventCounts = await prisma.analyticsEvent.groupBy({
+ by: ['event'],
+ where: { organizationId: params.orgId },
+ _count: {
+ event: true,
+ },
+ });
+
+ // Get campaign-specific metrics
+ const campaignMetrics = await prisma.campaign.findMany({
+ where: { organizationId: params.orgId },
+ include: {
+ contents: {
+ include: {
+ analyticsEvents: true,
+ },
+ },
+ analyticsEvents: true,
+ },
+ });
+
+ // Calculate aggregated metrics
+ const totalImpressions = eventCounts.find((e) => e.event === 'impression')?._count.event || 0;
+ const totalClicks = eventCounts.find((e) => e.event === 'click')?._count.event || 0;
+ const totalViews = eventCounts.find((e) => e.event === 'view')?._count.event || 0;
+
+ const ctr = totalImpressions > 0 ? (totalClicks / totalImpressions) * 100 : 0;
+
+ // Calculate ROI (simplified - assuming some conversion value)
+ const conversions = eventCounts.find((e) => e.event === 'conversion')?._count.event || 0;
+ const roi = conversions > 0 ? ((conversions * 100) / totalClicks) * 100 : 0; // Assuming $100 per conversion
+
+ const metrics = {
+ totalEvents: eventCounts.reduce((sum, item) => sum + item._count.event, 0),
+ eventsByType: eventCounts.reduce(
+ (acc, item) => {
+ acc[item.event] = item._count.event;
+ return acc;
+ },
+ {} as Record
+ ),
+ impressions: totalImpressions,
+ clicks: totalClicks,
+ views: totalViews,
+ ctr: ctr,
+ roi: roi,
+ campaignMetrics: campaignMetrics.map((campaign) => ({
+ id: campaign.id,
+ name: campaign.name,
+ totalEvents: campaign.analyticsEvents.length,
+ contentCount: campaign.contents.length,
+ contentMetrics: campaign.contents.map((content) => ({
+ id: content.id,
+ title: content.title,
+ events: content.analyticsEvents.length,
+ impressions: content.analyticsEvents.filter((e) => e.event === 'impression').length,
+ clicks: content.analyticsEvents.filter((e) => e.event === 'click').length,
+ views: content.analyticsEvents.filter((e) => e.event === 'view').length,
+ })),
+ })),
+ };
+
+ return NextResponse.json(metrics);
+ } catch (error) {
+ console.error('Error fetching analytics metrics:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/analytics/track/route.ts b/app/api/[orgId]/analytics/track/route.ts
new file mode 100644
index 00000000..e4c2fd2d
--- /dev/null
+++ b/app/api/[orgId]/analytics/track/route.ts
@@ -0,0 +1,73 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.VIEW_ANALYTICS);
+
+ const body = await request.json();
+ const { event, campaignId, contentId, data } = body;
+
+ if (!event) {
+ return NextResponse.json({ error: 'Event type is required' }, { status: 400 });
+ }
+
+ // Validate that campaign and content belong to the organization
+ if (campaignId) {
+ const campaign = await prisma.campaign.findFirst({
+ where: {
+ id: campaignId,
+ organizationId: orgId,
+ },
+ });
+ if (!campaign) {
+ return NextResponse.json({ error: 'Campaign not found' }, { status: 404 });
+ }
+ }
+
+ if (contentId) {
+ const content = await prisma.content.findFirst({
+ where: {
+ id: contentId,
+ campaign: {
+ organizationId: orgId,
+ },
+ },
+ });
+ if (!content) {
+ return NextResponse.json({ error: 'Content not found' }, { status: 404 });
+ }
+ }
+
+ const analyticsEvent = await prisma.analyticsEvent.create({
+ data: {
+ event,
+ data,
+ userId: session.user.id,
+ organizationId: orgId,
+ campaignId: campaignId || null,
+ contentId: contentId || null,
+ },
+ });
+
+ return NextResponse.json(analyticsEvent, { status: 201 });
+ } catch (error) {
+ console.error('Error tracking analytics event:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/assets/[id]/route.ts b/app/api/[orgId]/assets/[id]/route.ts
new file mode 100644
index 00000000..bdf47728
--- /dev/null
+++ b/app/api/[orgId]/assets/[id]/route.ts
@@ -0,0 +1,136 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const asset = await prisma.asset.findFirst({
+ where: {
+ id,
+ content: { campaign: { organizationId: orgId } },
+ },
+ include: {
+ content: true,
+ },
+ });
+
+ if (!asset) {
+ return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
+ }
+
+ return NextResponse.json(asset);
+ } catch (error) {
+ console.error('Error fetching asset:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function PUT(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+ const body = await request.json();
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ // Verify asset belongs to org
+ const existingAsset = await prisma.asset.findFirst({
+ where: {
+ id,
+ content: { campaign: { organizationId: orgId } },
+ },
+ });
+
+ if (!existingAsset) {
+ return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
+ }
+
+ const updatedAsset = await prisma.asset.update({
+ where: { id },
+ data: {
+ url: body.url,
+ type: body.type,
+ },
+ include: {
+ content: true,
+ },
+ });
+
+ return NextResponse.json(updatedAsset);
+ } catch (error) {
+ console.error('Error updating asset:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ // Verify asset belongs to org
+ const existingAsset = await prisma.asset.findFirst({
+ where: {
+ id,
+ content: { campaign: { organizationId: orgId } },
+ },
+ });
+
+ if (!existingAsset) {
+ return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
+ }
+
+ await prisma.asset.delete({
+ where: { id },
+ });
+
+ return NextResponse.json({ message: 'Asset deleted successfully' });
+ } catch (error) {
+ console.error('Error deleting asset:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/assets/route.ts b/app/api/[orgId]/assets/route.ts
new file mode 100644
index 00000000..9a5d6c90
--- /dev/null
+++ b/app/api/[orgId]/assets/route.ts
@@ -0,0 +1,38 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+ const { searchParams } = new URL(request.url);
+ const contentId = searchParams.get('contentId');
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const where = contentId
+ ? { content: { campaign: { organizationId: orgId } }, contentId }
+ : { content: { campaign: { organizationId: orgId } } };
+
+ const assets = await prisma.asset.findMany({
+ where,
+ include: {
+ content: true,
+ },
+ });
+
+ return NextResponse.json(assets);
+ } catch (error) {
+ console.error('Error fetching assets:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/assets/upload/route.ts b/app/api/[orgId]/assets/upload/route.ts
new file mode 100644
index 00000000..fec30c42
--- /dev/null
+++ b/app/api/[orgId]/assets/upload/route.ts
@@ -0,0 +1,69 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+import formidable from 'formidable';
+import { promises as fs } from 'fs';
+import path from 'path';
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ // Parse multipart form data
+ const form = formidable({
+ uploadDir: path.join(process.cwd(), 'public/uploads'),
+ keepExtensions: true,
+ });
+
+ const [fields, files] = await form.parse(request as any);
+
+ const contentId = fields.contentId?.[0];
+ const file = files.file?.[0];
+
+ if (!contentId || !file) {
+ return NextResponse.json({ error: 'Missing contentId or file' }, { status: 400 });
+ }
+
+ // Verify content belongs to org
+ const content = await prisma.content.findFirst({
+ where: { id: contentId, campaign: { organizationId: orgId } },
+ });
+ if (!content) {
+ return NextResponse.json({ error: 'Content not found' }, { status: 404 });
+ }
+
+ // Mock URL - in real implementation, upload to cloud storage
+ const mockUrl = `/uploads/${file.newFilename}`;
+
+ const asset = await prisma.asset.create({
+ data: {
+ url: mockUrl,
+ name: fields.name?.[0] || file.originalFilename || 'Untitled',
+ type: file.mimetype || 'unknown',
+ size: file.size,
+ description: fields.description?.[0],
+ tags: fields.tags?.[0] ? JSON.parse(fields.tags[0]) : [],
+ contentId,
+ },
+ });
+
+ return NextResponse.json(asset, { status: 201 });
+ } catch (error) {
+ console.error('Error uploading asset:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/campaigns/[id]/contents/route.ts b/app/api/[orgId]/campaigns/[id]/contents/route.ts
new file mode 100644
index 00000000..1b9b2765
--- /dev/null
+++ b/app/api/[orgId]/campaigns/[id]/contents/route.ts
@@ -0,0 +1,61 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Fetch contents for the specific campaign
+ const contents = await prisma.content.findMany({
+ where: {
+ campaignId: params.id,
+ organizationId: params.orgId,
+ },
+ include: {
+ campaign: {
+ select: {
+ id: true,
+ title: true,
+ },
+ },
+ creator: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+
+ return NextResponse.json({ contents });
+ } catch (error) {
+ console.error('Error fetching campaign contents:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/api/[orgId]/campaigns/[id]/labels/route.ts b/app/api/[orgId]/campaigns/[id]/labels/route.ts
new file mode 100644
index 00000000..6cc87e53
--- /dev/null
+++ b/app/api/[orgId]/campaigns/[id]/labels/route.ts
@@ -0,0 +1,138 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import { z } from 'zod';
+
+// Validation schemas
+const createLabelSchema = z.object({
+ name: z.string().min(1).max(50),
+ color: z
+ .string()
+ .regex(/^#[0-9A-F]{6}$/i)
+ .default('#3B82F6'),
+});
+
+// const updateLabelSchema = z.object({
+// name: z.string().min(1).max(50).optional(),
+// color: z.string().regex(/^#[0-9A-F]{6}$/i).optional(),
+// });
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Get campaign labels
+ const labels = await prisma.campaignLabel.findMany({
+ where: {
+ campaignId: params.id,
+ },
+ orderBy: [{ name: 'asc' }],
+ });
+
+ return NextResponse.json(labels);
+ } catch (error) {
+ console.error('Error fetching campaign labels:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Check if user is campaign member with edit permissions
+ const campaignMember = await prisma.campaignMember.findUnique({
+ where: {
+ campaignId_userId: {
+ campaignId: params.id,
+ userId: session.user.id,
+ },
+ },
+ });
+
+ if (!campaignMember || !['OWNER', 'MANAGER'].includes(campaignMember.role)) {
+ return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const validatedData = createLabelSchema.parse(body);
+
+ // Check if label name already exists for this campaign
+ const existingLabel = await prisma.campaignLabel.findUnique({
+ where: {
+ campaignId_name: {
+ campaignId: params.id,
+ name: validatedData.name,
+ },
+ },
+ });
+
+ if (existingLabel) {
+ return NextResponse.json(
+ { error: 'Label with this name already exists' },
+ { status: 400 }
+ );
+ }
+
+ // Create label
+ const label = await prisma.campaignLabel.create({
+ data: {
+ ...validatedData,
+ campaignId: params.id,
+ },
+ });
+
+ return NextResponse.json(label, { status: 201 });
+ } catch (error) {
+ console.error('Error creating campaign label:', error);
+
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: 'Invalid request data', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/api/[orgId]/campaigns/[id]/members/route.ts b/app/api/[orgId]/campaigns/[id]/members/route.ts
new file mode 100644
index 00000000..3848edd7
--- /dev/null
+++ b/app/api/[orgId]/campaigns/[id]/members/route.ts
@@ -0,0 +1,172 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import { z } from 'zod';
+
+// Validation schemas
+const addMemberSchema = z.object({
+ userId: z.string().cuid(),
+ role: z.enum(['OWNER', 'MANAGER', 'MEMBER', 'VIEWER']).default('MEMBER'),
+});
+
+// const updateMemberSchema = z.object({
+// role: z.enum(['OWNER', 'MANAGER', 'MEMBER', 'VIEWER']),
+// });
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Get campaign members
+ const members = await prisma.campaignMember.findMany({
+ where: {
+ campaignId: params.id,
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ orderBy: [{ role: 'asc' }, { createdAt: 'asc' }],
+ });
+
+ return NextResponse.json(members);
+ } catch (error) {
+ console.error('Error fetching campaign members:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Check if user is campaign member with edit permissions
+ const campaignMember = await prisma.campaignMember.findUnique({
+ where: {
+ campaignId_userId: {
+ campaignId: params.id,
+ userId: session.user.id,
+ },
+ },
+ });
+
+ if (!campaignMember || !['OWNER', 'MANAGER'].includes(campaignMember.role)) {
+ return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const validatedData = addMemberSchema.parse(body);
+
+ // Check if user exists in organization
+ const targetUserMembership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: validatedData.userId,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!targetUserMembership) {
+ return NextResponse.json(
+ { error: 'User is not a member of this organization' },
+ { status: 400 }
+ );
+ }
+
+ // Check if user is already a campaign member
+ const existingMember = await prisma.campaignMember.findUnique({
+ where: {
+ campaignId_userId: {
+ campaignId: params.id,
+ userId: validatedData.userId,
+ },
+ },
+ });
+
+ if (existingMember) {
+ return NextResponse.json(
+ { error: 'User is already a member of this campaign' },
+ { status: 400 }
+ );
+ }
+
+ // Add member to campaign
+ const newMember = await prisma.campaignMember.create({
+ data: {
+ campaignId: params.id,
+ userId: validatedData.userId,
+ role: validatedData.role,
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ });
+
+ return NextResponse.json(newMember, { status: 201 });
+ } catch (error) {
+ console.error('Error adding campaign member:', error);
+
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: 'Invalid request data', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/api/[orgId]/campaigns/[id]/route.ts b/app/api/[orgId]/campaigns/[id]/route.ts
new file mode 100644
index 00000000..edf1ffc3
--- /dev/null
+++ b/app/api/[orgId]/campaigns/[id]/route.ts
@@ -0,0 +1,307 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import { z } from 'zod';
+
+// Validation schemas
+const updateCampaignSchema = z.object({
+ name: z.string().min(3).max(100).optional(),
+ summary: z.string().max(200).optional(),
+ description: z.string().max(2000).optional(),
+ status: z.enum(['DRAFT', 'PLANNING', 'READY', 'DONE', 'CANCELED']).optional(),
+ health: z.enum(['ON_TRACK', 'AT_RISK', 'OFF_TRACK']).optional(),
+ priority: z.enum(['NO_PRIORITY', 'LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
+ leadId: z.string().cuid().optional(),
+ startDate: z.string().datetime().optional(),
+ targetDate: z.string().datetime().optional(),
+});
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Get campaign with all related data
+ const campaign = await prisma.campaign.findFirst({
+ where: {
+ id: params.id,
+ organizationId: params.orgId,
+ },
+ include: {
+ lead: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ members: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ tasks: {
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ subtasks: {
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ },
+ orderBy: [{ priority: 'desc' }, { createdAt: 'asc' }],
+ },
+ labels: true,
+ milestones: {
+ orderBy: { dueDate: 'asc' },
+ },
+ contents: {
+ select: {
+ id: true,
+ title: true,
+ status: true,
+ createdAt: true,
+ },
+ },
+ schedules: {
+ select: {
+ id: true,
+ name: true,
+ status: true,
+ runAt: true,
+ channel: true,
+ },
+ },
+ _count: {
+ select: {
+ tasks: true,
+ members: true,
+ labels: true,
+ milestones: true,
+ contents: true,
+ schedules: true,
+ },
+ },
+ },
+ });
+
+ if (!campaign) {
+ return NextResponse.json({ error: 'Campaign not found' }, { status: 404 });
+ }
+
+ return NextResponse.json(campaign);
+ } catch (error) {
+ console.error('Error fetching campaign:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function PUT(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Check if user is campaign member with edit permissions
+ const campaignMember = await prisma.campaignMember.findUnique({
+ where: {
+ campaignId_userId: {
+ campaignId: params.id,
+ userId: session.user.id,
+ },
+ },
+ });
+
+ if (!campaignMember || !['OWNER', 'MANAGER'].includes(campaignMember.role)) {
+ return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const validatedData = updateCampaignSchema.parse(body);
+
+ // Validate dates if both are provided
+ if (validatedData.startDate && validatedData.targetDate) {
+ const startDate = new Date(validatedData.startDate);
+ const targetDate = new Date(validatedData.targetDate);
+
+ if (startDate >= targetDate) {
+ return NextResponse.json(
+ { error: 'Target date must be after start date' },
+ { status: 400 }
+ );
+ }
+ }
+
+ // Update campaign
+ const updatedCampaign = await prisma.campaign.update({
+ where: {
+ id: params.id,
+ organizationId: params.orgId,
+ },
+ data: {
+ ...validatedData,
+ startDate: validatedData.startDate ? new Date(validatedData.startDate) : undefined,
+ targetDate: validatedData.targetDate ? new Date(validatedData.targetDate) : undefined,
+ },
+ include: {
+ lead: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ members: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ labels: true,
+ milestones: true,
+ _count: {
+ select: {
+ tasks: true,
+ members: true,
+ labels: true,
+ milestones: true,
+ },
+ },
+ },
+ });
+
+ return NextResponse.json(updatedCampaign);
+ } catch (error) {
+ console.error('Error updating campaign:', error);
+
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: 'Invalid request data', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Only campaign OWNER or organization ADMIN can delete campaigns
+ const campaignMember = await prisma.campaignMember.findUnique({
+ where: {
+ campaignId_userId: {
+ campaignId: params.id,
+ userId: session.user.id,
+ },
+ },
+ });
+
+ const canDelete = campaignMember?.role === 'OWNER' || membership.role === 'ADMIN';
+
+ if (!canDelete) {
+ return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
+ }
+
+ // Delete campaign (cascades to related data)
+ await prisma.campaign.delete({
+ where: {
+ id: params.id,
+ organizationId: params.orgId,
+ },
+ });
+
+ return NextResponse.json({ message: 'Campaign deleted successfully' });
+ } catch (error) {
+ console.error('Error deleting campaign:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/api/[orgId]/campaigns/[id]/tasks/[taskId]/route.ts b/app/api/[orgId]/campaigns/[id]/tasks/[taskId]/route.ts
new file mode 100644
index 00000000..039e5b66
--- /dev/null
+++ b/app/api/[orgId]/campaigns/[id]/tasks/[taskId]/route.ts
@@ -0,0 +1,106 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+
+export async function PUT(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string; taskId: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ const body = await request.json();
+
+ // Update the task
+ const updatedTask = await prisma.campaignTask.update({
+ where: {
+ id: params.taskId,
+ campaignId: params.id,
+ },
+ data: body,
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ subtasks: {
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ return NextResponse.json({ task: updatedTask });
+ } catch (error) {
+ console.error('Error updating task:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string; taskId: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Delete the task and all its subtasks
+ await prisma.campaignTask.deleteMany({
+ where: {
+ OR: [{ id: params.taskId }, { parentTaskId: params.taskId }],
+ campaignId: params.id,
+ },
+ });
+
+ return NextResponse.json({ message: 'Task deleted successfully' });
+ } catch (error) {
+ console.error('Error deleting task:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/api/[orgId]/campaigns/[id]/tasks/route.ts b/app/api/[orgId]/campaigns/[id]/tasks/route.ts
new file mode 100644
index 00000000..85f4d7ba
--- /dev/null
+++ b/app/api/[orgId]/campaigns/[id]/tasks/route.ts
@@ -0,0 +1,164 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import { z } from 'zod';
+
+// Validation schemas
+const createTaskSchema = z.object({
+ title: z.string().min(3).max(200),
+ description: z.string().max(2000).optional(),
+ status: z.enum(['TODO', 'IN_PROGRESS', 'REVIEW', 'DONE', 'CANCELLED']).default('TODO'),
+ priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).default('MEDIUM'),
+ assigneeId: z.string().cuid().optional(),
+ dueDate: z.string().datetime().optional(),
+ parentTaskId: z.string().cuid().optional(),
+});
+
+// const updateTaskSchema = z.object({
+// title: z.string().min(3).max(200).optional(),
+// description: z.string().max(3).max(2000).optional(),
+// status: z.enum(['TODO', 'IN_PROGRESS', 'REVIEW', 'DONE', 'CANCELLED']).optional(),
+// priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
+// assigneeId: z.string().cuid().optional(),
+// dueDate: z.string().datetime().optional(),
+// parentTaskId: z.string().cuid().optional(),
+// });
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Get tasks for the campaign
+ const tasks = await prisma.campaignTask.findMany({
+ where: {
+ campaignId: params.id,
+ parentTaskId: null, // Only get top-level tasks
+ },
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ subtasks: {
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ orderBy: [{ priority: 'desc' }, { createdAt: 'asc' }],
+ },
+ },
+ orderBy: [{ priority: 'desc' }, { createdAt: 'asc' }],
+ });
+
+ return NextResponse.json(tasks);
+ } catch (error) {
+ console.error('Error fetching campaign tasks:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Check if user is campaign member with edit permissions
+ const campaignMember = await prisma.campaignMember.findUnique({
+ where: {
+ campaignId_userId: {
+ campaignId: params.id,
+ userId: session.user.id,
+ },
+ },
+ });
+
+ if (!campaignMember || !['OWNER', 'MANAGER'].includes(campaignMember.role)) {
+ return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const validatedData = createTaskSchema.parse(body);
+
+ // Create task
+ const task = await prisma.campaignTask.create({
+ data: {
+ ...validatedData,
+ campaignId: params.id,
+ dueDate: validatedData.dueDate ? new Date(validatedData.dueDate) : null,
+ },
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ subtasks: true,
+ },
+ });
+
+ return NextResponse.json(task, { status: 201 });
+ } catch (error) {
+ console.error('Error creating campaign task:', error);
+
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: 'Invalid request data', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/api/[orgId]/campaigns/route.ts b/app/api/[orgId]/campaigns/route.ts
new file mode 100644
index 00000000..925f4c62
--- /dev/null
+++ b/app/api/[orgId]/campaigns/route.ts
@@ -0,0 +1,269 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { prisma } from '@/lib/prisma';
+import { z } from 'zod';
+
+// Validation schemas
+const createCampaignSchema = z.object({
+ name: z.string().min(3).max(100),
+ summary: z.string().max(200).optional(),
+ description: z.string().max(2000).optional(),
+ status: z.enum(['DRAFT', 'PLANNING', 'READY', 'DONE', 'CANCELED']).default('DRAFT'),
+ health: z.enum(['ON_TRACK', 'AT_RISK', 'OFF_TRACK']).default('ON_TRACK'),
+ priority: z.enum(['NO_PRIORITY', 'LOW', 'MEDIUM', 'HIGH', 'URGENT']).default('NO_PRIORITY'),
+ leadId: z.string().cuid().optional(),
+ startDate: z.string().datetime().optional(),
+ targetDate: z.string().datetime().optional(),
+});
+
+const getCampaignsSchema = z.object({
+ status: z.enum(['DRAFT', 'PLANNING', 'READY', 'DONE', 'CANCELED']).optional(),
+ health: z.enum(['ON_TRACK', 'AT_RISK', 'OFF_TRACK']).optional(),
+ priority: z.enum(['NO_PRIORITY', 'LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
+ search: z.string().optional(),
+ page: z.coerce.number().min(1).default(1),
+ limit: z.coerce.number().min(1).max(100).default(20),
+});
+
+export async function GET(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(request.url);
+ const query = getCampaignsSchema.parse({
+ status: searchParams.get('status'),
+ health: searchParams.get('health'),
+ priority: searchParams.get('priority'),
+ search: searchParams.get('search'),
+ page: searchParams.get('page'),
+ limit: searchParams.get('limit'),
+ });
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Build where clause
+ const where: any = {
+ organizationId: params.orgId,
+ };
+
+ if (query.status) {
+ where.status = query.status;
+ }
+
+ if (query.health) {
+ where.health = query.health;
+ }
+
+ if (query.priority) {
+ where.priority = query.priority;
+ }
+
+ if (query.search) {
+ where.OR = [
+ { name: { contains: query.search, mode: 'insensitive' } },
+ { summary: { contains: query.search, mode: 'insensitive' } },
+ { description: { contains: query.search, mode: 'insensitive' } },
+ ];
+ }
+
+ // Get total count for pagination
+ const total = await prisma.campaign.count({ where });
+
+ // Get campaigns with pagination
+ const campaigns = await prisma.campaign.findMany({
+ where,
+ include: {
+ lead: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ members: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ tasks: {
+ select: {
+ id: true,
+ status: true,
+ },
+ },
+ labels: true,
+ milestones: true,
+ _count: {
+ select: {
+ tasks: true,
+ members: true,
+ labels: true,
+ milestones: true,
+ },
+ },
+ },
+ orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }],
+ skip: (query.page - 1) * query.limit,
+ take: query.limit,
+ });
+
+ // Calculate pagination info
+ const totalPages = Math.ceil(total / query.limit);
+ const hasNextPage = query.page < totalPages;
+ const hasPrevPage = query.page > 1;
+
+ return NextResponse.json({
+ campaigns,
+ pagination: {
+ page: query.page,
+ limit: query.limit,
+ total,
+ totalPages,
+ hasNextPage,
+ hasPrevPage,
+ },
+ });
+ } catch (error) {
+ console.error('Error fetching campaigns:', error);
+
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: 'Invalid query parameters', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Check if user has access to this organization
+ const membership = await prisma.membership.findUnique({
+ where: {
+ userId_organizationId: {
+ userId: session.user.id,
+ organizationId: params.orgId,
+ },
+ },
+ });
+
+ if (!membership) {
+ return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+ }
+
+ // Only ADMIN and BRAND_OWNER can create campaigns
+ if (!['ADMIN', 'BRAND_OWNER'].includes(membership.role)) {
+ return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const validatedData = createCampaignSchema.parse(body);
+
+ // Validate dates
+ if (validatedData.startDate && validatedData.targetDate) {
+ const startDate = new Date(validatedData.startDate);
+ const targetDate = new Date(validatedData.targetDate);
+
+ if (startDate >= targetDate) {
+ return NextResponse.json(
+ { error: 'Target date must be after start date' },
+ { status: 400 }
+ );
+ }
+ }
+
+ // Create campaign
+ const campaign = await prisma.campaign.create({
+ data: {
+ ...validatedData,
+ organizationId: params.orgId,
+ startDate: validatedData.startDate ? new Date(validatedData.startDate) : null,
+ targetDate: validatedData.targetDate ? new Date(validatedData.targetDate) : null,
+ },
+ include: {
+ lead: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ members: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ labels: true,
+ milestones: true,
+ _count: {
+ select: {
+ tasks: true,
+ members: true,
+ labels: true,
+ milestones: true,
+ },
+ },
+ },
+ });
+
+ // Add creator as campaign member with OWNER role
+ await prisma.campaignMember.create({
+ data: {
+ campaignId: campaign.id,
+ userId: session.user.id,
+ role: 'OWNER',
+ },
+ });
+
+ return NextResponse.json(campaign, { status: 201 });
+ } catch (error) {
+ console.error('Error creating campaign:', error);
+
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: 'Invalid request data', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/api/[orgId]/content/[id]/route.ts b/app/api/[orgId]/content/[id]/route.ts
new file mode 100644
index 00000000..9c4ce172
--- /dev/null
+++ b/app/api/[orgId]/content/[id]/route.ts
@@ -0,0 +1,111 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { updateContentSchema } from '@/lib/schemas';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const content = await prisma.content.findFirst({
+ where: { id, campaign: { organizationId: orgId } },
+ include: {
+ campaign: true,
+ assets: true,
+ },
+ });
+
+ if (!content) {
+ return NextResponse.json({ error: 'Content not found' }, { status: 404 });
+ }
+
+ return NextResponse.json(content);
+ } catch (error) {
+ console.error('Error fetching content:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function PUT(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const body = await request.json();
+ const validatedData = updateContentSchema.parse(body);
+
+ const content = await prisma.content.update({
+ where: { id },
+ data: validatedData,
+ include: {
+ campaign: true,
+ assets: true,
+ },
+ });
+
+ return NextResponse.json(content);
+ } catch (error) {
+ console.error('Error updating content:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ await prisma.content.delete({
+ where: { id },
+ });
+
+ return NextResponse.json({ message: 'Content deleted' });
+ } catch (error) {
+ console.error('Error deleting content:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/content/generate/route.ts b/app/api/[orgId]/content/generate/route.ts
new file mode 100644
index 00000000..0ccb6865
--- /dev/null
+++ b/app/api/[orgId]/content/generate/route.ts
@@ -0,0 +1,61 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { generateContentSchema } from '@/lib/schemas';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+import { generateContent } from '@/lib/openai';
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const body = await request.json();
+ const validatedData = generateContentSchema.parse(body);
+
+ // Verify campaign belongs to org
+ const campaign = await prisma.campaign.findFirst({
+ where: { id: validatedData.campaignId, organizationId: orgId },
+ });
+ if (!campaign) {
+ return NextResponse.json({ error: 'Campaign not found' }, { status: 404 });
+ }
+
+ // Generate content using OpenAI
+ const generatedBody = await generateContent({
+ prompt: validatedData.prompt,
+ type: 'general',
+ tone: 'professional',
+ length: 'medium',
+ });
+
+ const content = await prisma.content.create({
+ data: {
+ title: `AI Generated: ${validatedData.prompt.slice(0, 50)}...`,
+ body: generatedBody,
+ campaignId: validatedData.campaignId,
+ },
+ include: {
+ campaign: true,
+ assets: true,
+ },
+ });
+
+ return NextResponse.json(content, { status: 201 });
+ } catch (error) {
+ console.error('Error generating content:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/content/ideas/route.ts b/app/api/[orgId]/content/ideas/route.ts
new file mode 100644
index 00000000..18f83c18
--- /dev/null
+++ b/app/api/[orgId]/content/ideas/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { generateIdeas } from '@/lib/openai';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const body = await request.json();
+ const { topic, count = 5, type = 'general' } = body;
+
+ if (!topic) {
+ return NextResponse.json({ error: 'Topic is required' }, { status: 400 });
+ }
+
+ const ideas = await generateIdeas({
+ topic,
+ count,
+ type,
+ });
+
+ return NextResponse.json({ ideas }, { status: 200 });
+ } catch (error) {
+ console.error('Error generating ideas:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/content/route.ts b/app/api/[orgId]/content/route.ts
new file mode 100644
index 00000000..98139997
--- /dev/null
+++ b/app/api/[orgId]/content/route.ts
@@ -0,0 +1,111 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { createContentSchema } from '@/lib/schemas';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_UNAUTHORIZED', message: 'Unauthorized' } },
+ { status: 401 }
+ );
+ }
+
+ const { orgId } = params;
+ const { searchParams } = new URL(request.url);
+ const campaignId = searchParams.get('campaignId');
+ const status = searchParams.get('status');
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const where: any = {
+ campaign: { organizationId: orgId },
+ };
+
+ if (campaignId) {
+ where.campaignId = campaignId;
+ }
+
+ if (status) {
+ where.status = status;
+ }
+
+ const contents = await prisma.content.findMany({
+ where,
+ include: {
+ campaign: true,
+ assets: true,
+ },
+ orderBy: { createdAt: 'desc' },
+ });
+
+ return NextResponse.json({ ok: true, data: contents });
+ } catch (error) {
+ console.error('Error fetching contents:', error);
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_INTERNAL', message: 'Internal server error' } },
+ { status: 500 }
+ );
+ }
+}
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_UNAUTHORIZED', message: 'Unauthorized' } },
+ { status: 401 }
+ );
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const body = await request.json();
+ const validatedData = createContentSchema.parse(body);
+
+ // Verify campaign belongs to org
+ const campaign = await prisma.campaign.findFirst({
+ where: { id: validatedData.campaignId, organizationId: orgId },
+ });
+ if (!campaign) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: { code: 'E_NOT_FOUND', message: 'Campaign not found' },
+ },
+ { status: 404 }
+ );
+ }
+
+ const content = await prisma.content.create({
+ data: validatedData,
+ include: {
+ campaign: true,
+ assets: true,
+ },
+ });
+
+ return NextResponse.json({ ok: true, data: content }, { status: 201 });
+ } catch (error) {
+ console.error('Error creating content:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: { code: 'E_FORBIDDEN', message: 'Forbidden' },
+ },
+ { status: 403 }
+ );
+ }
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_INTERNAL', message: 'Internal server error' } },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/content/summarize/route.ts b/app/api/[orgId]/content/summarize/route.ts
new file mode 100644
index 00000000..e6de9068
--- /dev/null
+++ b/app/api/[orgId]/content/summarize/route.ts
@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { summarizeContent } from '@/lib/openai';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const body = await request.json();
+ const { content, length = 'brief' } = body;
+
+ if (!content) {
+ return NextResponse.json({ error: 'Content is required' }, { status: 400 });
+ }
+
+ const summary = await summarizeContent({
+ content,
+ length,
+ });
+
+ return NextResponse.json({ summary }, { status: 200 });
+ } catch (error) {
+ console.error('Error summarizing content:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/content/translate/route.ts b/app/api/[orgId]/content/translate/route.ts
new file mode 100644
index 00000000..29c528a3
--- /dev/null
+++ b/app/api/[orgId]/content/translate/route.ts
@@ -0,0 +1,44 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { translateContent } from '@/lib/openai';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_CONTENT);
+
+ const body = await request.json();
+ const { content, targetLanguage, sourceLanguage } = body;
+
+ if (!content || !targetLanguage) {
+ return NextResponse.json(
+ { error: 'Content and target language are required' },
+ { status: 400 }
+ );
+ }
+
+ const translatedContent = await translateContent({
+ content,
+ targetLanguage,
+ sourceLanguage,
+ });
+
+ return NextResponse.json({ translatedContent }, { status: 200 });
+ } catch (error) {
+ console.error('Error translating content:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/schedules/[id]/route.ts b/app/api/[orgId]/schedules/[id]/route.ts
new file mode 100644
index 00000000..ff8e6913
--- /dev/null
+++ b/app/api/[orgId]/schedules/[id]/route.ts
@@ -0,0 +1,110 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { updateScheduleSchema } from '@/lib/schemas';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_SCHEDULES);
+
+ const schedule = await prisma.schedule.findFirst({
+ where: { id, campaign: { organizationId: orgId } },
+ include: {
+ campaign: true,
+ content: true,
+ },
+ });
+
+ if (!schedule) {
+ return NextResponse.json({ error: 'Schedule not found' }, { status: 404 });
+ }
+
+ return NextResponse.json(schedule);
+ } catch (error) {
+ console.error('Error fetching schedule:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function PUT(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_SCHEDULES);
+
+ const body = await request.json();
+ const validatedData = updateScheduleSchema.parse(body);
+
+ const schedule = await prisma.schedule.update({
+ where: { id },
+ data: validatedData,
+ include: {
+ campaign: true,
+ },
+ });
+
+ return NextResponse.json(schedule);
+ } catch (error) {
+ console.error('Error updating schedule:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
+
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { orgId: string; id: string } }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { orgId, id } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_SCHEDULES);
+
+ await prisma.schedule.delete({
+ where: { id },
+ });
+
+ return NextResponse.json({ message: 'Schedule deleted' });
+ } catch (error) {
+ console.error('Error deleting schedule:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/[orgId]/schedules/route.ts b/app/api/[orgId]/schedules/route.ts
new file mode 100644
index 00000000..d956f514
--- /dev/null
+++ b/app/api/[orgId]/schedules/route.ts
@@ -0,0 +1,190 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { createScheduleSchema } from '@/lib/schemas';
+import { requirePermission, PERMISSIONS } from '@/lib/rbac';
+
+export async function GET(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_UNAUTHORIZED', message: 'Unauthorized' } },
+ { status: 401 }
+ );
+ }
+
+ const { orgId } = params;
+ const { searchParams } = new URL(request.url);
+ const from = searchParams.get('from');
+ const to = searchParams.get('to');
+ const channels = searchParams.get('channels')?.split(',');
+ const campaigns = searchParams.get('campaigns')?.split(',');
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_SCHEDULES);
+
+ const where: any = {
+ campaign: { organizationId: orgId },
+ };
+
+ // Add date range filter
+ if (from && to) {
+ where.runAt = {
+ gte: new Date(from),
+ lte: new Date(to),
+ };
+ }
+
+ // Add channel filter
+ if (channels && channels.length > 0) {
+ where.channel = { in: channels };
+ }
+
+ // Add campaign filter
+ if (campaigns && campaigns.length > 0) {
+ where.campaignId = { in: campaigns };
+ }
+
+ const schedules = await prisma.schedule.findMany({
+ where,
+ include: {
+ campaign: true,
+ content: true,
+ },
+ orderBy: { runAt: 'asc' },
+ });
+
+ return NextResponse.json({ ok: true, data: schedules });
+ } catch (error) {
+ console.error('Error fetching schedules:', error);
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_INTERNAL', message: 'Internal server error' } },
+ { status: 500 }
+ );
+ }
+}
+
+export async function POST(request: NextRequest, { params }: { params: { orgId: string } }) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_UNAUTHORIZED', message: 'Unauthorized' } },
+ { status: 401 }
+ );
+ }
+
+ const { orgId } = params;
+
+ await requirePermission(session.user.id, orgId, PERMISSIONS.MANAGE_SCHEDULES);
+
+ const body = await request.json();
+ const validatedData = createScheduleSchema.parse(body);
+
+ // Verify campaign belongs to org
+ const campaign = await prisma.campaign.findFirst({
+ where: { id: validatedData.campaignId, organizationId: orgId },
+ });
+ if (!campaign) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: { code: 'E_NOT_FOUND', message: 'Campaign not found' },
+ },
+ { status: 404 }
+ );
+ }
+
+ // Verify content belongs to campaign
+ const content = await prisma.content.findFirst({
+ where: { id: validatedData.contentId, campaignId: validatedData.campaignId },
+ });
+ if (!content) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: { code: 'E_NOT_FOUND', message: 'Content not found' },
+ },
+ { status: 404 }
+ );
+ }
+
+ // Check for potential conflicts (same channel at overlapping times)
+ const conflictingSchedule = await prisma.schedule.findFirst({
+ where: {
+ channel: validatedData.channel,
+ runAt: {
+ gte: new Date(new Date(validatedData.runAt).getTime() - 15 * 60 * 1000), // 15 minutes before
+ lte: new Date(new Date(validatedData.runAt).getTime() + 15 * 60 * 1000), // 15 minutes after
+ },
+ status: { not: 'CANCELLED' },
+ },
+ });
+
+ if (conflictingSchedule) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: {
+ code: 'E_CONFLICT_SLOT',
+ message: 'Potential scheduling conflict detected',
+ details: { conflictingScheduleId: conflictingSchedule.id },
+ },
+ },
+ { status: 409 }
+ );
+ }
+
+ // Create schedule and update content status in a transaction
+ const result = await prisma.$transaction(async (tx) => {
+ const schedule = await tx.schedule.create({
+ data: {
+ runAt: new Date(validatedData.runAt),
+ timezone: validatedData.timezone,
+ channel: validatedData.channel,
+ status: validatedData.status || 'PENDING',
+ campaignId: validatedData.campaignId,
+ contentId: validatedData.contentId,
+ },
+ include: {
+ campaign: true,
+ content: true,
+ },
+ });
+
+ // Update content status to SCHEDULED
+ await tx.content.update({
+ where: { id: validatedData.contentId },
+ data: { status: 'SCHEDULED' },
+ });
+
+ return schedule;
+ });
+
+ return NextResponse.json({ ok: true, data: result }, { status: 201 });
+ } catch (error) {
+ console.error('Error creating schedule:', error);
+ if (error instanceof Error && error.message === 'Insufficient permissions') {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: { code: 'E_FORBIDDEN', message: 'Forbidden' },
+ },
+ { status: 403 }
+ );
+ }
+ if (error instanceof Error && error.message.includes('Invalid date')) {
+ return NextResponse.json(
+ {
+ ok: false,
+ error: { code: 'E_VALIDATION', message: 'Invalid date format' },
+ },
+ { status: 400 }
+ );
+ }
+ return NextResponse.json(
+ { ok: false, error: { code: 'E_INTERNAL', message: 'Internal server error' } },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 00000000..bf39bebd
--- /dev/null
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,3 @@
+import { authHandlers } from '@/lib/auth';
+
+export const { GET, POST } = authHandlers;
diff --git a/app/api/cron/publish/route.ts b/app/api/cron/publish/route.ts
new file mode 100644
index 00000000..d4234386
--- /dev/null
+++ b/app/api/cron/publish/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { runCronJob } from '@/lib/cron-worker';
+
+export async function POST(request: NextRequest) {
+ try {
+ // You might want to add authentication/authorization here
+ // to prevent unauthorized access to the cron endpoint
+
+ const result = await runCronJob();
+
+ if (result.success) {
+ return NextResponse.json({
+ message: 'Cron job executed successfully',
+ published: result.published,
+ });
+ } else {
+ return NextResponse.json({ error: result.error }, { status: 500 });
+ }
+ } catch (error) {
+ console.error('Error running cron job:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+// Optional: GET endpoint to check cron status
+export async function GET() {
+ return NextResponse.json({
+ message: 'Cron endpoint is available',
+ endpoint: '/api/cron/publish',
+ method: 'POST',
+ });
+}
diff --git a/app/api/health/route.ts b/app/api/health/route.ts
new file mode 100644
index 00000000..ec945278
--- /dev/null
+++ b/app/api/health/route.ts
@@ -0,0 +1,38 @@
+import { NextResponse } from 'next/server';
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+export async function GET() {
+ try {
+ // Check database connection
+ await prisma.$queryRaw`SELECT 1`;
+
+ // Check OpenAI API key (basic check)
+ const openaiKey = process.env.OPENAI_API_KEY;
+ const openaiOk = openaiKey && openaiKey.startsWith('sk-');
+
+ return NextResponse.json({
+ ok: true,
+ status: 'healthy',
+ timestamp: new Date().toISOString(),
+ services: {
+ database: 'connected',
+ openai: openaiOk ? 'configured' : 'not configured',
+ },
+ });
+ } catch (error) {
+ console.error('Health check failed:', error);
+ return NextResponse.json(
+ {
+ ok: false,
+ status: 'unhealthy',
+ timestamp: new Date().toISOString(),
+ error: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 503 }
+ );
+ } finally {
+ await prisma.$disconnect();
+ }
+}
diff --git a/app/api/me/role/route.ts b/app/api/me/role/route.ts
new file mode 100644
index 00000000..39c390b6
--- /dev/null
+++ b/app/api/me/role/route.ts
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { getUserRole } from '@/lib/rbac';
+
+export async function GET(request: NextRequest) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(request.url);
+ const orgId = searchParams.get('orgId');
+
+ if (!orgId) {
+ return NextResponse.json({ error: 'Organization ID required' }, { status: 400 });
+ }
+
+ const userRole = await getUserRole(orgId);
+
+ if (!userRole) {
+ return NextResponse.json(
+ { error: 'No role found for this organization' },
+ { status: 403 }
+ );
+ }
+
+ return NextResponse.json({ role: userRole });
+ } catch (error) {
+ console.error('Error fetching user role:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/me/route.ts b/app/api/me/route.ts
new file mode 100644
index 00000000..cef8894c
--- /dev/null
+++ b/app/api/me/route.ts
@@ -0,0 +1,13 @@
+import { NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import { auth } from '@/lib/auth';
+
+export async function GET() {
+ const session = await auth();
+ if (!session?.user?.id) return NextResponse.json({ hasMembership: false });
+ const membership = await prisma.membership.findFirst({ where: { userId: session.user.id } });
+ return NextResponse.json({
+ hasMembership: !!membership,
+ organizationId: membership?.organizationId,
+ });
+}
diff --git a/app/auth/signin/page.tsx b/app/auth/signin/page.tsx
new file mode 100644
index 00000000..47d4f1e0
--- /dev/null
+++ b/app/auth/signin/page.tsx
@@ -0,0 +1,110 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { signIn } from 'next-auth/react';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+
+export default function SignInPage() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [callbackUrl, setCallbackUrl] = useState('/');
+ const router = useRouter();
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ try {
+ const url = new URL(window.location.href);
+ const cb = url.searchParams.get('callbackUrl');
+ if (cb) setCallbackUrl(cb);
+ } catch {}
+ }
+ }, []);
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setLoading(true);
+ const res = await signIn('credentials', {
+ email,
+ password,
+ redirect: false,
+ callbackUrl,
+ });
+ setLoading(false);
+ if (res?.ok) {
+ try {
+ const me = await fetch('/api/me').then((r) =>
+ r.ok ? r.json() : { hasMembership: false }
+ );
+
+ if (!me.hasMembership) {
+ router.push('/onboarding');
+ return;
+ }
+
+ // If user has membership, check if callbackUrl is valid
+ // If callbackUrl is from an org route but user doesn't have access, use their actual org
+ let targetUrl = callbackUrl;
+
+ if (callbackUrl) {
+ // Check if callbackUrl is an org route (contains orgId)
+ const urlParts = callbackUrl.split('/').filter(Boolean);
+ if (
+ urlParts.length >= 1 &&
+ urlParts[0] !== 'auth' &&
+ urlParts[0] !== 'api' &&
+ urlParts[0] !== '_next'
+ ) {
+ // This looks like an org route, verify it matches user's org
+ const callbackOrgId = urlParts[0];
+ if (callbackOrgId !== me.organizationId) {
+ targetUrl = `/${me.organizationId}/projects`;
+ }
+ }
+ }
+
+ targetUrl = targetUrl || `/${me.organizationId}/projects`;
+ router.push(targetUrl);
+ } catch (error) {
+ router.push('/onboarding');
+ }
+ } else {
+ alert('Invalid credentials (dev: check DEV_LOGIN_PASSWORD)');
+ }
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/app/auth/signup/page.tsx b/app/auth/signup/page.tsx
new file mode 100644
index 00000000..11705d18
--- /dev/null
+++ b/app/auth/signup/page.tsx
@@ -0,0 +1,173 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { signIn } from 'next-auth/react';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Label } from '@/components/ui/label';
+
+export default function SignUpPage() {
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [success, setSuccess] = useState(false);
+ const router = useRouter();
+
+ const validateForm = () => {
+ if (!name.trim()) {
+ setError('Name is required');
+ return false;
+ }
+ if (!email.trim()) {
+ setError('Email is required');
+ return false;
+ }
+ if (!email.includes('@')) {
+ setError('Please enter a valid email');
+ return false;
+ }
+ if (password.length < 6) {
+ setError('Password must be at least 6 characters');
+ return false;
+ }
+ if (password !== confirmPassword) {
+ setError('Passwords do not match');
+ return false;
+ }
+ return true;
+ };
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError('');
+
+ if (!validateForm()) {
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ // For now, we'll use the same logic as signin since this is dev-only
+ // In production, you'd want to create a proper signup API endpoint
+ const res = await signIn('credentials', {
+ email,
+ password,
+ redirect: false,
+ });
+
+ if (res?.ok) {
+ setSuccess(true);
+ setTimeout(() => {
+ router.push('/onboarding');
+ }, 2000);
+ } else {
+ setError('Failed to create account. Please try again.');
+ }
+ } catch (error) {
+ setError('An error occurred. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+ Create Account
+
+ Join AiM Platform to start creating amazing campaigns
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 5572a576..e8c74411 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,6 +1,9 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import { Toaster } from '@/components/ui/sonner';
+import { NextIntlClientProvider } from 'next-intl';
+import { getMessages } from 'next-intl/server';
+import { SessionProvider } from 'next-auth/react';
import './globals.css';
const geistSans = Geist({
@@ -55,21 +58,30 @@ export const metadata: Metadata = {
import { ThemeProvider } from '@/components/layout/theme-provider';
-export default function RootLayout({
+export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
+ const messages = await getMessages();
+
return (
-
-
- {children}
-
-
+
+
+
+
+ {children}
+
+
+
+
);
diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx
new file mode 100644
index 00000000..025460a3
--- /dev/null
+++ b/app/onboarding/page.tsx
@@ -0,0 +1,127 @@
+import { redirect } from 'next/navigation';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+
+export default async function OnboardingPage() {
+ try {
+ const session = await auth();
+
+ if (!session?.user?.id) {
+ redirect('/auth/signin');
+ }
+
+ console.log('Onboarding - User ID:', session.user.id);
+ console.log('Onboarding - User email:', session.user.email);
+
+ // First, check if user exists by ID
+ let user = await prisma.user.findUnique({
+ where: { id: session.user.id },
+ });
+
+ if (!user) {
+ // Check if user exists by email (might have different ID)
+ const existingUserByEmail = await prisma.user.findUnique({
+ where: { email: session.user.email! },
+ });
+
+ if (existingUserByEmail) {
+ console.log('Onboarding - User found by email with different ID:', existingUserByEmail);
+
+ // Check if this user already has membership
+ const existingMembership = await prisma.membership.findFirst({
+ where: { userId: existingUserByEmail.id },
+ });
+
+ if (existingMembership) {
+ console.log('Onboarding - Existing user has membership, redirecting');
+ redirect(`/${existingMembership.organizationId}/projects`);
+ }
+
+ // Use the existing user ID for membership creation
+ user = existingUserByEmail;
+ } else {
+ console.log('Onboarding - User not found in DB, creating...');
+ // Create user if it doesn't exist
+ user = await prisma.user.create({
+ data: {
+ id: session.user.id,
+ email: session.user.email!,
+ name: session.user.name || 'Unknown User',
+ },
+ });
+ console.log('Onboarding - User created:', user);
+ }
+ } else {
+ console.log('Onboarding - User found in DB:', user);
+ }
+
+ // Check if user already has membership
+ const membership = await prisma.membership.findFirst({
+ where: { userId: user.id },
+ });
+
+ if (membership) {
+ console.log('Onboarding - User already has membership, redirecting');
+ // User already has membership, redirect to their org
+ redirect(`/${membership.organizationId}/projects`);
+ }
+
+ console.log('Onboarding - Creating organization and membership...');
+
+ // Create default organization and membership for the user
+ const org = await prisma.organization.create({
+ data: {
+ name: `${user.name || 'My'}'s Organization`,
+ },
+ });
+
+ console.log('Onboarding - Organization created:', org);
+
+ const newMembership = await prisma.membership.create({
+ data: {
+ userId: user.id,
+ organizationId: org.id,
+ role: 'ADMIN',
+ },
+ });
+
+ console.log('Onboarding - Membership created:', newMembership);
+
+ // Redirect to the new organization
+ redirect(`/${org.id}/projects`);
+ } catch (error) {
+ console.error('Onboarding error:', error);
+ return (
+
+
+
🎯 Welcome to AiM!
+
+
+
+
❌ Setup Error
+
+ {error instanceof Error ? error.message : 'Unknown error occurred'}
+
+
+
+
+
🔍 Debug Info
+
+ Check the server console for detailed error information.
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/page.tsx b/app/page.tsx
index 62fccc7e..9527b67b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,5 +1,77 @@
import { redirect } from 'next/navigation';
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
-export default function Home() {
- redirect('lndev-ui/team/CORE/all');
+export default async function Home() {
+ try {
+ const session = await auth();
+ console.log('Home page - Session:', session);
+
+ if (!session?.user?.id) {
+ console.log('Home page - No session, redirecting to signin');
+ redirect('/auth/signin');
+ }
+
+ // Check if user has membership by session user ID first
+ let membership = await prisma.membership.findFirst({
+ where: { userId: session.user.id },
+ });
+
+ // If no membership found by session ID, check by email
+ if (!membership && session.user.email) {
+ console.log('Home page - No membership by ID, checking by email...');
+ const userByEmail = await prisma.user.findUnique({
+ where: { email: session.user.email },
+ });
+
+ if (userByEmail) {
+ console.log('Home page - User found by email:', userByEmail);
+ membership = await prisma.membership.findFirst({
+ where: { userId: userByEmail.id },
+ });
+ }
+ }
+
+ console.log('Home page - Membership:', membership);
+
+ if (!membership) {
+ console.log('Home page - No membership, redirecting to onboarding');
+ redirect('/onboarding');
+ }
+
+ // User has membership, redirect to their organization
+ const redirectUrl = `/${membership.organizationId}/projects`;
+ console.log('Home page - Redirecting to:', redirectUrl);
+ redirect(redirectUrl);
+ } catch (error) {
+ // Don't log NEXT_REDIRECT errors as they are expected
+ if (error instanceof Error && error.message.includes('NEXT_REDIRECT')) {
+ // This is expected behavior, not an error
+ throw error; // Re-throw to let Next.js handle the redirect
+ }
+
+ console.error('Home page - Actual error:', error);
+ return (
+
+
+
❌ Error Loading Page
+
+
+
+
Error Details
+
+ {error instanceof Error ? error.message : 'Unknown error'}
+
+
+ Try Again
+
+
+
+
+
+ );
+ }
}
diff --git a/app/test/page.tsx b/app/test/page.tsx
new file mode 100644
index 00000000..f09d843b
--- /dev/null
+++ b/app/test/page.tsx
@@ -0,0 +1,33 @@
+export default function TestPage() {
+ return (
+
+
+
Test Page
+
+ This page bypasses authentication to test if the app works.
+
+
+
+
✅ App is Working
+
The basic Next.js app is functioning correctly.
+
+
+
🔐 Authentication Issue
+
+ The problem is likely in the authentication flow or redirects.
+
+
+
+
📝 Next Steps
+
Check the browser console for error messages.
+
+
+
+
+
+ );
+}
diff --git a/components/__tests__/admin-dashboard.test.tsx b/components/__tests__/admin-dashboard.test.tsx
new file mode 100644
index 00000000..35439d83
--- /dev/null
+++ b/components/__tests__/admin-dashboard.test.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { AdminDashboard } from '../dashboards/admin-dashboard';
+
+describe('AdminDashboard', () => {
+ const mockOrgId = 'test-org-123';
+
+ it('renders the dashboard title', () => {
+ render( );
+ expect(screen.getByText('Admin Dashboard')).toBeInTheDocument();
+ });
+
+ it('renders organization statistics', () => {
+ render( );
+
+ expect(screen.getByText('Total Users')).toBeInTheDocument();
+ expect(screen.getByText('45')).toBeInTheDocument(); // totalUsers
+ expect(screen.getByText('38 active')).toBeInTheDocument(); // activeUsers
+
+ expect(screen.getByText('Active Campaigns')).toBeInTheDocument();
+ expect(screen.getByText('8')).toBeInTheDocument(); // activeCampaigns
+ expect(screen.getByText('of 12 total')).toBeInTheDocument(); // totalCampaigns
+
+ expect(screen.getByText('Content Pieces')).toBeInTheDocument();
+ expect(screen.getByText('156')).toBeInTheDocument(); // totalContent
+
+ expect(screen.getByText('Pending Approvals')).toBeInTheDocument();
+ expect(screen.getByText('23')).toBeInTheDocument(); // pendingApprovals
+ });
+
+ it('renders tabs', () => {
+ render( );
+
+ expect(screen.getByText('User Management')).toBeInTheDocument();
+ expect(screen.getByText('Organization')).toBeInTheDocument();
+ expect(screen.getByText('System')).toBeInTheDocument();
+ });
+
+ it('renders system settings button', () => {
+ render( );
+
+ expect(screen.getByText('System Settings')).toBeInTheDocument();
+ });
+});
diff --git a/components/analytics/analytics-charts.tsx b/components/analytics/analytics-charts.tsx
new file mode 100644
index 00000000..a98da55a
--- /dev/null
+++ b/components/analytics/analytics-charts.tsx
@@ -0,0 +1,149 @@
+'use client';
+
+import React from 'react';
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+ PieChart,
+ Pie,
+ Cell,
+ LineChart,
+ Line,
+} from 'recharts';
+
+interface AnalyticsChartsProps {
+ metrics: {
+ totalEvents: number;
+ eventsByType: Record;
+ impressions: number;
+ clicks: number;
+ views: number;
+ ctr: number;
+ roi: number;
+ campaignMetrics: Array<{
+ id: string;
+ name: string;
+ totalEvents: number;
+ contentCount: number;
+ contentMetrics: Array<{
+ id: string;
+ title: string;
+ events: number;
+ impressions: number;
+ clicks: number;
+ views: number;
+ }>;
+ }>;
+ };
+}
+
+const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8'];
+
+export function AnalyticsCharts({ metrics }: AnalyticsChartsProps) {
+ // Prepare data for event types pie chart
+ const eventTypesData = Object.entries(metrics.eventsByType).map(([type, count]) => ({
+ name: type.charAt(0).toUpperCase() + type.slice(1),
+ value: count,
+ }));
+
+ // Prepare data for campaign performance bar chart
+ const campaignData = metrics.campaignMetrics.map((campaign) => ({
+ name: campaign.name.length > 15 ? campaign.name.substring(0, 15) + '...' : campaign.name,
+ events: campaign.totalEvents,
+ content: campaign.contentCount,
+ }));
+
+ // Prepare data for performance metrics line chart
+ const performanceData = [
+ { name: 'Impressions', value: metrics.impressions },
+ { name: 'Clicks', value: metrics.clicks },
+ { name: 'Views', value: metrics.views },
+ ];
+
+ return (
+
+ {/* Event Types Distribution */}
+
+
Event Types Distribution
+
+
+ `${name} ${(percent * 100).toFixed(0)}%`}
+ outerRadius={80}
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {eventTypesData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+ {/* Campaign Performance */}
+
+
Campaign Performance
+
+
+
+
+
+
+
+
+
+
+
+ {/* Performance Metrics */}
+
+
Key Performance Metrics
+
+
+
+ {metrics.impressions.toLocaleString()}
+
+
Impressions
+
+
+
+ {metrics.clicks.toLocaleString()}
+
+
Clicks
+
+
+
+ {metrics.ctr.toFixed(2)}%
+
+
CTR
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/analytics/analytics-example.tsx b/components/analytics/analytics-example.tsx
new file mode 100644
index 00000000..b38adfcf
--- /dev/null
+++ b/components/analytics/analytics-example.tsx
@@ -0,0 +1,119 @@
+'use client';
+
+import React from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { useAnalytics } from '@/hooks/use-analytics';
+import { Eye, MousePointer, ThumbsUp, Share2 } from 'lucide-react';
+
+interface AnalyticsExampleProps {
+ orgId: string;
+ campaignId?: string;
+ contentId?: string;
+}
+
+export function AnalyticsExample({ orgId, campaignId, contentId }: AnalyticsExampleProps) {
+ const { trackEvent } = useAnalytics(orgId);
+
+ const handleView = async () => {
+ try {
+ await trackEvent('view', campaignId, contentId, {
+ source: 'example_component',
+ timestamp: new Date().toISOString(),
+ });
+ console.log('View event tracked');
+ } catch (error) {
+ console.error('Failed to track view:', error);
+ }
+ };
+
+ const handleClick = async () => {
+ try {
+ await trackEvent('click', campaignId, contentId, {
+ element: 'example_button',
+ position: 'center',
+ });
+ console.log('Click event tracked');
+ } catch (error) {
+ console.error('Failed to track click:', error);
+ }
+ };
+
+ const handleLike = async () => {
+ try {
+ await trackEvent('like', campaignId, contentId, {
+ reaction_type: 'thumbs_up',
+ });
+ console.log('Like event tracked');
+ } catch (error) {
+ console.error('Failed to track like:', error);
+ }
+ };
+
+ const handleShare = async () => {
+ try {
+ await trackEvent('share', campaignId, contentId, {
+ platform: 'social_media',
+ });
+ console.log('Share event tracked');
+ } catch (error) {
+ console.error('Failed to track share:', error);
+ }
+ };
+
+ return (
+
+
+ Analytics Event Tracking Example
+
+ Click buttons to track different types of analytics events
+
+
+
+
+
+
+ Track View
+
+
+
+ Track Click
+
+
+
+ Track Like
+
+
+
+ Track Share
+
+
+
+
+
+ Campaign ID: {campaignId || 'Not set'}
+
+
+ Content ID: {contentId || 'Not set'}
+
+
+ Organization ID: {orgId}
+
+
+
+
+
+ Usage:
+
+
+ {`const { trackEvent } = useAnalytics(orgId);
+
+await trackEvent('event_type', campaignId, contentId, {
+ custom_data: 'value'
+});`}
+
+
+
+
+ );
+}
diff --git a/components/assets/asset-library.tsx b/components/assets/asset-library.tsx
new file mode 100644
index 00000000..e0d301b4
--- /dev/null
+++ b/components/assets/asset-library.tsx
@@ -0,0 +1,312 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useSession } from 'next-auth/react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Input } from '@/components/ui/input';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { Upload, Search, Image, Video, FileText, Music, Trash2, Eye } from 'lucide-react';
+import { AssetUpload } from './asset-upload';
+import { AssetPreview } from './asset-preview';
+
+interface Asset {
+ id: string;
+ url: string;
+ name?: string;
+ type: string;
+ size?: number;
+ description?: string;
+ tags?: string[];
+ createdAt: string;
+ content: {
+ id: string;
+ title: string;
+ };
+}
+
+interface AssetLibraryProps {
+ orgId: string;
+ contentId?: string;
+ onAssetSelect?: (asset: Asset) => void;
+}
+
+export function AssetLibrary({ orgId, contentId, onAssetSelect }: AssetLibraryProps) {
+ const { data: session } = useSession();
+ const [assets, setAssets] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedType, setSelectedType] = useState('all');
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [sortBy, setSortBy] = useState('newest');
+ const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
+ const [previewAsset, setPreviewAsset] = useState(null);
+
+ const fetchAssets = async () => {
+ try {
+ const params = new URLSearchParams();
+ if (contentId) params.append('contentId', contentId);
+
+ const response = await fetch(`/api/${orgId}/assets?${params}`);
+ if (response.ok) {
+ const data = await response.json();
+ setAssets(data);
+ }
+ } catch (error) {
+ console.error('Error fetching assets:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (session?.user) {
+ fetchAssets();
+ }
+ }, [session, orgId, contentId]);
+
+ const filteredAssets = assets
+ .filter((asset) => {
+ const matchesSearch =
+ !searchTerm ||
+ asset.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ asset.content.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ asset.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ asset.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ asset.tags?.some((tag) => tag.toLowerCase().includes(searchTerm.toLowerCase()));
+
+ const matchesType = selectedType === 'all' || asset.type.startsWith(selectedType);
+ const matchesTags =
+ selectedTags.length === 0 || selectedTags.every((tag) => asset.tags?.includes(tag));
+
+ return matchesSearch && matchesType && matchesTags;
+ })
+ .sort((a, b) => {
+ switch (sortBy) {
+ case 'newest':
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
+ case 'oldest':
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
+ case 'name':
+ return (a.name || a.content.title).localeCompare(b.name || b.content.title);
+ case 'size':
+ return (b.size || 0) - (a.size || 0);
+ default:
+ return 0;
+ }
+ });
+
+ const getAssetIcon = (type: string) => {
+ if (type.startsWith('image/')) return ;
+ if (type.startsWith('video/')) return ;
+ if (type.startsWith('audio/')) return ;
+ if (type === 'application/pdf') return ;
+ return ;
+ };
+
+ const getTypeBadgeColor = (type: string) => {
+ if (type.startsWith('image/')) return 'bg-green-100 text-green-800';
+ if (type.startsWith('video/')) return 'bg-blue-100 text-blue-800';
+ if (type.startsWith('audio/')) return 'bg-purple-100 text-purple-800';
+ return 'bg-gray-100 text-gray-800';
+ };
+
+ const handleAssetUpload = () => {
+ setUploadDialogOpen(false);
+ fetchAssets();
+ };
+
+ const handleDeleteAsset = async (assetId: string) => {
+ if (!confirm('Are you sure you want to delete this asset?')) return;
+
+ try {
+ const response = await fetch(`/api/${orgId}/assets/${assetId}`, {
+ method: 'DELETE',
+ });
+
+ if (response.ok) {
+ setAssets(assets.filter((asset) => asset.id !== assetId));
+ }
+ } catch (error) {
+ console.error('Error deleting asset:', error);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Asset Library
+
+
+
+
+ Upload Asset
+
+
+
+
+ Upload New Asset
+
+
+
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
setSelectedType(e.target.value)}
+ className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
+ >
+ All Types
+ Images
+ Videos
+ Audio
+ PDFs
+
+
setSortBy(e.target.value)}
+ className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
+ >
+ Newest First
+ Oldest First
+ Name A-Z
+ Largest First
+
+
+
+ {/* Tag filters */}
+
+ {Array.from(new Set(assets.flatMap((asset) => asset.tags || []))).map((tag) => (
+ {
+ setSelectedTags((prev) =>
+ prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
+ );
+ }}
+ className={`px-3 py-1 text-sm rounded-full border ${
+ selectedTags.includes(tag)
+ ? 'bg-primary text-primary-foreground border-primary'
+ : 'bg-gray-100 text-gray-700 border-gray-300 hover:bg-gray-200'
+ }`}
+ >
+ {tag}
+
+ ))}
+
+
+
+
+ {filteredAssets.map((asset) => (
+
+
+
+
+ {getAssetIcon(asset.type)}
+
+ {asset.type.split('/')[0]}
+
+
+
+ setPreviewAsset(asset)}>
+
+
+ handleDeleteAsset(asset.id)}
+ >
+
+
+
+
+
+
+
+
+ {asset.name || asset.content.title}
+
+ {asset.description && (
+
{asset.description}
+ )}
+
+ {asset.tags?.slice(0, 2).map((tag) => (
+
+ {tag}
+
+ ))}
+ {asset.tags && asset.tags.length > 2 && (
+
+ +{asset.tags.length - 2}
+
+ )}
+
+
+ {new Date(asset.createdAt).toLocaleDateString()}
+ {asset.size && {(asset.size / 1024 / 1024).toFixed(1)} MB }
+
+ {onAssetSelect && (
+
onAssetSelect(asset)}
+ >
+ Select
+
+ )}
+
+
+
+ ))}
+
+
+ {filteredAssets.length === 0 && (
+
+
+
No assets found
+
+ {searchTerm || selectedType !== 'all'
+ ? 'Try adjusting your search or filter criteria.'
+ : 'Upload your first asset to get started.'}
+
+
+ )}
+
+ {previewAsset && (
+
setPreviewAsset(null)} />
+ )}
+
+ );
+}
diff --git a/components/assets/asset-preview.tsx b/components/assets/asset-preview.tsx
new file mode 100644
index 00000000..48db5230
--- /dev/null
+++ b/components/assets/asset-preview.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Download, ExternalLink, Image, Video, FileText, Music } from 'lucide-react';
+
+interface Asset {
+ id: string;
+ url: string;
+ type: string;
+ createdAt: string;
+ content: {
+ id: string;
+ title: string;
+ };
+}
+
+interface AssetPreviewProps {
+ asset: Asset | null;
+ onClose: () => void;
+}
+
+export function AssetPreview({ asset, onClose }: AssetPreviewProps) {
+ if (!asset) return null;
+
+ const getAssetIcon = (type: string) => {
+ if (type.startsWith('image/')) return ;
+ if (type.startsWith('video/')) return ;
+ if (type.startsWith('audio/')) return ;
+ if (type === 'application/pdf') return ;
+ return ;
+ };
+
+ const getTypeBadgeColor = (type: string) => {
+ if (type.startsWith('image/')) return 'bg-green-100 text-green-800';
+ if (type.startsWith('video/')) return 'bg-blue-100 text-blue-800';
+ if (type.startsWith('audio/')) return 'bg-purple-100 text-purple-800';
+ return 'bg-gray-100 text-gray-800';
+ };
+
+ const renderAssetContent = () => {
+ if (asset.type.startsWith('image/')) {
+ return (
+
+ );
+ }
+
+ if (asset.type.startsWith('video/')) {
+ return ;
+ }
+
+ if (asset.type.startsWith('audio/')) {
+ return ;
+ }
+
+ if (asset.type === 'application/pdf') {
+ return (
+
+ );
+ }
+
+ return (
+
+ {getAssetIcon(asset.type)}
+
Preview not available for this file type
+
+ );
+ };
+
+ const handleDownload = () => {
+ const link = document.createElement('a');
+ link.href = asset.url;
+ link.download = asset.content.title;
+ link.click();
+ };
+
+ const handleOpenInNewTab = () => {
+ window.open(asset.url, '_blank');
+ };
+
+ return (
+ onClose()}>
+
+
+
+ {asset.content.title}
+
+ {asset.type.split('/')[0]}
+
+
+
+
+
+
{renderAssetContent()}
+
+
+
+
+ Type: {asset.type}
+
+
+ Uploaded: {new Date(asset.createdAt).toLocaleString()}
+
+
+
+
+
+ Download
+
+
+
+ Open
+
+
+
+
+
+
+ );
+}
diff --git a/components/assets/asset-upload.tsx b/components/assets/asset-upload.tsx
new file mode 100644
index 00000000..2b9edd49
--- /dev/null
+++ b/components/assets/asset-upload.tsx
@@ -0,0 +1,121 @@
+'use client';
+
+import { useState } from 'react';
+import { useSession } from 'next-auth/react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Upload, X } from 'lucide-react';
+
+interface AssetUploadProps {
+ orgId: string;
+ contentId?: string;
+ onUploadComplete: () => void;
+}
+
+export function AssetUpload({ orgId, contentId, onUploadComplete }: AssetUploadProps) {
+ const { data: session } = useSession();
+ const [file, setFile] = useState(null);
+ const [uploading, setUploading] = useState(false);
+ const [selectedContentId, setSelectedContentId] = useState(contentId || '');
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const selectedFile = e.target.files?.[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ }
+ };
+
+ const handleUpload = async () => {
+ if (!file || !selectedContentId) return;
+
+ setUploading(true);
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('contentId', selectedContentId);
+
+ const response = await fetch(`/api/${orgId}/assets/upload`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (response.ok) {
+ onUploadComplete();
+ setFile(null);
+ setSelectedContentId('');
+ } else {
+ const error = await response.json();
+ alert(`Upload failed: ${error.error}`);
+ }
+ } catch (error) {
+ console.error('Upload error:', error);
+ alert('Upload failed. Please try again.');
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ const removeFile = () => {
+ setFile(null);
+ };
+
+ return (
+
+
+ Select File
+
+
+
+ {file && (
+
+
+
+ {file.name}
+
+ ({(file.size / 1024 / 1024).toFixed(2)} MB)
+
+
+
+
+
+
+ )}
+
+
+ Associate with Content
+
+
+
+
+
+ {/* This would be populated with actual content from the API */}
+ Sample Content 1
+ Sample Content 2
+
+
+
+
+
+ {uploading ? 'Uploading...' : 'Upload Asset'}
+
+
+ );
+}
diff --git a/components/campaigns/analytics/campaign-analytics-page.tsx b/components/campaigns/analytics/campaign-analytics-page.tsx
new file mode 100644
index 00000000..41cebcb0
--- /dev/null
+++ b/components/campaigns/analytics/campaign-analytics-page.tsx
@@ -0,0 +1,541 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ TrendingUp,
+ Users,
+ CheckSquare,
+ FileText,
+ Calendar,
+ Target,
+ BarChart3,
+ PieChart,
+ Activity,
+ Clock,
+ AlertTriangle,
+ CheckCircle,
+ XCircle,
+} from 'lucide-react';
+import type { Campaign, CampaignTask, CampaignContent } from '@/types/campaign';
+
+interface CampaignAnalyticsPageProps {
+ orgId: string;
+ campaignId: string;
+ campaign: Campaign;
+}
+
+const statusConfig = {
+ DRAFT: { label: 'Draft', color: 'bg-gray-500', icon: '📝' },
+ PLANNING: { label: 'Planning', color: 'bg-yellow-500', icon: '📋' },
+ READY: { label: 'Ready', color: 'bg-green-500', icon: '✅' },
+ DONE: { label: 'Done', color: 'bg-blue-500', icon: '🎉' },
+ CANCELED: { label: 'Canceled', color: 'bg-red-500', icon: '❌' },
+};
+
+const healthConfig = {
+ ON_TRACK: { label: 'On Track', color: 'bg-green-500', icon: '🟢' },
+ AT_RISK: { label: 'At Risk', color: 'bg-yellow-500', icon: '🟡' },
+ OFF_TRACK: { label: 'Off Track', color: 'bg-red-500', icon: '🔴' },
+};
+
+const taskStatusConfig = {
+ TODO: { label: 'To Do', color: 'bg-gray-500', icon: '⏳' },
+ IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-500', icon: '🔄' },
+ REVIEW: { label: 'Review', color: 'bg-yellow-500', icon: '👀' },
+ DONE: { label: 'Done', color: 'bg-green-500', icon: '✅' },
+ CANCELLED: { label: 'Cancelled', color: 'bg-red-500', icon: '❌' },
+};
+
+const contentStatusConfig = {
+ DRAFT: { label: 'Draft', color: 'bg-gray-500', icon: '📝' },
+ SUBMITTED: { label: 'Submitted', color: 'bg-yellow-500', icon: '📤' },
+ APPROVED: { label: 'Approved', color: 'bg-green-500', icon: '✅' },
+ PUBLISHED: { label: 'Published', color: 'bg-blue-500', icon: '🌐' },
+ REJECTED: { label: 'Rejected', color: 'bg-red-500', icon: '❌' },
+};
+
+export function CampaignAnalyticsPage({ orgId, campaignId, campaign }: CampaignAnalyticsPageProps) {
+ const router = useRouter();
+ const [tasks, setTasks] = useState([]);
+ const [contents, setContents] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchAnalyticsData();
+ }, [orgId, campaignId]);
+
+ const fetchAnalyticsData = async () => {
+ try {
+ // Fetch tasks and contents for analytics
+ const [tasksResponse, contentsResponse] = await Promise.all([
+ fetch(`/api/${orgId}/campaigns/${campaignId}/tasks`),
+ fetch(`/api/${orgId}/campaigns/${campaignId}/contents`),
+ ]);
+
+ if (tasksResponse.ok) {
+ const tasksData = await tasksResponse.json();
+ setTasks(tasksData.tasks || []);
+ }
+
+ if (contentsResponse.ok) {
+ const contentsData = await contentsResponse.json();
+ setContents(contentsData.contents || []);
+ }
+ } catch (error) {
+ console.error('Error fetching analytics data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const calculateTaskProgress = () => {
+ if (tasks.length === 0) return 0;
+ const completedTasks = tasks.filter((task) => task.status === 'DONE').length;
+ return Math.round((completedTasks / tasks.length) * 100);
+ };
+
+ const calculateContentProgress = () => {
+ if (contents.length === 0) return 0;
+ const publishedContents = contents.filter((content) => content.status === 'PUBLISHED').length;
+ return Math.round((publishedContents / contents.length) * 100);
+ };
+
+ const getTaskStats = () => {
+ return {
+ total: tasks.length,
+ todo: tasks.filter((task) => task.status === 'TODO').length,
+ inProgress: tasks.filter((task) => task.status === 'IN_PROGRESS').length,
+ review: tasks.filter((task) => task.status === 'REVIEW').length,
+ done: tasks.filter((task) => task.status === 'DONE').length,
+ cancelled: tasks.filter((task) => task.status === 'CANCELLED').length,
+ };
+ };
+
+ const getContentStats = () => {
+ return {
+ total: contents.length,
+ draft: contents.filter((content) => content.status === 'DRAFT').length,
+ submitted: contents.filter((content) => content.status === 'SUBMITTED').length,
+ approved: contents.filter((content) => content.status === 'APPROVED').length,
+ published: contents.filter((content) => content.status === 'PUBLISHED').length,
+ rejected: contents.filter((content) => content.status === 'REJECTED').length,
+ };
+ };
+
+ const getTimelineProgress = () => {
+ if (!campaign.startDate || !campaign.endDate) return 0;
+
+ const now = new Date();
+ const start = new Date(campaign.startDate);
+ const end = new Date(campaign.endDate);
+
+ if (now < start) return 0;
+ if (now > end) return 100;
+
+ const totalDuration = end.getTime() - start.getTime();
+ const elapsed = now.getTime() - start.getTime();
+ return Math.round((elapsed / totalDuration) * 100);
+ };
+
+ const getOverdueTasks = () => {
+ const now = new Date();
+ return tasks.filter((task) => {
+ if (!task.dueDate) return false;
+ return new Date(task.dueDate) < now && task.status !== 'DONE';
+ });
+ };
+
+ const getUpcomingDeadlines = () => {
+ const now = new Date();
+ const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
+
+ return tasks
+ .filter((task) => {
+ if (!task.dueDate) return false;
+ const dueDate = new Date(task.dueDate);
+ return dueDate >= now && dueDate <= nextWeek && task.status !== 'DONE';
+ })
+ .sort((a, b) => new Date(a.dueDate!).getTime() - new Date(b.dueDate!).getTime());
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading analytics...
+
+
+ );
+ }
+
+ const taskStats = getTaskStats();
+ const contentStats = getContentStats();
+ const timelineProgress = getTimelineProgress();
+ const overdueTasks = getOverdueTasks();
+ const upcomingDeadlines = getUpcomingDeadlines();
+
+ return (
+
+ {/* Page Header */}
+
+
+
Campaign Analytics
+
+ Performance insights for: {campaign.title}
+
+
+
+ router.push(`/${orgId}/campaigns/${campaignId}`)}
+ >
+ Back to Campaign
+
+
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
Task Progress
+
{calculateTaskProgress()}%
+
+
+
+
+ {taskStats.done} of {taskStats.total} tasks completed
+
+
+
+
+
+
+
+
+
+
Content Progress
+
{calculateContentProgress()}%
+
+
+
+
+ {contentStats.published} of {contentStats.total} contents published
+
+
+
+
+
+
+
+
+
+
Timeline Progress
+
{timelineProgress}%
+
+
+
+ Campaign timeline progress
+
+
+
+
+
+
+
+
+
Team Members
+
{campaign.members?.length || 0}
+
+
+ Active team members
+
+
+
+
+ {/* Detailed Analytics */}
+
+
+ Overview
+ Task Analytics
+ Content Analytics
+ Timeline
+ Alerts
+
+
+
+
+
+
+ Campaign Status
+
+
+
+ Status
+
+ {statusConfig[campaign.status].icon}{' '}
+ {statusConfig[campaign.status].label}
+
+
+
+ Health
+
+ {healthConfig[campaign.health].icon}{' '}
+ {healthConfig[campaign.health].label}
+
+
+
+ Priority
+
+ {campaign.priority === 'NO_PRIORITY'
+ ? 'No Priority'
+ : campaign.priority}
+
+
+
+
+
+
+
+ Quick Stats
+
+
+
+ Total Tasks
+ {taskStats.total}
+
+
+ Total Content
+ {contentStats.total}
+
+
+ Overdue Tasks
+ {overdueTasks.length}
+
+
+ Upcoming Deadlines
+
+ {upcomingDeadlines.length}
+
+
+
+
+
+
+
+
+
+
+ Task Status Distribution
+
+
+
+ {Object.entries(taskStatusConfig).map(([status, config]) => {
+ const count =
+ taskStats[status.toLowerCase() as keyof typeof taskStats] || 0;
+ const percentage =
+ taskStats.total > 0 ? Math.round((count / taskStats.total) * 100) : 0;
+
+ return (
+
+
+ {config.icon}
+ {config.label}
+ {count} tasks
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ Content Status Distribution
+
+
+
+ {Object.entries(contentStatusConfig).map(([status, config]) => {
+ const count =
+ contentStats[status.toLowerCase() as keyof typeof contentStats] || 0;
+ const percentage =
+ contentStats.total > 0
+ ? Math.round((count / contentStats.total) * 100)
+ : 0;
+
+ return (
+
+
+ {config.icon}
+ {config.label}
+ {count} items
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ Campaign Timeline
+
+
+
+ Start Date
+
+ {campaign.startDate
+ ? new Date(campaign.startDate).toLocaleDateString()
+ : 'Not set'}
+
+
+
+ End Date
+
+ {campaign.endDate
+ ? new Date(campaign.endDate).toLocaleDateString()
+ : 'Not set'}
+
+
+
+ Progress
+ {timelineProgress}%
+
+
+
+
+
+
+
+ Upcoming Deadlines
+
+
+ {upcomingDeadlines.length > 0 ? (
+
+ {upcomingDeadlines.slice(0, 5).map((task) => (
+
+ {task.title}
+
+ {new Date(task.dueDate!).toLocaleDateString()}
+
+
+ ))}
+
+ ) : (
+
+ No upcoming deadlines
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ Overdue Tasks
+
+
+
+ {overdueTasks.length > 0 ? (
+
+ {overdueTasks.map((task) => (
+
+ {task.title}
+
+ {Math.abs(
+ Math.ceil(
+ (new Date(task.dueDate!).getTime() -
+ new Date().getTime()) /
+ (1000 * 60 * 60 * 24)
+ )
+ )}
+ d overdue
+
+
+ ))}
+
+ ) : (
+
+
+
All tasks are on time!
+
+ )}
+
+
+
+
+
+
+
+ Recent Activity
+
+
+
+
+
+
+
Campaign created
+
+ {campaign.createdAt
+ ? new Date(campaign.createdAt).toLocaleDateString()
+ : 'Unknown'}
+
+
+ {campaign.updatedAt && (
+
+
+
Last updated
+
+ {new Date(campaign.updatedAt).toLocaleDateString()}
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/analytics/index.ts b/components/campaigns/analytics/index.ts
new file mode 100644
index 00000000..f54f462a
--- /dev/null
+++ b/components/campaigns/analytics/index.ts
@@ -0,0 +1 @@
+export { CampaignAnalyticsPage } from './campaign-analytics-page';
diff --git a/components/campaigns/campaign-card.tsx b/components/campaigns/campaign-card.tsx
new file mode 100644
index 00000000..a2b024cb
--- /dev/null
+++ b/components/campaigns/campaign-card.tsx
@@ -0,0 +1,83 @@
+'use client';
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Calendar, FileText, Clock, MoreHorizontal } from 'lucide-react';
+import Link from 'next/link';
+import { formatDistanceToNow } from 'date-fns';
+
+interface Campaign {
+ id: string;
+ name: string;
+ description?: string;
+ createdAt: string;
+ contents: any[];
+ schedules: any[];
+}
+
+interface CampaignCardProps {
+ campaign: Campaign;
+ orgId: string;
+}
+
+export function CampaignCard({ campaign, orgId }: CampaignCardProps) {
+ const contentCount = campaign.contents.length;
+ const scheduleCount = campaign.schedules.length;
+ const createdDate = new Date(campaign.createdAt);
+
+ return (
+
+
+
+
+
+ {campaign.name}
+
+ {campaign.description && (
+
+ {campaign.description}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {contentCount} content{contentCount !== 1 ? 's' : ''}
+
+
+
+
+
+ {scheduleCount} schedule{scheduleCount !== 1 ? 's' : ''}
+
+
+
+
+
+
+ Created {formatDistanceToNow(createdDate, { addSuffix: true })}
+
+
+
+
+
+ View Details
+
+
+
+ Add Content
+
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-creation-modal.tsx b/components/campaigns/campaign-creation-modal.tsx
new file mode 100644
index 00000000..3a1b50b0
--- /dev/null
+++ b/components/campaigns/campaign-creation-modal.tsx
@@ -0,0 +1,58 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Plus } from 'lucide-react';
+import CampaignForm from './campaign-form';
+import { CreateCampaignData, User } from '@/types/campaign';
+
+interface CampaignCreationModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSubmit: (data: CreateCampaignData) => void;
+ teams: User[];
+ members: User[];
+ loading?: boolean;
+}
+
+export default function CampaignCreationModal({
+ isOpen,
+ onClose,
+ onSubmit,
+ teams,
+ members,
+ loading = false,
+}: CampaignCreationModalProps) {
+ const handleSubmit = (data: CreateCampaignData) => {
+ onSubmit(data);
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+ Create New Campaign
+
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-date-selector.tsx b/components/campaigns/campaign-date-selector.tsx
new file mode 100644
index 00000000..e9651b6b
--- /dev/null
+++ b/components/campaigns/campaign-date-selector.tsx
@@ -0,0 +1,258 @@
+'use client';
+
+import { useState } from 'react';
+import { Calendar } from '@/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Calendar as CalendarIcon, X } from 'lucide-react';
+import { format, isValid, parse } from 'date-fns';
+
+interface CampaignDateSelectorProps {
+ startDate?: string;
+ endDate?: string;
+ onStartDateChange: (date: string | undefined) => void;
+ onEndDateChange: (date: string | undefined) => void;
+ disabled?: boolean;
+ className?: string;
+}
+
+export default function CampaignDateSelector({
+ startDate,
+ endDate,
+ onStartDateChange,
+ onEndDateChange,
+ disabled = false,
+ className = '',
+}: CampaignDateSelectorProps) {
+ const [startDateOpen, setStartDateOpen] = useState(false);
+ const [endDateOpen, setEndDateOpen] = useState(false);
+ const [startDateInput, setStartDateInput] = useState(startDate || '');
+ const [endDateInput, setEndDateInput] = useState(endDate || '');
+
+ const handleStartDateSelect = (date: Date | undefined) => {
+ if (date) {
+ const dateString = format(date, 'yyyy-MM-dd');
+ onStartDateChange(dateString);
+ setStartDateInput(dateString);
+ setStartDateOpen(false);
+ }
+ };
+
+ const handleEndDateSelect = (date: Date | undefined) => {
+ if (date) {
+ const dateString = format(date, 'yyyy-MM-dd');
+ onEndDateChange(dateString);
+ setEndDateInput(dateString);
+ setEndDateOpen(false);
+ }
+ };
+
+ const handleStartDateInputChange = (value: string) => {
+ setStartDateInput(value);
+ const date = parse(value, 'yyyy-MM-dd', new Date());
+ if (isValid(date)) {
+ onStartDateChange(value);
+ }
+ };
+
+ const handleEndDateInputChange = (value: string) => {
+ setEndDateInput(value);
+ const date = parse(value, 'yyyy-MM-dd', new Date());
+ if (isValid(date)) {
+ onEndDateChange(value);
+ }
+ };
+
+ const clearStartDate = () => {
+ onStartDateChange(undefined);
+ setStartDateInput('');
+ };
+
+ const clearEndDate = () => {
+ onEndDateChange(undefined);
+ setEndDateInput('');
+ };
+
+ const getDateDisplay = (dateString: string | undefined) => {
+ if (!dateString) return 'Select date';
+ try {
+ return format(new Date(dateString), 'MMM dd, yyyy');
+ } catch {
+ return 'Invalid date';
+ }
+ };
+
+ const isStartDateValid = startDate && isValid(new Date(startDate));
+ const isEndDateValid = endDate && isValid(new Date(endDate));
+ const hasDateConflict =
+ isStartDateValid && isEndDateValid && new Date(startDate) >= new Date(endDate);
+
+ return (
+
+
+
Campaign Timeline
+
+ Set the start and end dates for your campaign
+
+
+
+
+ {/* Start Date */}
+
+
Start Date
+
+
+
+
+ {getDateDisplay(startDate)}
+ {startDate && (
+ {
+ e.stopPropagation();
+ clearStartDate();
+ }}
+ className="ml-auto h-6 w-6 p-0 hover:bg-transparent"
+ >
+
+
+ )}
+
+
+
+ {
+ // Disable past dates
+ return date < new Date(new Date().setHours(0, 0, 0, 0));
+ }}
+ className="rounded-md border"
+ />
+
+
+
+ {/* Manual Input */}
+
handleStartDateInputChange(e.target.value)}
+ disabled={disabled}
+ className="text-sm"
+ />
+
+
+ {/* End Date */}
+
+
End Date
+
+
+
+
+ {getDateDisplay(endDate)}
+ {endDate && (
+ {
+ e.stopPropagation();
+ clearEndDate();
+ }}
+ className="ml-auto h-6 w-6 p-0 hover:bg-transparent"
+ >
+
+
+ )}
+
+
+
+ {
+ // Disable dates before start date
+ if (startDate) {
+ return date <= new Date(startDate);
+ }
+ // Disable past dates
+ return date < new Date(new Date().setHours(0, 0, 0, 0));
+ }}
+ className="rounded-md border"
+ />
+
+
+
+ {/* Manual Input */}
+
handleEndDateInputChange(e.target.value)}
+ disabled={disabled}
+ className="text-sm"
+ />
+
+
+
+ {/* Date Conflict Warning */}
+ {hasDateConflict && (
+
+
⚠️ End date must be after start date
+
+ )}
+
+ {/* Date Range Display */}
+ {isStartDateValid && isEndDateValid && !hasDateConflict && (
+
+
+ Campaign duration:{' '}
+
+ {format(new Date(startDate), 'MMM dd, yyyy')} -{' '}
+ {format(new Date(endDate), 'MMM dd, yyyy')}
+
+
+
+ (
+ {Math.ceil(
+ (new Date(endDate).getTime() - new Date(startDate).getTime()) /
+ (1000 * 60 * 60 * 24)
+ )}{' '}
+ days)
+
+
+
+ )}
+
+ {/* Clear All Dates */}
+ {(startDate || endDate) && (
+
{
+ clearStartDate();
+ clearEndDate();
+ }}
+ disabled={disabled}
+ className="w-full"
+ >
+
+ Clear All Dates
+
+ )}
+
+ );
+}
diff --git a/components/campaigns/campaign-detail-wrapper.tsx b/components/campaigns/campaign-detail-wrapper.tsx
new file mode 100644
index 00000000..c1c3661a
--- /dev/null
+++ b/components/campaigns/campaign-detail-wrapper.tsx
@@ -0,0 +1,196 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'sonner';
+import { CampaignDetail } from './campaign-detail';
+import type {
+ Campaign,
+ UpdateCampaignData,
+ AddMemberData,
+ CreateTaskData,
+ UpdateTaskData,
+} from '@/types/campaign';
+
+interface CampaignDetailWrapperProps {
+ campaign: Campaign;
+}
+
+export function CampaignDetailWrapper({ campaign }: CampaignDetailWrapperProps) {
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+
+ const handleUpdate = async (data: UpdateCampaignData) => {
+ setLoading(true);
+ try {
+ const response = await fetch(`/api/${campaign.organizationId}/campaigns/${campaign.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update campaign');
+ }
+
+ toast.success('Campaign updated successfully');
+ router.refresh();
+ } catch (error) {
+ console.error('Error updating campaign:', error);
+ toast.error('Failed to update campaign');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async () => {
+ if (
+ !confirm('Are you sure you want to delete this campaign? This action cannot be undone.')
+ ) {
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const response = await fetch(`/api/${campaign.organizationId}/campaigns/${campaign.id}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete campaign');
+ }
+
+ toast.success('Campaign deleted successfully');
+ router.push(`/${campaign.organizationId}/campaigns`);
+ } catch (error) {
+ console.error('Error deleting campaign:', error);
+ toast.error('Failed to delete campaign');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleBack = () => {
+ router.push(`/${campaign.organizationId}/campaigns`);
+ };
+
+ const handleEdit = () => {
+ // TODO: Implement edit mode
+ toast.info('Edit mode coming soon');
+ };
+
+ const handleAddMember = async (data: AddMemberData) => {
+ setLoading(true);
+ try {
+ const response = await fetch(
+ `/api/${campaign.organizationId}/campaigns/${campaign.id}/members`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to add member');
+ }
+
+ toast.success('Member added successfully');
+ router.refresh();
+ } catch (error) {
+ console.error('Error adding member:', error);
+ toast.error('Failed to add member');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRemoveMember = async (memberId: string) => {
+ if (!confirm('Are you sure you want to remove this member?')) {
+ return;
+ }
+
+ setLoading(true);
+ try {
+ // TODO: Implement remove member API
+ toast.info('Remove member functionality coming soon');
+ } catch (error) {
+ console.error('Error removing member:', error);
+ toast.error('Failed to remove member');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCreateTask = async (data: CreateTaskData) => {
+ setLoading(true);
+ try {
+ const response = await fetch(
+ `/api/${campaign.organizationId}/campaigns/${campaign.id}/tasks`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to create task');
+ }
+
+ toast.success('Task created successfully');
+ router.refresh();
+ } catch (error) {
+ console.error('Error creating task:', error);
+ toast.error('Failed to create task');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleUpdateTask = async (taskId: string, data: UpdateTaskData) => {
+ setLoading(true);
+ try {
+ // TODO: Implement update task API
+ toast.info('Update task functionality coming soon');
+ } catch (error) {
+ console.error('Error updating task:', error);
+ toast.error('Failed to update task');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDeleteTask = async (taskId: string) => {
+ if (!confirm('Are you sure you want to delete this task?')) {
+ return;
+ }
+
+ setLoading(true);
+ try {
+ // TODO: Implement delete task API
+ toast.info('Delete task functionality coming soon');
+ } catch (error) {
+ console.error('Error deleting task:', error);
+ toast.error('Failed to delete task');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/components/campaigns/campaign-detail.tsx b/components/campaigns/campaign-detail.tsx
new file mode 100644
index 00000000..d048f7e2
--- /dev/null
+++ b/components/campaigns/campaign-detail.tsx
@@ -0,0 +1,778 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ ArrowLeft,
+ Edit,
+ MoreHorizontal,
+ Users,
+ CheckSquare,
+ Calendar,
+ Target,
+ TrendingUp,
+ Settings,
+ Plus,
+ FileText,
+ BarChart3,
+ PieChart,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { Separator } from '@/components/ui/separator';
+import type {
+ Campaign,
+ CampaignTask,
+ CampaignMember,
+ UpdateCampaignData,
+ CreateTaskData,
+ UpdateTaskData,
+ AddMemberData,
+} from '@/types/campaign';
+
+interface CampaignDetailProps {
+ campaign: Campaign;
+ onUpdate: (data: UpdateCampaignData) => void;
+ onDelete: () => void;
+ onBack: () => void;
+ onEdit: () => void;
+ onAddMember: (data: AddMemberData) => void;
+ onRemoveMember: (memberId: string) => void;
+ onCreateTask: (data: CreateTaskData) => void;
+ onUpdateTask: (taskId: string, data: UpdateTaskData) => void;
+ onDeleteTask: (taskId: string) => void;
+ loading?: boolean;
+}
+
+const statusConfig = {
+ DRAFT: { label: 'Draft', color: 'bg-gray-500' },
+ PLANNING: { label: 'Planning', color: 'bg-yellow-500' },
+ READY: { label: 'Ready', color: 'bg-green-500' },
+ DONE: { label: 'Done', color: 'bg-blue-500' },
+ CANCELED: { label: 'Canceled', color: 'bg-red-500' },
+};
+
+const healthConfig = {
+ ON_TRACK: { label: 'On Track', color: 'bg-green-500', icon: '🟢' },
+ AT_RISK: { label: 'At Risk', color: 'bg-yellow-500', icon: '🟡' },
+ OFF_TRACK: { label: 'Off Track', color: 'bg-red-500', icon: '🔴' },
+};
+
+const priorityConfig = {
+ NO_PRIORITY: { label: 'No Priority', color: 'bg-gray-400' },
+ LOW: { label: 'Low', color: 'bg-green-400' },
+ MEDIUM: { label: 'Medium', color: 'bg-yellow-400' },
+ HIGH: { label: 'High', color: 'bg-orange-400' },
+ URGENT: { label: 'Urgent', color: 'bg-red-500' },
+};
+
+const taskStatusConfig = {
+ TODO: { label: 'To Do', color: 'bg-gray-500', icon: '⏳' },
+ IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-500', icon: '🔄' },
+ REVIEW: { label: 'Review', color: 'bg-yellow-500', icon: '👀' },
+ DONE: { label: 'Done', color: 'bg-green-500', icon: '✅' },
+ CANCELLED: { label: 'Cancelled', color: 'bg-red-500', icon: '❌' },
+};
+
+export function CampaignDetail({
+ campaign,
+ onUpdate,
+ onDelete,
+ onBack,
+ onEdit,
+ onAddMember,
+ onRemoveMember,
+ onCreateTask,
+ onUpdateTask,
+ onDeleteTask,
+ loading = false,
+}: CampaignDetailProps) {
+ const [activeTab, setActiveTab] = useState('overview');
+
+ const formatDate = (date: Date | string | null) => {
+ if (!date) return 'Not set';
+ return new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ const calculateProgress = () => {
+ const tasks = campaign.tasks || [];
+ if (tasks.length === 0) return 0;
+ const completedTasks = tasks.filter((task) => task.status === 'DONE').length;
+ return Math.round((completedTasks / tasks.length) * 100);
+ };
+
+ const getTaskStats = () => {
+ const tasks = campaign.tasks || [];
+ return {
+ total: tasks.length,
+ todo: tasks.filter((task) => task.status === 'TODO').length,
+ inProgress: tasks.filter((task) => task.status === 'IN_PROGRESS').length,
+ review: tasks.filter((task) => task.status === 'REVIEW').length,
+ done: tasks.filter((task) => task.status === 'DONE').length,
+ cancelled: tasks.filter((task) => task.status === 'CANCELLED').length,
+ };
+ };
+
+ const getContentStats = () => {
+ const contents = campaign.contents || [];
+ return {
+ total: contents.length,
+ draft: contents.filter((content) => content.status === 'DRAFT').length,
+ submitted: contents.filter((content) => content.status === 'SUBMITTED').length,
+ approved: contents.filter((content) => content.status === 'APPROVED').length,
+ published: contents.filter((content) => content.status === 'PUBLISHED').length,
+ rejected: contents.filter((content) => content.status === 'REJECTED').length,
+ };
+ };
+
+ const renderStatusBadge = (status: Campaign['status']) => {
+ const config = statusConfig[status];
+ return {config.label} ;
+ };
+
+ const renderHealthIndicator = (health: Campaign['health']) => {
+ const config = healthConfig[health];
+ return (
+
+ {config.icon}
+ {config.label}
+
+ );
+ };
+
+ const renderPriorityBadge = (priority: Campaign['priority']) => {
+ if (priority === 'NO_PRIORITY')
+ return No Priority ;
+ const config = priorityConfig[priority];
+ return (
+
+ {config.label}
+
+ );
+ };
+
+ const renderTaskItem = (task: CampaignTask) => {
+ const config = taskStatusConfig[task.status];
+ return (
+
+
+
{config.icon}
+
+
{task.title}
+ {task.description && (
+
{task.description}
+ )}
+
+
+
+ {task.assignee && (
+
+
+
+ {task.assignee.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+ )}
+
+ {config.label}
+
+
+
+
+
+
+
+
+ onUpdateTask(task.id, { status: 'DONE' })}>
+ Mark as Done
+
+ onDeleteTask(task.id)}
+ className="text-destructive"
+ >
+ Delete Task
+
+
+
+
+
+ );
+ };
+
+ const renderTeamMember = (member: CampaignMember) => (
+
+
+
+
+ {member.user.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+
{member.user.name || 'Unknown'}
+
{member.user.email}
+
+
+
+ {member.role}
+
+
+
+
+
+
+
+ onRemoveMember(member.id)}
+ className="text-destructive"
+ >
+ Remove Member
+
+
+
+
+
+ );
+
+ const renderContentItem = (content: any) => (
+
+
+
+
+
{content.title}
+
+ {content.body ? `${content.body.substring(0, 100)}...` : 'No content'}
+
+
+
+
+
{content.status}
+
+ View
+
+
+
+ );
+
+ const taskStats = getTaskStats();
+ const contentStats = getContentStats();
+ const progress = calculateProgress();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Back to Campaigns
+
+
+
+
{campaign.name}
+
+ {renderHealthIndicator(campaign.health)}
+ •
+ {renderStatusBadge(campaign.status)}
+ •
+ {renderPriorityBadge(campaign.priority)}
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+ Edit Campaign
+
+ Delete Campaign
+
+
+
+
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
Progress
+
{progress}%
+
+
+
+
+
+
+
+
+
+
+
+
Tasks
+
{taskStats.total}
+
+
+
+
+
+
+
+
+
+
+
Content
+
{contentStats.total}
+
+
+
+
+
+
+
+
+
+
+
Team
+
{campaign.members?.length || 0}
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ Overview
+ Tasks ({taskStats.total})
+ Content ({contentStats.total})
+ Team ({campaign.members?.length || 0})
+ Analytics
+
+
+
+
+
+
+ 📋 Overview
+
+
+ {campaign.summary && (
+
+
Summary
+
{campaign.summary}
+
+ )}
+ {campaign.description && (
+
+
Description
+
{campaign.description}
+
+ )}
+
+
+ Status:
+ {renderStatusBadge(campaign.status)}
+
+
+ Health:
+ {renderHealthIndicator(campaign.health)}
+
+
+ Priority:
+ {renderPriorityBadge(campaign.priority)}
+
+
+ Start Date:
+ {formatDate(campaign.startDate)}
+
+
+ Target Date:
+ {formatDate(campaign.targetDate)}
+
+
+
+
+
+
+
+ 📊 Progress Overview
+
+
+
+
+ To Do
+ {taskStats.todo}
+
+
+ In Progress
+ {taskStats.inProgress}
+
+
+ Review
+ {taskStats.review}
+
+
+ Done
+ {taskStats.done}
+
+
+
+
+ Total Progress
+ {progress}%
+
+
+
+
+
+
+
+
+
+
📋 Tasks ({taskStats.total})
+
+
onCreateTask({ title: 'New Task', campaignId: campaign.id })}
+ >
+
+ Add Task
+
+
+
+ Manage Tasks
+
+
+
+
+
+ {campaign.tasks && campaign.tasks.length > 0 ? (
+ campaign.tasks.map(renderTaskItem)
+ ) : (
+
+
No tasks yet
+
+ Add tasks to track progress on this campaign
+
+
+ onCreateTask({ title: 'New Task', campaignId: campaign.id })
+ }
+ >
+
+ Add First Task
+
+
+ )}
+
+
+
+
+
+
📄 Content ({contentStats.total})
+
+
+
+ {/* Content Stats */}
+
+
+
+ {contentStats.draft}
+ Draft
+
+
+
+
+
+ {contentStats.submitted}
+
+ Submitted
+
+
+
+
+
+ {contentStats.approved}
+
+ Approved
+
+
+
+
+
+ {contentStats.published}
+
+ Published
+
+
+
+
+
+ {contentStats.rejected}
+
+ Rejected
+
+
+
+
+
+ {campaign.contents && campaign.contents.length > 0 ? (
+ campaign.contents.map(renderContentItem)
+ ) : (
+
+ )}
+
+
+
+
+
+
+ 👥 Team Members ({campaign.members?.length || 0})
+
+
+
onAddMember({ userId: '', role: 'MEMBER' })}>
+
+ Add Member
+
+
+
+ Manage Team
+
+
+
+
+
+ {campaign.lead && (
+
+
+
+
+
+ {campaign.lead.name?.slice(0, 2).toUpperCase() || 'L'}
+
+
+
+
+ {campaign.lead.name || 'Unknown'}
+ Lead
+
+
{campaign.lead.email}
+
+
+
+ )}
+ {campaign.members && campaign.members.length > 0 ? (
+ campaign.members.map(renderTeamMember)
+ ) : (
+
+
No team members yet
+
+ Add team members to collaborate on this campaign
+
+
onAddMember({ userId: '', role: 'MEMBER' })}>
+
+ Add First Member
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Task Progress
+
+
+
+
+
+
+
In Progress
+
+
+
{taskStats.inProgress}
+
+
+
+
+
+
+
+
+
+
+
+
+ Content Status
+
+
+
+
+
+
Draft
+
+
+
{contentStats.draft}
+
+
+
+
Submitted
+
+
+
+ {contentStats.submitted}
+
+
+
+
+
Approved
+
+
+
+ {contentStats.approved}
+
+
+
+
+
Published
+
+
+
+ {contentStats.published}
+
+
+
+
+
+
+
+
+
+
+
+
+ Campaign Timeline
+
+
+
+
+
+ Start Date
+ {formatDate(campaign.startDate)}
+
+
+ Target Date
+ {formatDate(campaign.targetDate)}
+
+
+ Days Remaining
+
+ {campaign.targetDate
+ ? Math.max(
+ 0,
+ Math.ceil(
+ (new Date(campaign.targetDate).getTime() -
+ new Date().getTime()) /
+ (1000 * 60 * 60 * 24)
+ )
+ )
+ : 'Not set'}{' '}
+ days
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-edit-form.tsx b/components/campaigns/campaign-edit-form.tsx
new file mode 100644
index 00000000..bf33edf4
--- /dev/null
+++ b/components/campaigns/campaign-edit-form.tsx
@@ -0,0 +1,69 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Campaign, UpdateCampaignData, User } from '@/types/campaign';
+import CampaignForm from './campaign-form';
+
+interface CampaignEditFormProps {
+ campaign: Campaign;
+ onSubmit: (data: UpdateCampaignData) => void;
+ onCancel: () => void;
+ teams: User[];
+ members: User[];
+ loading?: boolean;
+}
+
+export default function CampaignEditForm({
+ campaign,
+ onSubmit,
+ onCancel,
+ teams,
+ members,
+ loading = false,
+}: CampaignEditFormProps) {
+ const [initialData, setInitialData] = useState({});
+
+ useEffect(() => {
+ // Convert campaign data to form data
+ setInitialData({
+ name: campaign.name,
+ summary: campaign.summary || undefined,
+ description: campaign.description || undefined,
+ status: campaign.status,
+ health: campaign.health,
+ priority: campaign.priority,
+ leadId: campaign.leadId || undefined,
+ startDate: campaign.startDate
+ ? new Date(campaign.startDate).toISOString().split('T')[0]
+ : undefined,
+ targetDate: campaign.targetDate
+ ? new Date(campaign.targetDate).toISOString().split('T')[0]
+ : undefined,
+ });
+ }, [campaign]);
+
+ const handleSubmit = (data: UpdateCampaignData) => {
+ // Merge with existing data to preserve unchanged fields
+ const updatedData = { ...initialData, ...data };
+ onSubmit(updatedData);
+ };
+
+ return (
+
+
+
Edit Campaign
+
Update campaign details and settings
+
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-filters.tsx b/components/campaigns/campaign-filters.tsx
new file mode 100644
index 00000000..c72e0130
--- /dev/null
+++ b/components/campaigns/campaign-filters.tsx
@@ -0,0 +1,267 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ CampaignFilters,
+ CampaignStatus,
+ CampaignHealth,
+ CampaignPriority,
+} from '@/types/campaign';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Calendar } from '@/components/ui/calendar';
+import { Badge } from '@/components/ui/badge';
+import { Search, Filter, X, Calendar as CalendarIcon } from 'lucide-react';
+import { format } from 'date-fns';
+
+interface CampaignFiltersProps {
+ filters: CampaignFilters;
+ onChange: (filters: CampaignFilters) => void;
+ onClear: () => void;
+}
+
+export default function CampaignFilters({ filters, onChange, onClear }: CampaignFiltersProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleFilterChange = (key: keyof CampaignFilters, value: any) => {
+ onChange({ ...filters, [key]: value });
+ };
+
+ const handleClearFilter = (key: keyof CampaignFilters) => {
+ const newFilters = { ...filters };
+ delete newFilters[key];
+ onChange(newFilters);
+ };
+
+ const hasActiveFilters = Object.keys(filters).some(
+ (key) => filters[key as keyof CampaignFilters] !== undefined
+ );
+
+ return (
+
+ {/* Search */}
+
+
+ handleFilterChange('search', e.target.value)}
+ className="pl-10 w-64"
+ />
+ {filters.search && (
+ handleClearFilter('search')}
+ className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0"
+ >
+
+
+ )}
+
+
+ {/* Status Filter */}
+
handleFilterChange('status', value || undefined)}
+ >
+
+
+
+
+ All Status
+ Draft
+ Planning
+ Ready
+ Done
+ Canceled
+
+
+
+ {/* Health Filter */}
+
handleFilterChange('health', value || undefined)}
+ >
+
+
+
+
+ All Health
+ On Track
+ At Risk
+ Off Track
+
+
+
+ {/* Priority Filter */}
+
handleFilterChange('priority', value || undefined)}
+ >
+
+
+
+
+ All Priority
+ No Priority
+ Low
+ Medium
+ High
+ Urgent
+
+
+
+ {/* Date Range Filter */}
+
+
+
+
+ {filters.startDate || filters.endDate ? (
+
+ {filters.startDate && format(new Date(filters.startDate), 'MMM dd')}
+ {filters.startDate && filters.endDate && ' - '}
+ {filters.endDate && format(new Date(filters.endDate), 'MMM dd')}
+
+ ) : (
+ Date range
+ )}
+
+
+
+
+
+
+ Start Date
+
+ handleFilterChange(
+ 'startDate',
+ date ? format(date, 'yyyy-MM-dd') : undefined
+ )
+ }
+ className="rounded-md border"
+ />
+
+
+ End Date
+
+ handleFilterChange(
+ 'endDate',
+ date ? format(date, 'yyyy-MM-dd') : undefined
+ )
+ }
+ className="rounded-md border"
+ />
+
+
+
+ {
+ handleClearFilter('startDate');
+ handleClearFilter('endDate');
+ }}
+ >
+ Clear Dates
+
+
+
+
+
+
+ {/* Clear All Filters */}
+ {hasActiveFilters && (
+
+
+ Clear All
+
+ )}
+
+ {/* Active Filters Display */}
+ {hasActiveFilters && (
+
+ {filters.status && (
+
+ Status: {filters.status}
+ handleClearFilter('status')}
+ className="h-4 w-4 p-0 hover:bg-transparent"
+ >
+
+
+
+ )}
+ {filters.health && (
+
+ Health: {filters.health}
+ handleClearFilter('health')}
+ className="h-4 w-4 p-0 hover:bg-transparent"
+ >
+
+
+
+ )}
+ {filters.priority && (
+
+ Priority: {filters.priority}
+ handleClearFilter('priority')}
+ className="h-4 w-4 p-0 hover:bg-transparent"
+ >
+
+
+
+ )}
+ {filters.startDate && (
+
+ From: {format(new Date(filters.startDate), 'MMM dd')}
+ handleClearFilter('startDate')}
+ className="h-4 w-4 p-0 hover:bg-transparent"
+ >
+
+
+
+ )}
+ {filters.endDate && (
+
+ To: {format(new Date(filters.endDate), 'MMM dd')}
+ handleClearFilter('endDate')}
+ className="h-4 w-4 p-0 hover:bg-transparent"
+ >
+
+
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/components/campaigns/campaign-form.tsx b/components/campaigns/campaign-form.tsx
new file mode 100644
index 00000000..6258b4bd
--- /dev/null
+++ b/components/campaigns/campaign-form.tsx
@@ -0,0 +1,132 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useParams } from 'next/navigation';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { createCampaignSchema } from '@/lib/schemas';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { ArrowLeft, Save, Loader2 } from 'lucide-react';
+import Link from 'next/link';
+import { toast } from 'sonner';
+
+type CreateCampaignData = {
+ name: string;
+ description?: string;
+};
+
+export function CampaignForm() {
+ const params = useParams();
+ const router = useRouter();
+ const orgId = params.orgId as string;
+ const [loading, setLoading] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(createCampaignSchema),
+ defaultValues: {
+ name: '',
+ description: '',
+ },
+ });
+
+ const onSubmit = async (data: CreateCampaignData) => {
+ setLoading(true);
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+
+ if (response.ok) {
+ const campaign = await response.json();
+ toast.success('Campaign created successfully!');
+ router.push(`/${orgId}/campaigns/${campaign.id}`);
+ } else {
+ const error = await response.json();
+ toast.error(error.error || 'Failed to create campaign');
+ }
+ } catch (error) {
+ console.error('Error creating campaign:', error);
+ toast.error('An unexpected error occurred');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ Back to Campaigns
+
+
+
+
+
+
+ Campaign Details
+
+ Create a new marketing campaign to organize your content and schedules.
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-labels-selector.tsx b/components/campaigns/campaign-labels-selector.tsx
new file mode 100644
index 00000000..af60f5d6
--- /dev/null
+++ b/components/campaigns/campaign-labels-selector.tsx
@@ -0,0 +1,235 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Label } from '@/components/ui/label';
+import { X, Plus, Tag } from 'lucide-react';
+import { CampaignLabel, CreateLabelData } from '@/types/campaign';
+
+interface CampaignLabelsSelectorProps {
+ value: CampaignLabel[];
+ onChange: (labels: CampaignLabel[]) => void;
+ availableLabels?: CampaignLabel[];
+ onCreateLabel?: (data: CreateLabelData) => void;
+ disabled?: boolean;
+ className?: string;
+}
+
+const DEFAULT_COLORS = [
+ '#3B82F6', // Blue
+ '#EF4444', // Red
+ '#10B981', // Green
+ '#F59E0B', // Yellow
+ '#8B5CF6', // Purple
+ '#F97316', // Orange
+ '#06B6D4', // Cyan
+ '#84CC16', // Lime
+ '#EC4899', // Pink
+ '#6B7280', // Gray
+];
+
+export default function CampaignLabelsSelector({
+ value,
+ onChange,
+ availableLabels = [],
+ onCreateLabel,
+ disabled = false,
+ className = '',
+}: CampaignLabelsSelectorProps) {
+ const [isCreating, setIsCreating] = useState(false);
+ const [newLabelName, setNewLabelName] = useState('');
+ const [newLabelColor, setNewLabelColor] = useState(DEFAULT_COLORS[0]);
+ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
+
+ const allLabels = [...availableLabels, ...value];
+
+ const addLabel = (label: CampaignLabel) => {
+ if (!value.find((l) => l.id === label.id)) {
+ onChange([...value, label]);
+ }
+ };
+
+ const removeLabel = (labelId: string) => {
+ onChange(value.filter((label) => label.id !== labelId));
+ };
+
+ const createNewLabel = () => {
+ if (newLabelName.trim() && onCreateLabel) {
+ onCreateLabel({
+ name: newLabelName.trim(),
+ color: newLabelColor,
+ });
+ setNewLabelName('');
+ setNewLabelColor(DEFAULT_COLORS[0]);
+ setIsCreating(false);
+ }
+ };
+
+ const handleColorSelect = (color: string) => {
+ setNewLabelColor(color);
+ setIsColorPickerOpen(false);
+ };
+
+ return (
+
+
Labels
+
+ {/* Selected Labels */}
+
+ {value.map((label) => (
+
+
+ {label.name}
+ removeLabel(label.id)}
+ disabled={disabled}
+ className="h-4 w-4 p-0 ml-1 hover:bg-white/20"
+ >
+
+
+
+ ))}
+
+
+ {/* Add Labels */}
+
+ {!isCreating ? (
+
setIsCreating(true)}
+ disabled={disabled}
+ className="w-full"
+ >
+
+ Add Labels
+
+ ) : (
+
+
+
+ Create New Label
+
+
+
+
setNewLabelName(e.target.value)}
+ disabled={disabled}
+ className="text-sm"
+ />
+
+
+
+
+
+ Color
+
+
+
+
+ {DEFAULT_COLORS.map((color) => (
+ handleColorSelect(color)}
+ />
+ ))}
+
+
+ setNewLabelColor(e.target.value)}
+ className="w-full h-10"
+ />
+
+
+
+
+
+
+
+ Create
+
+ {
+ setIsCreating(false);
+ setNewLabelName('');
+ setNewLabelColor(DEFAULT_COLORS[0]);
+ }}
+ className="flex-1"
+ >
+ Cancel
+
+
+
+ )}
+
+
+ {/* Available Labels */}
+ {availableLabels.length > 0 && (
+
+
Available Labels
+
+ {availableLabels.map((label) => (
+
addLabel(label)}
+ disabled={disabled || value.find((l) => l.id === label.id)}
+ className="flex items-center gap-1 px-2 py-1 h-auto"
+ >
+
+ {label.name}
+ {value.find((l) => l.id === label.id) && (
+
+ Added
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* No Labels Message */}
+ {value.length === 0 && availableLabels.length === 0 && (
+
+ No labels available. Create your first label to get started.
+
+ )}
+
+ );
+}
diff --git a/components/campaigns/campaign-lead-selector.tsx b/components/campaigns/campaign-lead-selector.tsx
new file mode 100644
index 00000000..90d97ddf
--- /dev/null
+++ b/components/campaigns/campaign-lead-selector.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { User } from '@/types/campaign';
+
+interface CampaignLeadSelectorProps {
+ value?: string;
+ onChange: (leadId: string | undefined) => void;
+ members: User[];
+ disabled?: boolean;
+ className?: string;
+}
+
+export default function CampaignLeadSelector({
+ value,
+ onChange,
+ members,
+ disabled = false,
+ className = '',
+}: CampaignLeadSelectorProps) {
+ const selectedLead = members.find((member) => member.id === value);
+
+ const getInitials = (name: string | null) => {
+ if (!name) return '?';
+ return name
+ .split(' ')
+ .map((word) => word.charAt(0))
+ .join('')
+ .toUpperCase()
+ .slice(0, 2);
+ };
+
+ return (
+
+
Campaign Lead
+
onChange(newValue || undefined)}
+ disabled={disabled}
+ >
+
+
+ {selectedLead ? (
+
+
+
+
+ {getInitials(selectedLead.name)}
+
+
+
{selectedLead.name || selectedLead.email}
+
+ ) : (
+ Select a lead
+ )}
+
+
+
+
+
+
+ ?
+
+
No lead assigned
+
+
+ {members.map((member) => (
+
+
+
+
+
+ {getInitials(member.name)}
+
+
+
+ {member.name || 'Unknown'}
+ {member.email}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-list.tsx b/components/campaigns/campaign-list.tsx
new file mode 100644
index 00000000..42c0fd7a
--- /dev/null
+++ b/components/campaigns/campaign-list.tsx
@@ -0,0 +1,450 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Plus, Grid3X3, List, Filter, Search, MoreHorizontal } from 'lucide-react';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Input } from '@/components/ui/input';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Skeleton } from '@/components/ui/skeleton';
+import type { Campaign, CampaignFilters, PaginationMeta } from '@/types/campaign';
+
+interface CampaignListProps {
+ campaigns: Campaign[];
+ filters: CampaignFilters;
+ pagination: PaginationMeta;
+ onFilterChange: (filters: CampaignFilters) => void;
+ onPageChange: (page: number) => void;
+ onCreateCampaign: () => void;
+ onSelectCampaign: (campaign: Campaign) => void;
+ onEditCampaign: (campaign: Campaign) => void;
+ onDeleteCampaign: (campaign: Campaign) => void;
+ loading?: boolean;
+}
+
+const statusConfig = {
+ DRAFT: { label: 'Draft', color: 'bg-gray-500' },
+ PLANNING: { label: 'Planning', color: 'bg-yellow-500' },
+ READY: { label: 'Ready', color: 'bg-green-500' },
+ DONE: { label: 'Done', color: 'bg-blue-500' },
+ CANCELED: { label: 'Canceled', color: 'bg-red-500' },
+} as const;
+
+const healthConfig = {
+ ON_TRACK: { label: 'On Track', color: 'bg-green-500', icon: '🟢' },
+ AT_RISK: { label: 'At Risk', color: 'bg-yellow-500', icon: '🟡' },
+ OFF_TRACK: { label: 'Off Track', color: 'bg-red-500', icon: '🔴' },
+} as const;
+
+const priorityConfig = {
+ NO_PRIORITY: { label: 'No Priority', color: 'bg-gray-400' },
+ LOW: { label: 'Low', color: 'bg-green-400' },
+ MEDIUM: { label: 'Medium', color: 'bg-yellow-400' },
+ HIGH: { label: 'High', color: 'bg-orange-400' },
+ URGENT: { label: 'Urgent', color: 'bg-red-500' },
+} as const;
+
+export function CampaignList({
+ campaigns,
+ filters,
+ pagination,
+ onFilterChange,
+ onPageChange,
+ onCreateCampaign,
+ onSelectCampaign,
+ onEditCampaign,
+ onDeleteCampaign,
+ loading = false,
+}: CampaignListProps) {
+ const [searchQuery, setSearchQuery] = useState(filters.search || '');
+ const [viewMode, setViewMode] = useState<'table' | 'grid'>('table');
+
+ // Debounced search
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ onFilterChange({ ...filters, search: searchQuery });
+ }, 300);
+
+ return () => clearTimeout(timer);
+ }, [searchQuery]);
+
+ const handleStatusFilter = (status: string) => {
+ const newStatus = status === 'all' ? undefined : (status as Campaign['status']);
+ onFilterChange({ ...filters, status: newStatus });
+ };
+
+ const handleHealthFilter = (health: string) => {
+ const newHealth = health === 'all' ? undefined : (health as Campaign['health']);
+ onFilterChange({ ...filters, health: newHealth });
+ };
+
+ const handlePriorityFilter = (priority: string) => {
+ const newPriority = priority === 'all' ? undefined : (priority as Campaign['priority']);
+ onFilterChange({ ...filters, priority: newPriority });
+ };
+
+ const formatDate = (date: Date | string | null) => {
+ if (!date) return '--';
+ const d = new Date(date);
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+ };
+
+ const formatDateRange = (startDate: Date | string | null, endDate: Date | string | null) => {
+ const start = formatDate(startDate);
+ const end = formatDate(endDate);
+ if (start === '--' && end === '--') return '--';
+ return `${start} → ${end}`;
+ };
+
+ const renderStatusBadge = (status: Campaign['status']) => {
+ const config = statusConfig[status];
+ return (
+
+ {config.label}
+
+ );
+ };
+
+ const renderHealthIndicator = (health: Campaign['health']) => {
+ const config = healthConfig[health];
+ return (
+
+ {config.icon}
+ {config.label}
+
+ );
+ };
+
+ const renderPriorityBadge = (priority: Campaign['priority']) => {
+ if (priority === 'NO_PRIORITY') return null;
+ const config = priorityConfig[priority];
+ return (
+
+ {config.label}
+
+ );
+ };
+
+ const renderTeamMembers = (campaign: Campaign) => {
+ const members = campaign.members || [];
+ const visibleMembers = members.slice(0, 3);
+ const remainingCount = members.length - 3;
+
+ return (
+
+
👥
+
+ {visibleMembers.map((member) => (
+
+
+
+ {member.user.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+ ))}
+ {remainingCount > 0 && (
+
+ +{remainingCount}
+
+ )}
+
+ {campaign.lead && (
+ <>
+
,
+
+
+
+ {campaign.lead.name?.slice(0, 2).toUpperCase() || 'L'}
+
+
+ >
+ )}
+
+ );
+ };
+
+ const renderCampaignRow = (campaign: Campaign) => (
+ onSelectCampaign(campaign)}
+ >
+
+
+
+
+ {campaign.name}
+ {renderPriorityBadge(campaign.priority)}
+
+ {campaign.summary && (
+
{campaign.summary}
+ )}
+
+
+
+ {renderHealthIndicator(campaign.health)}
+
+ {campaign._count?.tasks || 0}
+
+ {renderTeamMembers(campaign)}
+
+
+ {formatDateRange(campaign.startDate, campaign.targetDate)}
+
+
+ {renderStatusBadge(campaign.status)}
+
+
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ onSelectCampaign(campaign)}>
+ View Details
+
+ onEditCampaign(campaign)}>
+ Edit Campaign
+
+ onDeleteCampaign(campaign)}
+ className="text-destructive"
+ >
+ Delete Campaign
+
+
+
+
+
+ );
+
+ const renderPagination = () => {
+ if (pagination.totalPages <= 1) return null;
+
+ return (
+
+
+ Showing {(pagination.page - 1) * pagination.limit + 1}-
+ {Math.min(pagination.page * pagination.limit, pagination.total)} of{' '}
+ {pagination.total} campaigns
+
+
+
onPageChange(pagination.page - 1)}
+ disabled={!pagination.hasPrevPage}
+ >
+ ← Previous
+
+
+ {Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => {
+ const page = i + 1;
+ return (
+ onPageChange(page)}
+ >
+ {page}
+
+ );
+ })}
+
+
onPageChange(pagination.page + 1)}
+ disabled={!pagination.hasNextPage}
+ >
+ Next →
+
+
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Campaigns
+
+ Manage your marketing campaigns and track their progress
+
+
+
+
+ Create Campaign
+
+
+
+ {/* Filters */}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+
+
+ All Status
+ Draft
+ Planning
+ Ready
+ Done
+ Canceled
+
+
+
+
+
+
+
+
+ All Health
+ On Track
+ At Risk
+ Off Track
+
+
+
+
+
+
+
+
+ All Priority
+ Low
+ Medium
+ High
+ Urgent
+
+
+
+
+ setViewMode('table')}
+ >
+
+
+ setViewMode('grid')}
+ >
+
+
+
+
+
+ {/* Campaign Table */}
+
+
+ 📋 Campaigns Overview ({campaigns.length})
+
+
+
+
+
+ Title
+ Health
+ Total Tasks
+ PIC
+ Timeline
+ Status
+
+ Actions
+
+
+
+
+ {campaigns.length === 0 ? (
+
+
+
+
📋
+
No campaigns found
+
+ Create your first campaign to get started
+
+
+
+ Create Campaign
+
+
+
+
+ ) : (
+ campaigns.map(renderCampaignRow)
+ )}
+
+
+ {renderPagination()}
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-members-selector.tsx b/components/campaigns/campaign-members-selector.tsx
new file mode 100644
index 00000000..76d940f2
--- /dev/null
+++ b/components/campaigns/campaign-members-selector.tsx
@@ -0,0 +1,250 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { X, Users, Plus } from 'lucide-react';
+import { User, CampaignMemberRole, CAMPAIGN_MEMBER_ROLES } from '@/types/campaign';
+
+interface CampaignMember {
+ userId: string;
+ role: CampaignMemberRole;
+ user: User;
+}
+
+interface CampaignMembersSelectorProps {
+ value: CampaignMember[];
+ onChange: (members: CampaignMember[]) => void;
+ availableMembers: User[];
+ disabled?: boolean;
+ className?: string;
+}
+
+export default function CampaignMembersSelector({
+ value,
+ onChange,
+ availableMembers,
+ disabled = false,
+ className = '',
+}: CampaignMembersSelectorProps) {
+ const [isAddingMember, setIsAddingMember] = useState(false);
+ const [selectedUserId, setSelectedUserId] = useState('');
+ const [selectedRole, setSelectedRole] = useState('MEMBER');
+
+ const availableUsers = availableMembers.filter(
+ (user) => !value.find((member) => member.userId === user.id)
+ );
+
+ const addMember = () => {
+ if (selectedUserId && selectedRole) {
+ const user = availableMembers.find((u) => u.id === selectedUserId);
+ if (user) {
+ const newMember: CampaignMember = {
+ userId: selectedUserId,
+ role: selectedRole,
+ user,
+ };
+ onChange([...value, newMember]);
+ setSelectedUserId('');
+ setSelectedRole('MEMBER');
+ setIsAddingMember(false);
+ }
+ }
+ };
+
+ const removeMember = (userId: string) => {
+ onChange(value.filter((member) => member.userId !== userId));
+ };
+
+ const updateMemberRole = (userId: string, newRole: CampaignMemberRole) => {
+ onChange(
+ value.map((member) => (member.userId === userId ? { ...member, role: newRole } : member))
+ );
+ };
+
+ const getInitials = (name: string | null) => {
+ if (!name) return '?';
+ return name
+ .split(' ')
+ .map((word) => word.charAt(0))
+ .join('')
+ .toUpperCase()
+ .slice(0, 2);
+ };
+
+ const getRoleColor = (role: CampaignMemberRole) => {
+ switch (role) {
+ case 'OWNER':
+ return 'destructive';
+ case 'MANAGER':
+ return 'default';
+ case 'MEMBER':
+ return 'secondary';
+ case 'VIEWER':
+ return 'outline';
+ default:
+ return 'secondary';
+ }
+ };
+
+ return (
+
+
Team Members
+
+ {/* Current Members */}
+
+ {value.map((member) => (
+
+
+
+
+
+ {getInitials(member.user.name)}
+
+
+
+ {member.user.name || 'Unknown'}
+ {member.user.email}
+
+
+
+
+
+ updateMemberRole(member.userId, newRole as CampaignMemberRole)
+ }
+ disabled={disabled}
+ >
+
+
+
+
+ {CAMPAIGN_MEMBER_ROLES.map((role) => (
+
+
+ {role}
+
+
+ ))}
+
+
+
+ removeMember(member.userId)}
+ disabled={disabled}
+ className="h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive"
+ >
+
+
+
+
+ ))}
+
+
+ {/* Add Member Section */}
+ {!isAddingMember ? (
+
setIsAddingMember(true)}
+ disabled={disabled || availableUsers.length === 0}
+ className="w-full"
+ >
+
+ Add Member
+
+ ) : (
+
+
+
+ Add New Member
+
+
+
+
+
+
+
+
+ {availableUsers.map((user) => (
+
+
+
+
+
+ {getInitials(user.name)}
+
+
+
{user.name || user.email}
+
+
+ ))}
+
+
+
+
setSelectedRole(value as CampaignMemberRole)}
+ >
+
+
+
+
+ {CAMPAIGN_MEMBER_ROLES.map((role) => (
+
+
+ {role}
+
+
+ ))}
+
+
+
+
+
+
+ Add
+
+ {
+ setIsAddingMember(false);
+ setSelectedUserId('');
+ setSelectedRole('MEMBER');
+ }}
+ className="flex-1"
+ >
+ Cancel
+
+
+
+ )}
+
+ {availableUsers.length === 0 && value.length > 0 && (
+
+ All available members have been added to the campaign
+
+ )}
+
+ );
+}
diff --git a/components/campaigns/campaign-pagination.tsx b/components/campaigns/campaign-pagination.tsx
new file mode 100644
index 00000000..7ce56484
--- /dev/null
+++ b/components/campaigns/campaign-pagination.tsx
@@ -0,0 +1,168 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
+import { PaginationMeta } from '@/types/campaign';
+
+interface CampaignPaginationProps {
+ pagination: PaginationMeta;
+ onPageChange: (page: number) => void;
+ onLimitChange: (limit: number) => void;
+ className?: string;
+}
+
+export default function CampaignPagination({
+ pagination,
+ onPageChange,
+ onLimitChange,
+ className = '',
+}: CampaignPaginationProps) {
+ const { page, limit, total, totalPages, hasNextPage, hasPrevPage } = pagination;
+
+ // Calculate page range for display
+ const getPageRange = () => {
+ const delta = 2; // Number of pages to show on each side of current page
+ const range = [];
+ const rangeWithDots = [];
+
+ for (let i = Math.max(2, page - delta); i <= Math.min(totalPages - 1, page + delta); i++) {
+ range.push(i);
+ }
+
+ if (page - delta > 2) {
+ rangeWithDots.push(1, '...');
+ } else {
+ rangeWithDots.push(1);
+ }
+
+ rangeWithDots.push(...range);
+
+ if (page + delta < totalPages - 1) {
+ rangeWithDots.push('...', totalPages);
+ } else if (totalPages > 1) {
+ rangeWithDots.push(totalPages);
+ }
+
+ return rangeWithDots;
+ };
+
+ const handlePageChange = (newPage: number) => {
+ if (newPage >= 1 && newPage <= totalPages) {
+ onPageChange(newPage);
+ }
+ };
+
+ const handleLimitChange = (newLimit: string) => {
+ const limitNum = parseInt(newLimit);
+ if (limitNum !== limit) {
+ onLimitChange(limitNum);
+ }
+ };
+
+ if (total === 0) {
+ return null;
+ }
+
+ return (
+
+ {/* Page Info */}
+
+
+ Showing {Math.min((page - 1) * limit + 1, total)} to {Math.min(page * limit, total)}{' '}
+ of {total} campaigns
+
+
+
+ {/* Pagination Controls */}
+
+ {/* Page Size Selector */}
+
+ Show:
+
+
+
+
+
+ 10
+ 20
+ 50
+ 100
+
+
+
+
+ {/* Page Navigation */}
+
+ {/* First Page */}
+
handlePageChange(1)}
+ disabled={!hasPrevPage}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+ {/* Previous Page */}
+
handlePageChange(page - 1)}
+ disabled={!hasPrevPage}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+ {/* Page Numbers */}
+ {getPageRange().map((pageNum, index) => (
+
+ {pageNum === '...' ? (
+ ...
+ ) : (
+ handlePageChange(pageNum as number)}
+ className="h-8 w-8 p-0"
+ >
+ {pageNum}
+
+ )}
+
+ ))}
+
+ {/* Next Page */}
+
handlePageChange(page + 1)}
+ disabled={!hasNextPage}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+ {/* Last Page */}
+
handlePageChange(totalPages)}
+ disabled={!hasNextPage}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-search.tsx b/components/campaigns/campaign-search.tsx
new file mode 100644
index 00000000..bb2dd06f
--- /dev/null
+++ b/components/campaigns/campaign-search.tsx
@@ -0,0 +1,262 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Search, X, Clock, Layers } from 'lucide-react';
+import { Campaign } from '@/types/campaign';
+
+interface CampaignSearchProps {
+ value: string;
+ onChange: (value: string) => void;
+ onSelect: (campaign: Campaign) => void;
+ campaigns: Campaign[];
+ placeholder?: string;
+ className?: string;
+}
+
+export default function CampaignSearch({
+ value,
+ onChange,
+ onSelect,
+ campaigns,
+ placeholder = 'Search campaigns...',
+ className = '',
+}: CampaignSearchProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [searchResults, setSearchResults] = useState([]);
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
+ const inputRef = useRef(null);
+ const dropdownRef = useRef(null);
+
+ // Filter campaigns based on search value
+ useEffect(() => {
+ if (!value.trim()) {
+ setSearchResults([]);
+ return;
+ }
+
+ const filtered = campaigns.filter((campaign) => {
+ const searchLower = value.toLowerCase();
+ return (
+ campaign.name.toLowerCase().includes(searchLower) ||
+ campaign.description?.toLowerCase().includes(searchLower) ||
+ campaign.summary?.toLowerCase().includes(searchLower) ||
+ campaign.status.toLowerCase().includes(searchLower) ||
+ campaign.health.toLowerCase().includes(searchLower)
+ );
+ });
+
+ setSearchResults(filtered.slice(0, 5)); // Limit to 5 results
+ setHighlightedIndex(-1);
+ }, [value, campaigns]);
+
+ // Handle keyboard navigation
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (!isOpen || searchResults.length === 0) return;
+
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setHighlightedIndex((prev) => (prev < searchResults.length - 1 ? prev + 1 : 0));
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : searchResults.length - 1));
+ break;
+ case 'Enter':
+ e.preventDefault();
+ if (highlightedIndex >= 0) {
+ handleSelect(searchResults[highlightedIndex]);
+ }
+ break;
+ case 'Escape':
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ break;
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [isOpen, searchResults, highlightedIndex]);
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node) &&
+ inputRef.current &&
+ !inputRef.current.contains(event.target as Node)
+ ) {
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ onChange(newValue);
+ setIsOpen(newValue.length > 0);
+ };
+
+ const handleInputFocus = () => {
+ if (value.length > 0) {
+ setIsOpen(true);
+ }
+ };
+
+ const handleSelect = (campaign: Campaign) => {
+ onSelect(campaign);
+ onChange('');
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ inputRef.current?.blur();
+ };
+
+ const handleClear = () => {
+ onChange('');
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ inputRef.current?.focus();
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'DRAFT':
+ return 'secondary';
+ case 'PLANNING':
+ return 'default';
+ case 'READY':
+ return 'default';
+ case 'DONE':
+ return 'default';
+ case 'CANCELED':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+ };
+
+ const getHealthColor = (health: string) => {
+ switch (health) {
+ case 'ON_TRACK':
+ return 'default';
+ case 'AT_RISK':
+ return 'destructive';
+ case 'OFF_TRACK':
+ return 'secondary';
+ default:
+ return 'default';
+ }
+ };
+
+ const formatDate = (date: string | Date) => {
+ return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+ };
+
+ return (
+
+
+
+
+ {value && (
+
+
+
+ )}
+
+
+ {/* Search Results Dropdown */}
+ {isOpen && searchResults.length > 0 && (
+
+ {searchResults.map((campaign, index) => (
+
handleSelect(campaign)}
+ onMouseEnter={() => setHighlightedIndex(index)}
+ >
+
+
+
+
+
+
{campaign.name}
+
+ {campaign.status}
+
+
+ {campaign.health.replace('_', ' ')}
+
+
+
+ {campaign.description && (
+
+ {campaign.description}
+
+ )}
+
+
+
+
+
+ {campaign.startDate && campaign.targetDate
+ ? `${formatDate(campaign.startDate)} → ${formatDate(campaign.targetDate)}`
+ : formatDate(campaign.createdAt)}
+
+
+
+
+
+ {campaign._count?.tasks || 0} tasks
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* No Results */}
+ {isOpen && value.length > 0 && searchResults.length === 0 && (
+
+
+
+
No campaigns found
+
Try different keywords or check your spelling
+
+
+ )}
+
+ );
+}
diff --git a/components/campaigns/campaign-status-selector.tsx b/components/campaigns/campaign-status-selector.tsx
new file mode 100644
index 00000000..247e654e
--- /dev/null
+++ b/components/campaigns/campaign-status-selector.tsx
@@ -0,0 +1,88 @@
+'use client';
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Badge } from '@/components/ui/badge';
+import { CampaignStatus, CAMPAIGN_STATUSES } from '@/types/campaign';
+
+interface CampaignStatusSelectorProps {
+ value: CampaignStatus;
+ onChange: (status: CampaignStatus) => void;
+ disabled?: boolean;
+ className?: string;
+}
+
+export default function CampaignStatusSelector({
+ value,
+ onChange,
+ disabled = false,
+ className = '',
+}: CampaignStatusSelectorProps) {
+ const getStatusColor = (status: CampaignStatus) => {
+ switch (status) {
+ case 'DRAFT':
+ return 'secondary';
+ case 'PLANNING':
+ return 'default';
+ case 'READY':
+ return 'default';
+ case 'DONE':
+ return 'default';
+ case 'CANCELED':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+ };
+
+ const getStatusIcon = (status: CampaignStatus) => {
+ switch (status) {
+ case 'DRAFT':
+ return '📝';
+ case 'PLANNING':
+ return '📋';
+ case 'READY':
+ return '✅';
+ case 'DONE':
+ return '🎉';
+ case 'CANCELED':
+ return '❌';
+ default:
+ return '📝';
+ }
+ };
+
+ return (
+
+
Status
+
+
+
+
+ {getStatusIcon(value)}
+ {value}
+
+
+
+
+ {CAMPAIGN_STATUSES.map((status) => (
+
+
+ {getStatusIcon(status)}
+ {status}
+
+ {status}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/campaigns/campaign-table.tsx b/components/campaigns/campaign-table.tsx
new file mode 100644
index 00000000..33939f5f
--- /dev/null
+++ b/components/campaigns/campaign-table.tsx
@@ -0,0 +1,423 @@
+'use client';
+
+import { useState } from 'react';
+import { Campaign, CampaignStatus, CampaignHealth, CampaignPriority } from '@/types/campaign';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ Layers,
+ FileText,
+ Users,
+ Calendar,
+ ChevronRight,
+ Search,
+ Filter,
+ Plus,
+} from 'lucide-react';
+import Link from 'next/link';
+
+interface CampaignTableProps {
+ campaigns: Campaign[];
+ onSelect: (campaign: Campaign) => void;
+ onEdit: (campaign: Campaign) => void;
+ onDelete: (campaign: Campaign) => void;
+ onCreateCampaign: () => void;
+ loading?: boolean;
+ orgId: string;
+}
+
+interface CampaignFilters {
+ status?: CampaignStatus;
+ health?: CampaignHealth;
+ priority?: CampaignPriority;
+ search?: string;
+}
+
+export default function CampaignTable({
+ campaigns,
+ onSelect,
+ onEdit,
+ onDelete,
+ onCreateCampaign,
+ loading = false,
+ orgId,
+}: CampaignTableProps) {
+ const [filters, setFilters] = useState({});
+ const [sortBy, setSortBy] = useState<'name' | 'status' | 'health' | 'priority' | 'createdAt'>(
+ 'createdAt'
+ );
+ const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
+
+ // Filter campaigns based on current filters
+ const filteredCampaigns = campaigns.filter((campaign) => {
+ if (filters.status && campaign.status !== filters.status) return false;
+ if (filters.health && campaign.health !== filters.health) return false;
+ if (filters.priority && campaign.priority !== filters.priority) return false;
+ if (filters.search) {
+ const searchLower = filters.search.toLowerCase();
+ return (
+ campaign.name.toLowerCase().includes(searchLower) ||
+ campaign.description?.toLowerCase().includes(searchLower) ||
+ campaign.summary?.toLowerCase().includes(searchLower)
+ );
+ }
+ return true;
+ });
+
+ // Sort campaigns
+ const sortedCampaigns = [...filteredCampaigns].sort((a, b) => {
+ let aValue: any = a[sortBy];
+ let bValue: any = b[sortBy];
+
+ if (sortBy === 'createdAt') {
+ aValue = new Date(aValue).getTime();
+ bValue = new Date(bValue).getTime();
+ }
+
+ if (sortOrder === 'asc') {
+ return aValue > bValue ? 1 : -1;
+ } else {
+ return aValue < bValue ? 1 : -1;
+ }
+ });
+
+ const handleSort = (field: typeof sortBy) => {
+ if (sortBy === field) {
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
+ } else {
+ setSortBy(field);
+ setSortOrder('asc');
+ }
+ };
+
+ const getHealthColor = (health: CampaignHealth) => {
+ switch (health) {
+ case 'ON_TRACK':
+ return 'default';
+ case 'AT_RISK':
+ return 'destructive';
+ case 'OFF_TRACK':
+ return 'secondary';
+ default:
+ return 'default';
+ }
+ };
+
+ const getStatusColor = (status: CampaignStatus) => {
+ switch (status) {
+ case 'DRAFT':
+ return 'secondary';
+ case 'PLANNING':
+ return 'default';
+ case 'READY':
+ return 'default';
+ case 'DONE':
+ return 'default';
+ case 'CANCELED':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+ };
+
+ const getPriorityColor = (priority: CampaignPriority) => {
+ switch (priority) {
+ case 'NO_PRIORITY':
+ return 'secondary';
+ case 'LOW':
+ return 'default';
+ case 'MEDIUM':
+ return 'default';
+ case 'HIGH':
+ return 'destructive';
+ case 'URGENT':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+ };
+
+ const formatDate = (date: string | Date) => {
+ return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+ };
+
+ if (loading) {
+ return (
+
+
+
Title
+
Health
+
Total Tasks
+
PIC
+
Timeline
+
Status
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {/* Filters and Actions */}
+
+
+
+
+ setFilters({ ...filters, search: e.target.value })}
+ className="pl-10 w-64"
+ />
+
+
+
+ setFilters({ ...filters, status: value as CampaignStatus })
+ }
+ >
+
+
+
+
+ All Status
+ Draft
+ Planning
+ Ready
+ Done
+ Canceled
+
+
+
+
+ setFilters({ ...filters, health: value as CampaignHealth })
+ }
+ >
+
+
+
+
+ All Health
+ On Track
+ At Risk
+ Off Track
+
+
+
+
+ setFilters({ ...filters, priority: value as CampaignPriority })
+ }
+ >
+
+
+
+
+ All Priority
+ No Priority
+ Low
+ Medium
+ High
+ Urgent
+
+
+
+
+
+
+ Create Campaign
+
+
+
+ {/* Table */}
+
+
+
+
+ handleSort('name')}
+ >
+
+ Title
+ {sortBy === 'name' && (
+ {sortOrder === 'asc' ? '↑' : '↓'}
+ )}
+
+
+ handleSort('health')}
+ >
+
+ Health
+ {sortBy === 'health' && (
+ {sortOrder === 'asc' ? '↑' : '↓'}
+ )}
+
+
+ Total Tasks
+ PIC
+ handleSort('createdAt')}
+ >
+
+ Timeline
+ {sortBy === 'createdAt' && (
+ {sortOrder === 'asc' ? '↑' : '↓'}
+ )}
+
+
+ handleSort('status')}
+ >
+
+ Status
+ {sortBy === 'status' && (
+ {sortOrder === 'asc' ? '↑' : '↓'}
+ )}
+
+
+ Actions
+
+
+
+ {sortedCampaigns.length === 0 ? (
+
+
+ No campaigns found.
+
+
+ ) : (
+ sortedCampaigns.map((campaign) => (
+
+
+
+
+
+
+ {campaign.name}
+
+ {campaign.description && (
+
+ {campaign.description}
+
+ )}
+
+
+
+
+
+
+ {campaign.health.replace('_', ' ')}
+
+
+
+
+
+
+ {campaign._count?.tasks || 0}
+
+
+
+
+
+
+
+ {campaign.members && campaign.members.length > 0
+ ? campaign.members.length > 1
+ ? `${campaign.members[0].user.name || 'Unknown'} +${campaign.members.length - 1}`
+ : campaign.members[0].user.name || 'Unknown'
+ : 'Unassigned'}
+
+
+
+
+
+
+
+
+ {campaign.startDate && campaign.targetDate
+ ? `${formatDate(campaign.startDate)} → ${formatDate(campaign.targetDate)}`
+ : formatDate(campaign.createdAt)}
+
+
+
+
+
+
+ {campaign.status}
+
+
+
+
+
+ onSelect(campaign)}
+ className="hover:bg-accent"
+ >
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ );
+}
diff --git a/components/campaigns/content/campaign-content-page.tsx b/components/campaigns/content/campaign-content-page.tsx
new file mode 100644
index 00000000..90441aa7
--- /dev/null
+++ b/components/campaigns/content/campaign-content-page.tsx
@@ -0,0 +1,695 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Plus,
+ Search,
+ FileText,
+ Edit,
+ Eye,
+ Trash2,
+ Filter,
+ Calendar,
+ User,
+ CheckCircle,
+ XCircle,
+ Clock,
+ Send,
+} from 'lucide-react';
+import type {
+ Campaign,
+ CampaignContent,
+ CreateContentData,
+ UpdateContentData,
+} from '@/types/campaign';
+
+interface CampaignContentPageProps {
+ orgId: string;
+ campaignId: string;
+ campaign: Campaign;
+}
+
+const contentStatusConfig = {
+ DRAFT: { label: 'Draft', color: 'bg-gray-500', icon: '📝' },
+ SUBMITTED: { label: 'Submitted', color: 'bg-yellow-500', icon: '📤' },
+ APPROVED: { label: 'Approved', color: 'bg-green-500', icon: '✅' },
+ PUBLISHED: { label: 'Published', color: 'bg-blue-500', icon: '🌐' },
+ REJECTED: { label: 'Rejected', color: 'bg-red-500', icon: '❌' },
+};
+
+const contentTypeConfig = {
+ BLOG_POST: { label: 'Blog Post', icon: '📝' },
+ SOCIAL_MEDIA: { label: 'Social Media', icon: '📱' },
+ EMAIL: { label: 'Email', icon: '📧' },
+ VIDEO: { label: 'Video', icon: '🎥' },
+ IMAGE: { label: 'Image', icon: '🖼️' },
+ DOCUMENT: { label: 'Document', icon: '📄' },
+};
+
+export function CampaignContentPage({ orgId, campaignId, campaign }: CampaignContentPageProps) {
+ const router = useRouter();
+ const [contents, setContents] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [editingContent, setEditingContent] = useState(null);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [typeFilter, setTypeFilter] = useState('all');
+ const [newContentData, setNewContentData] = useState({
+ title: '',
+ description: '',
+ body: '',
+ type: 'BLOG_POST' as CampaignContent['type'],
+ status: 'DRAFT' as CampaignContent['status'],
+ tags: '',
+ });
+
+ useEffect(() => {
+ fetchContents();
+ }, [orgId, campaignId]);
+
+ const fetchContents = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/contents`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch contents');
+ }
+ const data = await response.json();
+ setContents(data.contents || []);
+ } catch (error) {
+ console.error('Error fetching contents:', error);
+ toast.error('Failed to fetch contents');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCreateContent = async () => {
+ if (!newContentData.title.trim()) {
+ toast.error('Please enter a title');
+ return;
+ }
+
+ try {
+ const contentData = {
+ ...newContentData,
+ tags: newContentData.tags
+ ? newContentData.tags.split(',').map((tag) => tag.trim())
+ : [],
+ campaignId,
+ };
+
+ const response = await fetch(`/api/${orgId}/content`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(contentData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to create content');
+ }
+
+ const newContent = await response.json();
+ setContents((prev) => [...prev, newContent.content]);
+ setShowCreateModal(false);
+ setNewContentData({
+ title: '',
+ description: '',
+ body: '',
+ type: 'BLOG_POST',
+ status: 'DRAFT',
+ tags: '',
+ });
+ toast.success('Content created successfully');
+ } catch (error) {
+ console.error('Error creating content:', error);
+ toast.error('Failed to create content');
+ }
+ };
+
+ const handleUpdateContent = async () => {
+ if (!editingContent || !newContentData.title.trim()) {
+ toast.error('Please enter a title');
+ return;
+ }
+
+ try {
+ const contentData = {
+ ...newContentData,
+ tags: newContentData.tags
+ ? newContentData.tags.split(',').map((tag) => tag.trim())
+ : [],
+ };
+
+ const response = await fetch(`/api/${orgId}/content/${editingContent.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(contentData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update content');
+ }
+
+ const updatedContent = await response.json();
+ setContents((prev) =>
+ prev.map((content) =>
+ content.id === editingContent.id
+ ? { ...content, ...updatedContent.content }
+ : content
+ )
+ );
+ setShowEditModal(false);
+ setEditingContent(null);
+ toast.success('Content updated successfully');
+ } catch (error) {
+ console.error('Error updating content:', error);
+ toast.error('Failed to update content');
+ }
+ };
+
+ const handleDeleteContent = async (contentId: string) => {
+ if (!confirm('Are you sure you want to delete this content? This action cannot be undone.')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/${orgId}/content/${contentId}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete content');
+ }
+
+ setContents((prev) => prev.filter((content) => content.id !== contentId));
+ toast.success('Content deleted successfully');
+ } catch (error) {
+ console.error('Error deleting content:', error);
+ toast.error('Failed to delete content');
+ }
+ };
+
+ const handleEditContent = (content: CampaignContent) => {
+ setEditingContent(content);
+ setNewContentData({
+ title: content.title || '',
+ description: content.description || '',
+ body: content.body || '',
+ type: content.type,
+ status: content.status,
+ tags: content.tags?.join(', ') || '',
+ });
+ setShowEditModal(true);
+ };
+
+ const handleStatusChange = async (contentId: string, newStatus: CampaignContent['status']) => {
+ try {
+ const content = contents.find((c) => c.id === contentId);
+ if (!content) return;
+
+ const response = await fetch(`/api/${orgId}/content/${contentId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ...content, status: newStatus }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update content status');
+ }
+
+ const updatedContent = await response.json();
+ setContents((prev) =>
+ prev.map((c) => (c.id === contentId ? { ...c, ...updatedContent.content } : c))
+ );
+ toast.success(`Content status updated to ${contentStatusConfig[newStatus].label}`);
+ } catch (error) {
+ console.error('Error updating content status:', error);
+ toast.error('Failed to update content status');
+ }
+ };
+
+ const filteredContents = contents.filter((content) => {
+ const matchesSearch =
+ content.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ content.description?.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesStatus = statusFilter === 'all' || content.status === statusFilter;
+ const matchesType = typeFilter === 'all' || content.type === typeFilter;
+ return matchesSearch && matchesStatus && matchesType;
+ });
+
+ const getStatusStats = () => {
+ return contents.reduce(
+ (acc, content) => {
+ acc[content.status] = (acc[content.status] || 0) + 1;
+ return acc;
+ },
+ {} as Record
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading content management...
+
+
+ );
+ }
+
+ const statusStats = getStatusStats();
+
+ return (
+
+ {/* Page Header */}
+
+
+
Content Management
+
+ Manage content for campaign: {campaign.title}
+
+
+
+
setShowCreateModal(true)}>
+
+ Create Content
+
+
router.push(`/${orgId}/campaigns/${campaignId}`)}
+ >
+ Back to Campaign
+
+
+
+
+ {/* Content Statistics */}
+
+ {Object.entries(contentStatusConfig).map(([status, config]) => {
+ const count = statusStats[status] || 0;
+ return (
+
+
+
+ {config.icon}
+ {config.label}
+
+ {count}
+ content items
+
+
+ );
+ })}
+
+
+ {/* Search and Filters */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+
+
+ All Status
+ {Object.entries(contentStatusConfig).map(([status, config]) => (
+
+ {config.label}
+
+ ))}
+
+
+
+
+
+
+
+
+ All Types
+ {Object.entries(contentTypeConfig).map(([type, config]) => (
+
+ {config.icon} {config.label}
+
+ ))}
+
+
+
+
+ {/* Content List */}
+
+
+ Content Items ({filteredContents.length})
+
+
+
+ {filteredContents.map((content) => (
+
+
+
+
+
+
+
+
+
{content.title}
+
+ {contentStatusConfig[content.status].icon}{' '}
+ {contentStatusConfig[content.status].label}
+
+
+ {contentTypeConfig[content.type].icon}{' '}
+ {contentTypeConfig[content.type].label}
+
+
+
+ {content.description && (
+
+ {content.description}
+
+ )}
+
+
+ {content.createdAt && (
+
+
+ Created {new Date(content.createdAt).toLocaleDateString()}
+
+ )}
+ {content.updatedAt && (
+
+
+ Updated {new Date(content.updatedAt).toLocaleDateString()}
+
+ )}
+
+
+
+
+
+
+ handleStatusChange(content.id, value as CampaignContent['status'])
+ }
+ >
+
+
+
+
+ {Object.entries(contentStatusConfig).map(([status, config]) => (
+
+ {config.icon} {config.label}
+
+ ))}
+
+
+
+ handleEditContent(content)}
+ >
+
+
+
+ router.push(`/${orgId}/content/${content.id}`)}
+ >
+
+
+
+ handleDeleteContent(content.id)}
+ className="text-red-600 hover:text-red-700"
+ >
+
+
+
+
+ ))}
+
+ {filteredContents.length === 0 && (
+
+
No content found
+ {searchTerm || statusFilter !== 'all' || typeFilter !== 'all' ? (
+
{
+ setSearchTerm('');
+ setStatusFilter('all');
+ setTypeFilter('all');
+ }}
+ >
+ Clear filters
+
+ ) : (
+
setShowCreateModal(true)}>
+
+ Create your first content
+
+ )}
+
+ )}
+
+
+
+
+ {/* Create Content Modal */}
+
+
+
+ Create New Content
+ Create new content for this campaign
+
+
+
+
+
+ Title *
+
+ setNewContentData((prev) => ({ ...prev, title: e.target.value }))
+ }
+ placeholder="Enter content title"
+ required
+ />
+
+
+
+ Content Type
+
+ setNewContentData((prev) => ({
+ ...prev,
+ type: value as CampaignContent['type'],
+ }))
+ }
+ >
+
+
+
+
+ {Object.entries(contentTypeConfig).map(([type, config]) => (
+
+ {config.icon} {config.label}
+
+ ))}
+
+
+
+
+
+
+ Description
+
+ setNewContentData((prev) => ({ ...prev, description: e.target.value }))
+ }
+ placeholder="Brief description of the content..."
+ rows={3}
+ />
+
+
+
+ Content Body
+
+ setNewContentData((prev) => ({ ...prev, body: e.target.value }))
+ }
+ placeholder="Enter the main content..."
+ rows={6}
+ />
+
+
+
+ Tags
+
+ setNewContentData((prev) => ({ ...prev, tags: e.target.value }))
+ }
+ placeholder="Enter tags separated by commas"
+ />
+
+
+
+
+ setShowCreateModal(false)}>
+ Cancel
+
+ Create Content
+
+
+
+
+ {/* Edit Content Modal */}
+
+
+
+ Edit Content
+ Update content details
+
+
+
+
+
+ Title *
+
+ setNewContentData((prev) => ({ ...prev, title: e.target.value }))
+ }
+ placeholder="Enter content title"
+ required
+ />
+
+
+
+ Content Type
+
+ setNewContentData((prev) => ({
+ ...prev,
+ type: value as CampaignContent['type'],
+ }))
+ }
+ >
+
+
+
+
+ {Object.entries(contentTypeConfig).map(([type, config]) => (
+
+ {config.icon} {config.label}
+
+ ))}
+
+
+
+
+
+
+ Description
+
+ setNewContentData((prev) => ({ ...prev, description: e.target.value }))
+ }
+ placeholder="Brief description of the content..."
+ rows={3}
+ />
+
+
+
+ Content Body
+
+ setNewContentData((prev) => ({ ...prev, body: e.target.value }))
+ }
+ placeholder="Enter the main content..."
+ rows={6}
+ />
+
+
+
+ Tags
+
+ setNewContentData((prev) => ({ ...prev, tags: e.target.value }))
+ }
+ placeholder="Enter tags separated by commas"
+ />
+
+
+
+
+ setShowEditModal(false)}>
+ Cancel
+
+ Update Content
+
+
+
+
+ );
+}
diff --git a/components/campaigns/content/index.ts b/components/campaigns/content/index.ts
new file mode 100644
index 00000000..24bd996a
--- /dev/null
+++ b/components/campaigns/content/index.ts
@@ -0,0 +1 @@
+export { CampaignContentPage } from './campaign-content-page';
diff --git a/components/campaigns/create-campaign-button.tsx b/components/campaigns/create-campaign-button.tsx
new file mode 100644
index 00000000..4e769ff8
--- /dev/null
+++ b/components/campaigns/create-campaign-button.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Plus } from 'lucide-react';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+
+export function CreateCampaignButton() {
+ const params = useParams();
+ const orgId = params.orgId as string;
+
+ return (
+
+
+
+ New Campaign
+
+
+ );
+}
diff --git a/components/campaigns/member-detail-view.tsx b/components/campaigns/member-detail-view.tsx
new file mode 100644
index 00000000..d16e53f5
--- /dev/null
+++ b/components/campaigns/member-detail-view.tsx
@@ -0,0 +1,439 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Separator } from '@/components/ui/separator';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ MoreHorizontal,
+ Edit,
+ Trash2,
+ User,
+ CheckSquare,
+ Calendar,
+ Mail,
+ Phone,
+ MapPin,
+ Clock,
+ ArrowLeft,
+ Crown,
+ Shield,
+ Users,
+ Eye,
+} from 'lucide-react';
+import type { CampaignMember, CampaignTask } from '@/types/campaign';
+
+interface MemberDetailViewProps {
+ member: CampaignMember;
+ onUpdateRole: (memberId: string, role: CampaignMember['role']) => void;
+ onRemove: (memberId: string) => void;
+ onBack: () => void;
+ onEdit: () => void;
+}
+
+const roleConfig = {
+ OWNER: {
+ label: 'Owner',
+ color: 'bg-purple-500',
+ icon: Crown,
+ description: 'Full campaign control',
+ },
+ MANAGER: {
+ label: 'Manager',
+ color: 'bg-blue-500',
+ icon: Shield,
+ description: 'Manage tasks and members',
+ },
+ MEMBER: {
+ label: 'Member',
+ color: 'bg-green-500',
+ icon: Users,
+ description: 'Work on assigned tasks',
+ },
+ VIEWER: {
+ label: 'Viewer',
+ color: 'bg-gray-500',
+ icon: Eye,
+ description: 'View campaign information',
+ },
+};
+
+const taskStatusConfig = {
+ TODO: { label: 'To Do', color: 'bg-gray-500', icon: '⏳' },
+ IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-500', icon: '🔄' },
+ REVIEW: { label: 'Review', color: 'bg-yellow-500', icon: '👀' },
+ DONE: { label: 'Done', color: 'bg-green-500', icon: '✅' },
+ CANCELLED: { label: 'Cancelled', color: 'bg-red-500', icon: '❌' },
+};
+
+export function MemberDetailView({
+ member,
+ onUpdateRole,
+ onRemove,
+ onBack,
+ onEdit,
+}: MemberDetailViewProps) {
+ const [activeTab, setActiveTab] = useState('overview');
+
+ const formatDate = (date: Date | string | null) => {
+ if (!date) return 'Not set';
+ return new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ const getAssignedTasks = () => {
+ // In real app, this would come from the campaign's tasks filtered by assignee
+ return member.assignedTasks || [];
+ };
+
+ const getTaskStats = () => {
+ const tasks = getAssignedTasks();
+ return {
+ total: tasks.length,
+ todo: tasks.filter((task) => task.status === 'TODO').length,
+ inProgress: tasks.filter((task) => task.status === 'IN_PROGRESS').length,
+ review: tasks.filter((task) => task.status === 'REVIEW').length,
+ done: tasks.filter((task) => task.status === 'DONE').length,
+ cancelled: tasks.filter((task) => task.status === 'CANCELLED').length,
+ };
+ };
+
+ const calculateProgress = () => {
+ const tasks = getAssignedTasks();
+ if (tasks.length === 0) return 0;
+ const completedTasks = tasks.filter((task) => task.status === 'DONE').length;
+ return Math.round((completedTasks / tasks.length) * 100);
+ };
+
+ const renderRoleBadge = (role: CampaignMember['role']) => {
+ const config = roleConfig[role];
+ const IconComponent = config.icon;
+ return (
+
+
+ {config.label}
+
+ );
+ };
+
+ const renderTaskItem = (task: CampaignTask) => {
+ const config = taskStatusConfig[task.status];
+ return (
+
+
+
{config.icon}
+
+
{task.title}
+ {task.description && (
+
{task.description}
+ )}
+
+
+
+
+ {config.label}
+
+ {task.dueDate && (
+
+ Due {formatDate(task.dueDate)}
+
+ )}
+
+
+ );
+ };
+
+ const assignedTasks = getAssignedTasks();
+ const taskStats = getTaskStats();
+ const progress = calculateProgress();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Back to Campaign
+
+
+
+
+
+
+ {member.user.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+
+
{member.user.name || 'Unknown User'}
+
+ {renderRoleBadge(member.role)}
+ •
+ {member.user.email}
+ •
+
+ Joined {formatDate(member.createdAt)}
+
+
+
+
+
+
+
+
+ Edit Role
+
+
+
+
+
+
+
+
+ Change Role
+ onRemove(member.id)}
+ className="text-destructive"
+ >
+ Remove Member
+
+
+
+
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
Task Progress
+
{progress}%
+
+
+
+
+
+
+
+
+
+
+
+
Assigned Tasks
+
{taskStats.total}
+
+
+
+
+
+
+
+
+
+
+
Completed
+
{taskStats.done}
+
+
+
+
+
+
+
+
+
+
+
Member Since
+
{formatDate(member.createdAt)}
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ Overview
+ Tasks ({taskStats.total})
+ Activity
+
+
+
+
+
+
+ 👤 Member Information
+
+
+
+
+ Name:
+ {member.user.name || 'Unknown'}
+
+
+ Email:
+ {member.user.email}
+
+
+ Role:
+ {renderRoleBadge(member.role)}
+
+
+ Joined:
+ {formatDate(member.createdAt)}
+
+
+ Last Active:
+ {formatDate(member.updatedAt)}
+
+
+
+
+
+
+
+ 📊 Task Statistics
+
+
+
+
+ To Do
+ {taskStats.todo}
+
+
+ In Progress
+ {taskStats.inProgress}
+
+
+ Review
+ {taskStats.review}
+
+
+ Done
+ {taskStats.done}
+
+
+
+
+ Total Progress
+ {progress}%
+
+
+
+
+
+
+
+
+ 🔑 Role Permissions
+
+
+
+ {Object.entries(roleConfig).map(([role, config]) => {
+ const IconComponent = config.icon;
+ const isCurrentRole = member.role === role;
+ return (
+
+
+
+
+
+ {config.label}
+
+
+ {config.description}
+
+
+ {isCurrentRole && (
+
+ Current
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
📋 Assigned Tasks ({taskStats.total})
+
+
+ View All Tasks
+
+
+
+
+ {assignedTasks.length > 0 ? (
+ assignedTasks.map(renderTaskItem)
+ ) : (
+
+
+
No tasks assigned
+
+ This member hasn't been assigned any tasks yet
+
+
+
+ Assign Tasks
+
+
+ )}
+
+
+
+
+
+
+ 📝 Recent Activity
+
+
+
+
+
Activity tracking coming soon
+
+ Track member actions, task updates, and contributions
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/members/campaign-members-page.tsx b/components/campaigns/members/campaign-members-page.tsx
new file mode 100644
index 00000000..13431f33
--- /dev/null
+++ b/components/campaigns/members/campaign-members-page.tsx
@@ -0,0 +1,403 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Plus, Search, UserPlus, Mail, Phone, MapPin, Settings, Trash2 } from 'lucide-react';
+import type { Campaign, CampaignMember, AddMemberData } from '@/types/campaign';
+
+interface CampaignMembersPageProps {
+ orgId: string;
+ campaignId: string;
+ campaign: Campaign;
+}
+
+const memberRoleConfig = {
+ OWNER: { label: 'Owner', color: 'bg-purple-500', description: 'Full campaign control' },
+ MANAGER: { label: 'Manager', color: 'bg-blue-500', description: 'Manage tasks and members' },
+ ADMIN: { label: 'Admin', color: 'bg-green-500', description: 'Edit campaign details' },
+ MEMBER: { label: 'Member', color: 'bg-gray-500', description: 'View and contribute' },
+ VIEWER: { label: 'Viewer', color: 'bg-slate-500', description: 'Read-only access' },
+};
+
+export function CampaignMembersPage({ orgId, campaignId, campaign }: CampaignMembersPageProps) {
+ const router = useRouter();
+ const [members, setMembers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showAddMemberModal, setShowAddMemberModal] = useState(false);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [roleFilter, setRoleFilter] = useState('all');
+ const [newMemberData, setNewMemberData] = useState({
+ userId: '',
+ role: 'MEMBER' as CampaignMember['role'],
+ });
+
+ useEffect(() => {
+ fetchMembers();
+ }, [orgId, campaignId]);
+
+ const fetchMembers = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/members`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch members');
+ }
+ const data = await response.json();
+ setMembers(data.members || []);
+ } catch (error) {
+ console.error('Error fetching members:', error);
+ toast.error('Failed to fetch members');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAddMember = async () => {
+ if (!newMemberData.userId) {
+ toast.error('Please select a user');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/members`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newMemberData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to add member');
+ }
+
+ const newMember = await response.json();
+ setMembers((prev) => [...prev, newMember.member]);
+ setShowAddMemberModal(false);
+ setNewMemberData({ userId: '', role: 'MEMBER' });
+ toast.success('Member added successfully');
+ } catch (error) {
+ console.error('Error adding member:', error);
+ toast.error('Failed to add member');
+ }
+ };
+
+ const handleRemoveMember = async (memberId: string) => {
+ if (!confirm('Are you sure you want to remove this member?')) {
+ return;
+ }
+
+ try {
+ // TODO: Implement remove member API
+ toast.info('Remove member functionality coming soon');
+ } catch (error) {
+ console.error('Error removing member:', error);
+ toast.error('Failed to remove member');
+ }
+ };
+
+ const handleRoleChange = async (memberId: string, newRole: CampaignMember['role']) => {
+ try {
+ // TODO: Implement role change API
+ toast.info('Role change functionality coming soon');
+ } catch (error) {
+ console.error('Error changing role:', error);
+ toast.error('Failed to change role');
+ }
+ };
+
+ const filteredMembers = members.filter((member) => {
+ const matchesSearch =
+ member.user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ member.user.email?.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesRole = roleFilter === 'all' || member.role === roleFilter;
+ return matchesSearch && matchesRole;
+ });
+
+ const getRoleStats = () => {
+ return members.reduce(
+ (acc, member) => {
+ acc[member.role] = (acc[member.role] || 0) + 1;
+ return acc;
+ },
+ {} as Record
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading member management...
+
+
+ );
+ }
+
+ return (
+
+ {/* Page Header */}
+
+
+
Team Management
+
+ Manage team members for campaign:{' '}
+ {campaign.title}
+
+
+
+ setShowAddMemberModal(true)}>
+
+ Add Member
+
+ router.push(`/${orgId}/campaigns/${campaignId}`)}
+ >
+ Back to Campaign
+
+
+
+
+ {/* Member Statistics */}
+
+ {Object.entries(memberRoleConfig).map(([role, config]) => {
+ const count = getRoleStats()[role] || 0;
+ return (
+
+
+
+ {count}
+ {config.description}
+
+
+ );
+ })}
+
+
+ {/* Search and Filters */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+
+
+ All Roles
+ {Object.entries(memberRoleConfig).map(([role, config]) => (
+
+ {config.label}
+
+ ))}
+
+
+
+
+ {/* Members List */}
+
+
+ Team Members ({filteredMembers.length})
+
+
+
+ {filteredMembers.map((member) => (
+
+
+
+
+
+ {member.user.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+
+
+
+
{member.user.name}
+
+ {memberRoleConfig[member.role].label}
+
+
+
+
+ {member.user.email && (
+
+
+ {member.user.email}
+
+ )}
+ {member.user.phone && (
+
+
+ {member.user.phone}
+
+ )}
+
+
+
+
+
+
+ handleRoleChange(member.id, value as CampaignMember['role'])
+ }
+ >
+
+
+
+
+ {Object.entries(memberRoleConfig).map(([role, config]) => (
+
+ {config.label}
+
+ ))}
+
+
+
+ handleRemoveMember(member.id)}
+ className="text-red-600 hover:text-red-700"
+ >
+
+
+
+
+ ))}
+
+ {filteredMembers.length === 0 && (
+
+
No members found
+ {searchTerm || roleFilter !== 'all' ? (
+
{
+ setSearchTerm('');
+ setRoleFilter('all');
+ }}
+ >
+ Clear filters
+
+ ) : (
+
setShowAddMemberModal(true)}>
+
+ Add your first member
+
+ )}
+
+ )}
+
+
+
+
+ {/* Add Member Modal */}
+
+
+
+ Add Team Member
+ Add a new member to the campaign team
+
+
+
+
+ Select User
+
+ setNewMemberData((prev) => ({ ...prev, userId: value }))
+ }
+ >
+
+
+
+
+ {/* In a real app, you'd fetch available users from the organization */}
+ John Doe (john@example.com)
+ Jane Smith (jane@example.com)
+ Bob Johnson (bob@example.com)
+
+
+
+
+
+
Role
+
+ setNewMemberData((prev) => ({
+ ...prev,
+ role: value as CampaignMember['role'],
+ }))
+ }
+ >
+
+
+
+
+ {Object.entries(memberRoleConfig).map(([role, config]) => (
+
+
+ {config.label}
+
+ ({config.description})
+
+
+
+ ))}
+
+
+
+
+
+
+ setShowAddMemberModal(false)}>
+ Cancel
+
+ Add Member
+
+
+
+
+ );
+}
diff --git a/components/campaigns/members/index.ts b/components/campaigns/members/index.ts
new file mode 100644
index 00000000..0aefb66b
--- /dev/null
+++ b/components/campaigns/members/index.ts
@@ -0,0 +1 @@
+export { CampaignMembersPage } from './campaign-members-page';
diff --git a/components/campaigns/milestone-detail-view.tsx b/components/campaigns/milestone-detail-view.tsx
new file mode 100644
index 00000000..40b542ce
--- /dev/null
+++ b/components/campaigns/milestone-detail-view.tsx
@@ -0,0 +1,564 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Separator } from '@/components/ui/separator';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ MoreHorizontal,
+ Edit,
+ Trash2,
+ Calendar,
+ CheckSquare,
+ Target,
+ Clock,
+ Users,
+ ArrowLeft,
+ Flag,
+ TrendingUp,
+ AlertCircle,
+} from 'lucide-react';
+import type { CampaignMilestone, CampaignTask } from '@/types/campaign';
+
+interface MilestoneDetailViewProps {
+ milestone: CampaignMilestone;
+ onUpdate: (milestoneId: string, data: Partial) => void;
+ onDelete: (milestoneId: string) => void;
+ onBack: () => void;
+ onEdit: () => void;
+}
+
+const milestoneStatusConfig = {
+ PENDING: { label: 'Pending', color: 'bg-gray-500', icon: '⏳' },
+ IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-500', icon: '🔄' },
+ COMPLETED: { label: 'Completed', color: 'bg-green-500', icon: '✅' },
+ OVERDUE: { label: 'Overdue', color: 'bg-red-500', icon: '⚠️' },
+};
+
+export function MilestoneDetailView({
+ milestone,
+ onUpdate,
+ onDelete,
+ onBack,
+ onEdit,
+}: MilestoneDetailViewProps) {
+ const [activeTab, setActiveTab] = useState('overview');
+
+ const formatDate = (date: Date | string | null) => {
+ if (!date) return 'Not set';
+ return new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ const getMilestoneStatus = () => {
+ const now = new Date();
+ const targetDate = new Date(milestone.targetDate);
+
+ if (milestone.completedAt) {
+ return 'COMPLETED';
+ } else if (now > targetDate) {
+ return 'OVERDUE';
+ } else if (milestone.startedAt) {
+ return 'IN_PROGRESS';
+ } else {
+ return 'PENDING';
+ }
+ };
+
+ const calculateProgress = () => {
+ const tasks = milestone.tasks || [];
+ if (tasks.length === 0) return 0;
+ const completedTasks = tasks.filter((task) => task.status === 'DONE').length;
+ return Math.round((completedTasks / tasks.length) * 100);
+ };
+
+ const getTaskStats = () => {
+ const tasks = milestone.tasks || [];
+ return {
+ total: tasks.length,
+ todo: tasks.filter((task) => task.status === 'TODO').length,
+ inProgress: tasks.filter((task) => task.status === 'IN_PROGRESS').length,
+ review: tasks.filter((task) => task.status === 'REVIEW').length,
+ done: tasks.filter((task) => task.status === 'DONE').length,
+ cancelled: tasks.filter((task) => task.status === 'CANCELLED').length,
+ };
+ };
+
+ const getDaysRemaining = () => {
+ if (milestone.completedAt) return 0;
+ const now = new Date();
+ const targetDate = new Date(milestone.targetDate);
+ const diffTime = targetDate.getTime() - now.getTime();
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ return Math.max(0, diffDays);
+ };
+
+ const renderStatusBadge = (status: string) => {
+ const config = milestoneStatusConfig[status as keyof typeof milestoneStatusConfig];
+ return (
+
+ {config.icon} {config.label}
+
+ );
+ };
+
+ const renderTaskItem = (task: CampaignTask) => {
+ const config = {
+ TODO: { label: 'To Do', color: 'bg-gray-500', icon: '⏳' },
+ IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-500', icon: '🔄' },
+ REVIEW: { label: 'Review', color: 'bg-yellow-500', icon: '👀' },
+ DONE: { label: 'Done', color: 'bg-green-500', icon: '✅' },
+ CANCELLED: { label: 'Cancelled', color: 'bg-red-500', icon: '❌' },
+ }[task.status];
+
+ return (
+
+
+
{config.icon}
+
+
{task.title}
+ {task.description && (
+
{task.description}
+ )}
+
+
+
+ {task.assignee && (
+
+
+
+ {task.assignee.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+ )}
+
+ {config.label}
+
+ {task.dueDate && (
+
+ Due {formatDate(task.dueDate)}
+
+ )}
+
+
+ );
+ };
+
+ const currentStatus = getMilestoneStatus();
+ const taskStats = getTaskStats();
+ const progress = calculateProgress();
+ const daysRemaining = getDaysRemaining();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Back to Campaign
+
+
+
+
{milestone.title}
+
+ {renderStatusBadge(currentStatus)}
+ •
+
+ Due {formatDate(milestone.targetDate)}
+
+ {daysRemaining > 0 && (
+ <>
+ •
+
+ {daysRemaining} days remaining
+
+ >
+ )}
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+ Edit Milestone
+ onDelete(milestone.id)}
+ className="text-destructive"
+ >
+ Delete Milestone
+
+
+
+
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
Progress
+
{progress}%
+
+
+
+
+
+
+
+
+
+
+
+
Tasks
+
{taskStats.total}
+
+
+
+
+
+
+
+
+
+
+
Days Left
+
+ {daysRemaining}
+
+
+
+
+
+
+
+
+
+
+
+
Team Members
+
{milestone.teamMembers?.length || 0}
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ Overview
+ Tasks ({taskStats.total})
+ Timeline
+ Team
+
+
+
+
+
+
+ 📋 Milestone Information
+
+
+ {milestone.description && (
+
+
Description
+
{milestone.description}
+
+ )}
+
+
+ Status:
+ {renderStatusBadge(currentStatus)}
+
+
+ Target Date:
+ {formatDate(milestone.targetDate)}
+
+
+ Started:
+
+ {milestone.startedAt
+ ? formatDate(milestone.startedAt)
+ : 'Not started'}
+
+
+
+ Completed:
+
+ {milestone.completedAt
+ ? formatDate(milestone.completedAt)
+ : 'Not completed'}
+
+
+
+ Created:
+ {formatDate(milestone.createdAt)}
+
+
+
+
+
+
+
+ 📊 Task Progress
+
+
+
+
+ To Do
+ {taskStats.todo}
+
+
+ In Progress
+ {taskStats.inProgress}
+
+
+ Review
+ {taskStats.review}
+
+
+ Done
+ {taskStats.done}
+
+
+
+
+ Total Progress
+ {progress}%
+
+
+
+
+
+
+ {/* Timeline Overview */}
+
+
+ ⏰ Timeline Overview
+
+
+
+
+
+
+ {formatDate(milestone.createdAt)}
+
+
+
+ {milestone.startedAt && (
+
+
+
+ {formatDate(milestone.startedAt)}
+
+
+ )}
+
+
+
+
+ {formatDate(milestone.targetDate)}
+
+
+
+ {milestone.completedAt && (
+
+
+
+ {formatDate(milestone.completedAt)}
+
+
+ )}
+
+
+
+
+
+
+
+
📋 Milestone Tasks ({taskStats.total})
+
+
+ Add Task
+
+
+
+
+ {milestone.tasks && milestone.tasks.length > 0 ? (
+ milestone.tasks.map(renderTaskItem)
+ ) : (
+
+
+
No tasks yet
+
+ Add tasks to track progress on this milestone
+
+
+
+ Add First Task
+
+
+ )}
+
+
+
+
+
+
+ 📅 Detailed Timeline
+
+
+
+
+
+
+
Milestone Created
+
+ {formatDate(milestone.createdAt)}
+
+
+
+
+ {milestone.startedAt && (
+
+
+
+
Work Started
+
+ {formatDate(milestone.startedAt)}
+
+
+
+ )}
+
+
+
+
+
Target Completion
+
+ {formatDate(milestone.targetDate)}
+
+ {daysRemaining > 0 && (
+
+ {daysRemaining} days remaining
+
+ )}
+
+
+
+ {milestone.completedAt && (
+
+
+
+
Milestone Completed
+
+ {formatDate(milestone.completedAt)}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ 👥 Team Members ({milestone.teamMembers?.length || 0})
+
+
+
+ Add Member
+
+
+
+ {milestone.teamMembers && milestone.teamMembers.length > 0 ? (
+
+ {milestone.teamMembers.map((member: any) => (
+
+
+
+
+
+
+ {member.user?.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+
+
+ {member.user?.name || 'Unknown'}
+
+
{member.role}
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
No team members assigned
+
+ Assign team members to work on this milestone
+
+
+
+ Add First Member
+
+
+ )}
+
+
+
+ );
+}
diff --git a/components/campaigns/task-detail-view.tsx b/components/campaigns/task-detail-view.tsx
new file mode 100644
index 00000000..9ba8ec1c
--- /dev/null
+++ b/components/campaigns/task-detail-view.tsx
@@ -0,0 +1,416 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Separator } from '@/components/ui/separator';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ MoreHorizontal,
+ Edit,
+ Trash2,
+ Plus,
+ CheckSquare,
+ Clock,
+ User,
+ Calendar,
+ Flag,
+ MessageSquare,
+ FileText,
+ ArrowLeft,
+} from 'lucide-react';
+import type { CampaignTask } from '@/types/campaign';
+
+interface TaskDetailViewProps {
+ task: CampaignTask;
+ onUpdate: (taskId: string, data: Partial) => void;
+ onDelete: (taskId: string) => void;
+ onBack: () => void;
+ onEdit: () => void;
+ onAddSubtask: (parentTaskId: string) => void;
+}
+
+const taskStatusConfig = {
+ TODO: { label: 'To Do', color: 'bg-gray-500', icon: '⏳' },
+ IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-500', icon: '🔄' },
+ REVIEW: { label: 'Review', color: 'bg-yellow-500', icon: '👀' },
+ DONE: { label: 'Done', color: 'bg-green-500', icon: '✅' },
+ CANCELLED: { label: 'Cancelled', color: 'bg-red-500', icon: '❌' },
+};
+
+const taskPriorityConfig = {
+ NO_PRIORITY: { label: 'No Priority', color: 'bg-gray-400' },
+ LOW: { label: 'Low', color: 'bg-green-400' },
+ MEDIUM: { label: 'Medium', color: 'bg-yellow-400' },
+ HIGH: { label: 'High', color: 'bg-orange-400' },
+ URGENT: { label: 'Urgent', color: 'bg-red-500' },
+};
+
+export function TaskDetailView({
+ task,
+ onUpdate,
+ onDelete,
+ onBack,
+ onEdit,
+ onAddSubtask,
+}: TaskDetailViewProps) {
+ const [activeTab, setActiveTab] = useState('overview');
+
+ const formatDate = (date: Date | string | null) => {
+ if (!date) return 'Not set';
+ return new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ const calculateProgress = () => {
+ const subtasks = task.subtasks || [];
+ if (subtasks.length === 0) return 0;
+ const completedSubtasks = subtasks.filter((subtask) => subtask.status === 'DONE').length;
+ return Math.round((completedSubtasks / subtasks.length) * 100);
+ };
+
+ const getSubtaskStats = () => {
+ const subtasks = task.subtasks || [];
+ return {
+ total: subtasks.length,
+ todo: subtasks.filter((subtask) => subtask.status === 'TODO').length,
+ inProgress: subtasks.filter((subtask) => subtask.status === 'IN_PROGRESS').length,
+ review: subtasks.filter((subtask) => subtask.status === 'REVIEW').length,
+ done: subtasks.filter((subtask) => subtask.status === 'DONE').length,
+ cancelled: subtasks.filter((subtask) => subtask.status === 'CANCELLED').length,
+ };
+ };
+
+ const renderStatusBadge = (status: CampaignTask['status']) => {
+ const config = taskStatusConfig[status];
+ return (
+
+ {config.icon} {config.label}
+
+ );
+ };
+
+ const renderPriorityBadge = (priority: CampaignTask['priority']) => {
+ if (priority === 'NO_PRIORITY')
+ return No Priority ;
+ const config = taskPriorityConfig[priority];
+ return (
+
+
+ {config.label}
+
+ );
+ };
+
+ const renderSubtaskItem = (subtask: CampaignTask) => {
+ const config = taskStatusConfig[subtask.status];
+ return (
+
+
+
{config.icon}
+
+
{subtask.title}
+ {subtask.description && (
+
{subtask.description}
+ )}
+
+
+
+ {subtask.assignee && (
+
+
+
+ {subtask.assignee.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+ )}
+
+ {config.label}
+
+
+
+
+
+
+
+
+ onUpdate(subtask.id, { status: 'DONE' })}>
+ Mark as Done
+
+ onDelete(subtask.id)}
+ className="text-destructive"
+ >
+ Delete Subtask
+
+
+
+
+
+ );
+ };
+
+ const subtaskStats = getSubtaskStats();
+ const progress = calculateProgress();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Back to Campaign
+
+
+
+
{task.title}
+
+ {renderStatusBadge(task.status)}
+ •
+ {renderPriorityBadge(task.priority)}
+ {task.dueDate && (
+ <>
+ •
+
+ Due {formatDate(task.dueDate)}
+
+ >
+ )}
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+ Edit Task
+ onDelete(task.id)}
+ className="text-destructive"
+ >
+ Delete Task
+
+
+
+
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
Progress
+
{progress}%
+
+
+
+
+
+
+
+
+
+
+
+
Subtasks
+
{subtaskStats.total}
+
+
+
+
+
+
+
+
+
+
+
Assignee
+
+ {task.assignee ? task.assignee.name : 'Unassigned'}
+
+
+
+
+
+
+
+
+
+
+
+
Due Date
+
+ {task.dueDate ? formatDate(task.dueDate) : 'Not set'}
+
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ Overview
+ Subtasks ({subtaskStats.total})
+ Activity
+
+
+
+
+
+
+ 📋 Task Information
+
+
+ {task.description && (
+
+
Description
+
{task.description}
+
+ )}
+
+
+ Status:
+ {renderStatusBadge(task.status)}
+
+
+ Priority:
+ {renderPriorityBadge(task.priority)}
+
+
+
Assignee:
+
+ {task.assignee ? (
+
+
+
+
+ {task.assignee.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+
{task.assignee.name}
+
+ ) : (
+ 'Unassigned'
+ )}
+
+
+
+ Due Date:
+ {formatDate(task.dueDate)}
+
+
+ Created:
+ {formatDate(task.createdAt)}
+
+
+ Updated:
+ {formatDate(task.updatedAt)}
+
+
+
+
+
+
+
+ 📊 Subtask Progress
+
+
+
+
+ To Do
+ {subtaskStats.todo}
+
+
+ In Progress
+ {subtaskStats.inProgress}
+
+
+ Review
+ {subtaskStats.review}
+
+
+ Done
+ {subtaskStats.done}
+
+
+
+
+ Total Progress
+ {progress}%
+
+
+
+
+
+
+
+
+
+
📋 Subtasks ({subtaskStats.total})
+
onAddSubtask(task.id)}>
+
+ Add Subtask
+
+
+
+ {task.subtasks && task.subtasks.length > 0 ? (
+ task.subtasks.map(renderSubtaskItem)
+ ) : (
+
+
No subtasks yet
+
+ Add subtasks to break down this task into smaller components
+
+
onAddSubtask(task.id)}>
+
+ Add First Subtask
+
+
+ )}
+
+
+
+
+
+
+ 📝 Activity Log
+
+
+
+
+
Activity tracking coming soon
+
Track comments, status changes, and updates
+
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/task-management/index.ts b/components/campaigns/task-management/index.ts
new file mode 100644
index 00000000..8338ab8c
--- /dev/null
+++ b/components/campaigns/task-management/index.ts
@@ -0,0 +1,6 @@
+export { TaskBoard } from './task-board';
+export { TaskList } from './task-list';
+export { TaskAnalytics } from './task-analytics';
+export { TaskCreationModal } from './task-creation-modal';
+export { TaskManagementContainer } from './task-management-container';
+export { TaskManagementPage } from './task-management-page';
diff --git a/components/campaigns/task-management/task-analytics.tsx b/components/campaigns/task-management/task-analytics.tsx
new file mode 100644
index 00000000..17b283cd
--- /dev/null
+++ b/components/campaigns/task-management/task-analytics.tsx
@@ -0,0 +1,452 @@
+'use client';
+
+import { useMemo } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { CheckSquare, Clock, TrendingUp, AlertTriangle, Users, Calendar } from 'lucide-react';
+import type { CampaignTask } from '@/types/campaign';
+
+interface TaskAnalyticsProps {
+ tasks: CampaignTask[];
+}
+
+export function TaskAnalytics({ tasks }: TaskAnalyticsProps) {
+ const analytics = useMemo(() => {
+ const totalTasks = tasks.length;
+ const completedTasks = tasks.filter((task) => task.status === 'DONE').length;
+ const inProgressTasks = tasks.filter((task) => task.status === 'IN_PROGRESS').length;
+ const overdueTasks = tasks.filter((task) => {
+ if (!task.dueDate) return false;
+ return new Date(task.dueDate) < new Date();
+ }).length;
+
+ const totalEstimatedHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
+ const completedHours = tasks
+ .filter((task) => task.status === 'DONE')
+ .reduce((sum, task) => sum + (task.estimatedHours || 0), 0);
+
+ const progressPercentage =
+ totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
+ const timeProgressPercentage =
+ totalEstimatedHours > 0 ? Math.round((completedHours / totalEstimatedHours) * 100) : 0;
+
+ // Status breakdown
+ const statusBreakdown = {
+ TODO: tasks.filter((task) => task.status === 'TODO').length,
+ IN_PROGRESS: tasks.filter((task) => task.status === 'IN_PROGRESS').length,
+ REVIEW: tasks.filter((task) => task.status === 'REVIEW').length,
+ DONE: tasks.filter((task) => task.status === 'DONE').length,
+ CANCELLED: tasks.filter((task) => task.status === 'CANCELLED').length,
+ };
+
+ // Priority breakdown
+ const priorityBreakdown = {
+ NO_PRIORITY: tasks.filter((task) => task.priority === 'NO_PRIORITY').length,
+ LOW: tasks.filter((task) => task.priority === 'LOW').length,
+ MEDIUM: tasks.filter((task) => task.priority === 'MEDIUM').length,
+ HIGH: tasks.filter((task) => task.priority === 'HIGH').length,
+ URGENT: tasks.filter((task) => task.priority === 'URGENT').length,
+ };
+
+ // Assignee workload
+ const assigneeWorkload = tasks.reduce(
+ (acc, task) => {
+ if (task.assignee) {
+ const assigneeId = task.assignee.id;
+ if (!acc[assigneeId]) {
+ acc[assigneeId] = {
+ name: task.assignee.name || 'Unknown',
+ total: 0,
+ completed: 0,
+ inProgress: 0,
+ overdue: 0,
+ };
+ }
+ acc[assigneeId].total++;
+ if (task.status === 'DONE') acc[assigneeId].completed++;
+ if (task.status === 'IN_PROGRESS') acc[assigneeId].inProgress++;
+ if (task.dueDate && new Date(task.dueDate) < new Date()) {
+ acc[assigneeId].overdue++;
+ }
+ }
+ return acc;
+ },
+ {} as Record
+ );
+
+ // Timeline analysis
+ const now = new Date();
+ const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
+ const nextMonth = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
+
+ const dueThisWeek = tasks.filter((task) => {
+ if (!task.dueDate) return false;
+ const dueDate = new Date(task.dueDate);
+ return dueDate >= now && dueDate <= nextWeek;
+ }).length;
+
+ const dueThisMonth = tasks.filter((task) => {
+ if (!task.dueDate) return false;
+ const dueDate = new Date(task.dueDate);
+ return dueDate >= now && dueDate <= nextMonth;
+ }).length;
+
+ return {
+ totalTasks,
+ completedTasks,
+ inProgressTasks,
+ overdueTasks,
+ totalEstimatedHours,
+ completedHours,
+ progressPercentage,
+ timeProgressPercentage,
+ statusBreakdown,
+ priorityBreakdown,
+ assigneeWorkload,
+ dueThisWeek,
+ dueThisMonth,
+ };
+ }, [tasks]);
+
+ const renderStatusBar = (status: string, count: number, total: number) => {
+ const percentage = total > 0 ? Math.round((count / total) * 100) : 0;
+ const colorMap: Record = {
+ TODO: 'bg-gray-500',
+ IN_PROGRESS: 'bg-blue-500',
+ REVIEW: 'bg-yellow-500',
+ DONE: 'bg-green-500',
+ CANCELLED: 'bg-red-500',
+ };
+
+ return (
+
+
+ {status}
+ {count} tasks
+
+
+
{percentage}%
+
+ );
+ };
+
+ const renderPriorityBar = (priority: string, count: number, total: number) => {
+ const percentage = total > 0 ? Math.round((count / total) * 100) : 0;
+ const colorMap: Record = {
+ NO_PRIORITY: 'bg-gray-400',
+ LOW: 'bg-green-400',
+ MEDIUM: 'bg-yellow-400',
+ HIGH: 'bg-orange-400',
+ URGENT: 'bg-red-500',
+ };
+
+ return (
+
+
+ {priority.replace('_', ' ')}
+ {count} tasks
+
+
+
{percentage}%
+
+ );
+ };
+
+ return (
+
+
+
Task Analytics
+
Track progress and performance metrics
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
Total Progress
+
{analytics.progressPercentage}%
+
+
+
+
+ {analytics.completedTasks} of {analytics.totalTasks} tasks completed
+
+
+
+
+
+
+
+
+
+
Time Progress
+
{analytics.timeProgressPercentage}%
+
+
+
+
+ {analytics.completedHours}h of {analytics.totalEstimatedHours}h completed
+
+
+
+
+
+
+
+
+
+
Overdue Tasks
+
{analytics.overdueTasks}
+
+
+
+ {analytics.overdueTasks > 0 ? 'Requires attention' : 'All on track'}
+
+
+
+
+
+
+
+
+
+
In Progress
+
{analytics.inProgressTasks}
+
+
+ Currently being worked on
+
+
+
+
+ {/* Detailed Analytics */}
+
+
+ Overview
+ Status Breakdown
+ Priority Analysis
+ Team Workload
+ Timeline
+
+
+
+
+
+
+ Task Status Distribution
+
+
+ {Object.entries(analytics.statusBreakdown).map(([status, count]) =>
+ renderStatusBar(status, count, analytics.totalTasks)
+ )}
+
+
+
+
+
+ Priority Distribution
+
+
+ {Object.entries(analytics.priorityBreakdown).map(([priority, count]) =>
+ renderPriorityBar(priority, count, analytics.totalTasks)
+ )}
+
+
+
+
+
+
+
+
+ Detailed Status Analysis
+
+
+
+ {Object.entries(analytics.statusBreakdown).map(([status, count]) => {
+ const percentage =
+ analytics.totalTasks > 0
+ ? Math.round((count / analytics.totalTasks) * 100)
+ : 0;
+ return (
+
+
+ {status.replace('_', ' ')}
+ {count} tasks
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ Priority Analysis
+
+
+
+ {Object.entries(analytics.priorityBreakdown).map(([priority, count]) => {
+ const percentage =
+ analytics.totalTasks > 0
+ ? Math.round((count / analytics.totalTasks) * 100)
+ : 0;
+ return (
+
+
+ {priority.replace('_', ' ')}
+ {count} tasks
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ Team Workload Distribution
+
+
+ {Object.keys(analytics.assigneeWorkload).length > 0 ? (
+
+ {Object.entries(analytics.assigneeWorkload).map(([assigneeId, data]) => (
+
+
+
{data.name}
+ {data.total} tasks
+
+
+
+ Completed:
+
+ {data.completed}
+
+
+
+ In Progress:
+
+ {data.inProgress}
+
+
+
+ Overdue:
+
+ {data.overdue}
+
+
+
+
0
+ ? Math.round((data.completed / data.total) * 100)
+ : 0
+ }
+ className="mt-2"
+ />
+
+ ))}
+
+ ) : (
+
+
+
No assigned tasks yet
+
+ )}
+
+
+
+
+
+
+
+
+ Upcoming Deadlines
+
+
+
+
+
+ Due this week
+
+
{analytics.dueThisWeek}
+
+
+
+
+ Due this month
+
+
{analytics.dueThisMonth}
+
+
+
+
{analytics.overdueTasks}
+
+
+
+
+
+
+ Time Tracking
+
+
+
+ Total Estimated
+ {analytics.totalEstimatedHours}h
+
+
+ Completed
+
+ {analytics.completedHours}h
+
+
+
+ Remaining
+
+ {analytics.totalEstimatedHours - analytics.completedHours}h
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/task-management/task-board.tsx b/components/campaigns/task-management/task-board.tsx
new file mode 100644
index 00000000..9f5c7b6e
--- /dev/null
+++ b/components/campaigns/task-management/task-board.tsx
@@ -0,0 +1,242 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Plus, MoreHorizontal, User, Calendar, Flag } from 'lucide-react';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import type { CampaignTask } from '@/types/campaign';
+
+interface TaskBoardProps {
+ tasks: CampaignTask[];
+ onTaskUpdate: (taskId: string, data: Partial) => void;
+ onTaskCreate: () => void;
+ onTaskClick: (task: CampaignTask) => void;
+}
+
+const taskStatusConfig = {
+ TODO: { label: 'To Do', color: 'bg-gray-500', bgLight: 'bg-gray-50', border: 'border-gray-200' },
+ IN_PROGRESS: {
+ label: 'In Progress',
+ color: 'bg-blue-500',
+ bgLight: 'bg-blue-50',
+ border: 'border-blue-200',
+ },
+ REVIEW: {
+ label: 'Review',
+ color: 'bg-yellow-500',
+ bgLight: 'bg-yellow-50',
+ border: 'border-yellow-200',
+ },
+ DONE: {
+ label: 'Done',
+ color: 'bg-green-500',
+ bgLight: 'bg-green-50',
+ border: 'border-green-200',
+ },
+ CANCELLED: {
+ label: 'Cancelled',
+ color: 'bg-red-500',
+ bgLight: 'bg-red-50',
+ border: 'border-red-200',
+ },
+};
+
+const taskPriorityConfig = {
+ NO_PRIORITY: { label: 'No Priority', color: 'bg-gray-400' },
+ LOW: { label: 'Low', color: 'bg-green-400' },
+ MEDIUM: { label: 'Medium', color: 'bg-yellow-400' },
+ HIGH: { label: 'High', color: 'bg-orange-400' },
+ URGENT: { label: 'Urgent', color: 'bg-red-500' },
+};
+
+export function TaskBoard({ tasks, onTaskUpdate, onTaskCreate, onTaskClick }: TaskBoardProps) {
+ const [draggedTask, setDraggedTask] = useState(null);
+
+ const groupedTasks = useMemo(() => {
+ const groups: Record = {
+ TODO: [],
+ IN_PROGRESS: [],
+ REVIEW: [],
+ DONE: [],
+ CANCELLED: [],
+ };
+
+ tasks.forEach((task) => {
+ if (groups[task.status]) {
+ groups[task.status].push(task);
+ }
+ });
+
+ return groups;
+ }, [tasks]);
+
+ const handleDragStart = (e: React.DragEvent, taskId: string) => {
+ setDraggedTask(taskId);
+ e.dataTransfer.effectAllowed = 'move';
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ };
+
+ const handleDrop = (e: React.DragEvent, targetStatus: string) => {
+ e.preventDefault();
+ if (draggedTask) {
+ onTaskUpdate(draggedTask, { status: targetStatus as CampaignTask['status'] });
+ setDraggedTask(null);
+ }
+ };
+
+ const renderTaskCard = (task: CampaignTask) => {
+ const priorityConfig = taskPriorityConfig[task.priority];
+
+ return (
+ handleDragStart(e, task.id)}
+ onClick={() => onTaskClick(task)}
+ >
+
+
+
+
{task.title}
+
+
+
+
+
+
+
+ onTaskClick(task)}>
+ View Details
+
+ onTaskUpdate(task.id, { status: 'DONE' })}
+ >
+ Mark as Done
+
+
+
+
+
+ {task.description && (
+
+ {task.description}
+
+ )}
+
+
+
+ {task.assignee && (
+
+
+
+ {task.assignee.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+ )}
+ {task.priority !== 'NO_PRIORITY' && (
+
+
+ {priorityConfig.label}
+
+ )}
+
+
+ {task.dueDate && (
+
+
+ {new Date(task.dueDate).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ })}
+
+ )}
+
+
+ {task.subtasks && task.subtasks.length > 0 && (
+
+
+ {task.subtasks.length} subtask{task.subtasks.length !== 1 ? 's' : ''}
+
+ )}
+
+
+
+ );
+ };
+
+ const renderStatusColumn = (status: string, tasks: CampaignTask[]) => {
+ const config = taskStatusConfig[status as keyof typeof taskStatusConfig];
+
+ return (
+ handleDrop(e, status)}
+ >
+
+
+
+
{config.label}
+
+ {tasks.length}
+
+
+ {status === 'TODO' && (
+
+
+
+ )}
+
+
+
+ {tasks.map(renderTaskCard)}
+ {tasks.length === 0 && (
+
+ )}
+
+
+ );
+ };
+
+ return (
+
+
+
+
Task Board
+
Manage and track campaign tasks
+
+
+
+ New Task
+
+
+
+
+ {Object.entries(groupedTasks).map(([status, statusTasks]) =>
+ renderStatusColumn(status, statusTasks)
+ )}
+
+
+ );
+}
diff --git a/components/campaigns/task-management/task-creation-modal.tsx b/components/campaigns/task-management/task-creation-modal.tsx
new file mode 100644
index 00000000..c8e87ef3
--- /dev/null
+++ b/components/campaigns/task-management/task-creation-modal.tsx
@@ -0,0 +1,297 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Calendar } from '@/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { CalendarIcon, Check, ChevronsUpDown } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { format } from 'date-fns';
+import type { CampaignTask, CampaignMember } from '@/types/campaign';
+
+interface TaskCreationModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSubmit: (taskData: Partial) => void;
+ campaignMembers: CampaignMember[];
+ parentTask?: CampaignTask | null;
+ loading?: boolean;
+}
+
+const taskPriorityOptions = [
+ { value: 'NO_PRIORITY', label: 'No Priority' },
+ { value: 'LOW', label: 'Low' },
+ { value: 'MEDIUM', label: 'Medium' },
+ { value: 'HIGH', label: 'High' },
+ { value: 'URGENT', label: 'Urgent' },
+];
+
+const taskStatusOptions = [
+ { value: 'TODO', label: 'To Do' },
+ { value: 'IN_PROGRESS', label: 'In Progress' },
+ { value: 'REVIEW', label: 'Review' },
+ { value: 'DONE', label: 'Done' },
+ { value: 'CANCELLED', label: 'Cancelled' },
+];
+
+export function TaskCreationModal({
+ open,
+ onOpenChange,
+ onSubmit,
+ campaignMembers,
+ parentTask,
+ loading = false,
+}: TaskCreationModalProps) {
+ const [formData, setFormData] = useState({
+ title: '',
+ description: '',
+ status: 'TODO' as CampaignTask['status'],
+ priority: 'NO_PRIORITY' as CampaignTask['priority'],
+ assigneeId: '',
+ dueDate: null as Date | null,
+ estimatedHours: '',
+ });
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!formData.title.trim()) return;
+
+ const taskData: Partial = {
+ title: formData.title.trim(),
+ description: formData.description.trim() || undefined,
+ status: formData.status,
+ priority: formData.priority,
+ assigneeId: formData.assigneeId || undefined,
+ dueDate: formData.dueDate || undefined,
+ estimatedHours: formData.estimatedHours ? parseInt(formData.estimatedHours) : undefined,
+ parentTaskId: parentTask?.id,
+ };
+
+ onSubmit(taskData);
+ };
+
+ const resetForm = () => {
+ setFormData({
+ title: '',
+ description: '',
+ status: 'TODO',
+ priority: 'NO_PRIORITY',
+ assigneeId: '',
+ dueDate: null,
+ estimatedHours: '',
+ });
+ };
+
+ const handleOpenChange = (newOpen: boolean) => {
+ if (!newOpen) {
+ resetForm();
+ }
+ onOpenChange(newOpen);
+ };
+
+ return (
+
+
+
+ {parentTask ? 'Create Subtask' : 'Create New Task'}
+
+ {parentTask
+ ? `Add a subtask to "${parentTask.title}"`
+ : 'Create a new task for this campaign'}
+
+
+
+
+
+
+ Task Title *
+
+ setFormData((prev) => ({ ...prev, title: e.target.value }))
+ }
+ placeholder="Enter task title"
+ required
+ />
+
+
+
+ Status
+
+ setFormData((prev) => ({
+ ...prev,
+ status: value as CampaignTask['status'],
+ }))
+ }
+ >
+
+
+
+
+ {taskStatusOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+
+ Description
+
+ setFormData((prev) => ({ ...prev, description: e.target.value }))
+ }
+ placeholder="Describe what needs to be done..."
+ rows={3}
+ />
+
+
+
+
+ Priority
+
+ setFormData((prev) => ({
+ ...prev,
+ priority: value as CampaignTask['priority'],
+ }))
+ }
+ >
+
+
+
+
+ {taskPriorityOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+
Assignee
+
+ setFormData((prev) => ({ ...prev, assigneeId: value }))
+ }
+ >
+
+
+
+
+ Unassigned
+ {campaignMembers.map((member) => (
+
+
+ {member.user.name}
+ ({member.role})
+
+
+ ))}
+
+
+
+
+
+
+
+
Due Date
+
+
+
+
+ {formData.dueDate ? format(formData.dueDate, 'PPP') : 'Pick a date'}
+
+
+
+
+ setFormData((prev) => ({ ...prev, dueDate: date }))
+ }
+ initialFocus
+ />
+
+
+
+
+
+ Estimated Hours
+
+ setFormData((prev) => ({ ...prev, estimatedHours: e.target.value }))
+ }
+ placeholder="e.g., 8"
+ />
+
+
+
+ {parentTask && (
+
+
+
+ This task will be a subtask of "{parentTask.title}"
+
+
+ )}
+
+
+ handleOpenChange(false)}
+ disabled={loading}
+ >
+ Cancel
+
+
+ {loading ? 'Creating...' : 'Create Task'}
+
+
+
+
+
+ );
+}
diff --git a/components/campaigns/task-management/task-list.tsx b/components/campaigns/task-management/task-list.tsx
new file mode 100644
index 00000000..46b9b99a
--- /dev/null
+++ b/components/campaigns/task-management/task-list.tsx
@@ -0,0 +1,420 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Plus, MoreHorizontal, Filter, Search, Flag, Calendar, User } from 'lucide-react';
+import type { CampaignTask } from '@/types/campaign';
+
+interface TaskListProps {
+ tasks: CampaignTask[];
+ onTaskUpdate: (taskId: string, data: Partial) => void;
+ onTaskDelete: (taskId: string) => void;
+ onTaskCreate: () => void;
+ onTaskClick: (task: CampaignTask) => void;
+ onBulkAction: (action: string, taskIds: string[]) => void;
+}
+
+const taskStatusConfig = {
+ TODO: { label: 'To Do', color: 'bg-gray-500' },
+ IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-500' },
+ REVIEW: { label: 'Review', color: 'bg-yellow-500' },
+ DONE: { label: 'Done', color: 'bg-green-500' },
+ CANCELLED: { label: 'Cancelled', color: 'bg-red-500' },
+};
+
+const taskPriorityConfig = {
+ NO_PRIORITY: { label: 'No Priority', color: 'bg-gray-400' },
+ LOW: { label: 'Low', color: 'bg-green-400' },
+ MEDIUM: { label: 'Medium', color: 'bg-yellow-400' },
+ HIGH: { label: 'High', color: 'bg-orange-400' },
+ URGENT: { label: 'Urgent', color: 'bg-red-500' },
+};
+
+export function TaskList({
+ tasks,
+ onTaskUpdate,
+ onTaskDelete,
+ onTaskCreate,
+ onTaskClick,
+ onBulkAction,
+}: TaskListProps) {
+ const [selectedTasks, setSelectedTasks] = useState>(new Set());
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [priorityFilter, setPriorityFilter] = useState('all');
+ const [assigneeFilter, setAssigneeFilter] = useState('all');
+
+ const filteredTasks = useMemo(() => {
+ return tasks.filter((task) => {
+ const matchesSearch =
+ task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (task.description && task.description.toLowerCase().includes(searchTerm.toLowerCase()));
+ const matchesStatus = statusFilter === 'all' || task.status === statusFilter;
+ const matchesPriority = priorityFilter === 'all' || task.priority === priorityFilter;
+ const matchesAssignee =
+ assigneeFilter === 'all' ||
+ (assigneeFilter === 'unassigned' && !task.assignee) ||
+ (task.assignee && task.assignee.id === assigneeFilter);
+
+ return matchesSearch && matchesStatus && matchesPriority && matchesAssignee;
+ });
+ }, [tasks, searchTerm, statusFilter, priorityFilter, assigneeFilter]);
+
+ const handleSelectAll = (checked: boolean) => {
+ if (checked) {
+ setSelectedTasks(new Set(filteredTasks.map((task) => task.id)));
+ } else {
+ setSelectedTasks(new Set());
+ }
+ };
+
+ const handleSelectTask = (taskId: string, checked: boolean) => {
+ const newSelected = new Set(selectedTasks);
+ if (checked) {
+ newSelected.add(taskId);
+ } else {
+ newSelected.delete(taskId);
+ }
+ setSelectedTasks(newSelected);
+ };
+
+ const handleBulkAction = (action: string) => {
+ const taskIds = Array.from(selectedTasks);
+ if (taskIds.length === 0) return;
+
+ onBulkAction(action, taskIds);
+ setSelectedTasks(new Set());
+ };
+
+ const formatDate = (date: Date | string | null) => {
+ if (!date) return 'Not set';
+ return new Date(date).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ });
+ };
+
+ const getDaysRemaining = (dueDate: Date | string | null) => {
+ if (!dueDate) return null;
+ const now = new Date();
+ const due = new Date(dueDate);
+ const diffTime = due.getTime() - now.getTime();
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ return diffDays;
+ };
+
+ const renderPriorityBadge = (priority: CampaignTask['priority']) => {
+ if (priority === 'NO_PRIORITY') return null;
+ const config = taskPriorityConfig[priority];
+ return (
+
+
+ {config.label}
+
+ );
+ };
+
+ const renderStatusBadge = (status: CampaignTask['status']) => {
+ const config = taskStatusConfig[status];
+ return {config.label} ;
+ };
+
+ const renderDueDate = (dueDate: Date | string | null) => {
+ if (!dueDate) return Not set ;
+
+ const daysRemaining = getDaysRemaining(dueDate);
+ const formattedDate = formatDate(dueDate);
+
+ if (daysRemaining === null) return formattedDate;
+
+ let className = '';
+ if (daysRemaining < 0) {
+ className = 'text-red-600 font-medium';
+ } else if (daysRemaining <= 3) {
+ className = 'text-orange-600 font-medium';
+ } else if (daysRemaining <= 7) {
+ className = 'text-yellow-600 font-medium';
+ }
+
+ return (
+
+
+ {formattedDate}
+ {daysRemaining !== null && (
+
+ {daysRemaining < 0
+ ? `${Math.abs(daysRemaining)}d overdue`
+ : `${daysRemaining}d left`}
+
+ )}
+
+ );
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Tasks
+
+ {filteredTasks.length} of {tasks.length} tasks
+
+
+
+
+ New Task
+
+
+
+ {/* Filters and Search */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+
+
+
+ All Status
+ {Object.entries(taskStatusConfig).map(([value, config]) => (
+
+ {config.label}
+
+ ))}
+
+
+
+
+
+
+
+
+ All Priority
+ {Object.entries(taskPriorityConfig).map(([value, config]) => (
+
+ {config.label}
+
+ ))}
+
+
+
+
+
+
+
+
+ All Assignees
+ Unassigned
+ {/* In a real app, you'd map through campaign members here */}
+
+
+
+
+
+ {/* Bulk Actions */}
+ {selectedTasks.size > 0 && (
+
+
+ {selectedTasks.size} task{selectedTasks.size !== 1 ? 's' : ''} selected
+
+
+ handleBulkAction('status', Array.from(selectedTasks))}
+ >
+ Change Status
+
+ handleBulkAction('assign', Array.from(selectedTasks))}
+ >
+ Reassign
+
+ handleBulkAction('delete', Array.from(selectedTasks))}
+ >
+ Delete
+
+
+
+ )}
+
+ {/* Task Table */}
+
+
+
+
+
+ 0
+ }
+ onCheckedChange={handleSelectAll}
+ />
+
+ Task
+ Status
+ Priority
+ Assignee
+ Due Date
+ Subtasks
+
+
+
+
+ {filteredTasks.map((task) => (
+
+
+
+ handleSelectTask(task.id, checked as boolean)
+ }
+ />
+
+
+
+
onTaskClick(task)}
+ >
+ {task.title}
+
+ {task.description && (
+
+ {task.description}
+
+ )}
+
+
+ {renderStatusBadge(task.status)}
+ {renderPriorityBadge(task.priority)}
+
+ {task.assignee ? (
+
+
+
+
+ {task.assignee.name?.slice(0, 2).toUpperCase() || 'U'}
+
+
+
{task.assignee.name}
+
+ ) : (
+ Unassigned
+ )}
+
+ {renderDueDate(task.dueDate)}
+
+ {task.subtasks && task.subtasks.length > 0 ? (
+
+ {task.subtasks.length} subtask
+ {task.subtasks.length !== 1 ? 's' : ''}
+
+ ) : (
+ -
+ )}
+
+
+
+
+
+
+
+
+
+ onTaskClick(task)}>
+ View Details
+
+ onTaskUpdate(task.id, { status: 'DONE' })}
+ >
+ Mark as Done
+
+ onTaskDelete(task.id)}
+ className="text-destructive"
+ >
+ Delete Task
+
+
+
+
+
+ ))}
+
+
+
+ {filteredTasks.length === 0 && (
+
+
No tasks found
+ {searchTerm ||
+ statusFilter !== 'all' ||
+ priorityFilter !== 'all' ||
+ assigneeFilter !== 'all' ? (
+
{
+ setSearchTerm('');
+ setStatusFilter('all');
+ setPriorityFilter('all');
+ setAssigneeFilter('all');
+ }}
+ >
+ Clear filters
+
+ ) : (
+
+
+ Create your first task
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/components/campaigns/task-management/task-management-container.tsx b/components/campaigns/task-management/task-management-container.tsx
new file mode 100644
index 00000000..cc5cd8ca
--- /dev/null
+++ b/components/campaigns/task-management/task-management-container.tsx
@@ -0,0 +1,228 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { Button } from '@/components/ui/button';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/dialog';
+import { Kanban, List, BarChart3, Plus } from 'lucide-react';
+import { TaskBoard } from './task-board';
+import { TaskList } from './task-list';
+import { TaskAnalytics } from './task-analytics';
+import { TaskCreationModal } from './task-creation-modal';
+import type { CampaignTask, CampaignMember } from '@/types/campaign';
+
+interface TaskManagementContainerProps {
+ tasks: CampaignTask[];
+ campaignMembers: CampaignMember[];
+ onTaskCreate: (taskData: Partial) => Promise;
+ onTaskUpdate: (taskId: string, data: Partial) => Promise;
+ onTaskDelete: (taskId: string) => Promise;
+ onBulkAction: (action: string, taskIds: string[]) => Promise;
+}
+
+type ViewMode = 'board' | 'list' | 'analytics';
+
+export function TaskManagementContainer({
+ tasks,
+ campaignMembers,
+ onTaskCreate,
+ onTaskUpdate,
+ onTaskDelete,
+ onBulkAction,
+}: TaskManagementContainerProps) {
+ const [viewMode, setViewMode] = useState('board');
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [selectedTask, setSelectedTask] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const topLevelTasks = useMemo(() => {
+ return tasks.filter((task) => !task.parentTaskId);
+ }, [tasks]);
+
+ const handleTaskCreate = async (taskData: Partial) => {
+ try {
+ setLoading(true);
+ await onTaskCreate(taskData);
+ setShowCreateModal(false);
+ } catch (error) {
+ console.error('Failed to create task:', error);
+ // In a real app, you'd show an error toast here
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleTaskUpdate = async (taskId: string, data: Partial) => {
+ try {
+ setLoading(true);
+ await onTaskUpdate(taskId, data);
+ } catch (error) {
+ console.error('Failed to update task:', error);
+ // In a real app, you'd show an error toast here
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleTaskDelete = async (taskId: string) => {
+ try {
+ setLoading(true);
+ await onTaskDelete(taskId);
+ } catch (error) {
+ console.error('Failed to delete task:', error);
+ // In a real app, you'd show an error toast here
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleBulkAction = async (action: string, taskIds: string[]) => {
+ try {
+ setLoading(true);
+ await onBulkAction(action, taskIds);
+ } catch (error) {
+ console.error('Failed to perform bulk action:', error);
+ // In a real app, you'd show an error toast here
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleTaskClick = (task: CampaignTask) => {
+ setSelectedTask(task);
+ // In a real app, you'd navigate to the task detail view or open a modal
+ console.log('Task clicked:', task);
+ };
+
+ const handleCreateSubtask = (parentTask: CampaignTask) => {
+ setSelectedTask(parentTask);
+ setShowCreateModal(true);
+ };
+
+ const renderViewModeButton = (mode: ViewMode, icon: React.ReactNode, label: string) => (
+ setViewMode(mode)}
+ className="flex items-center gap-2"
+ >
+ {icon}
+ {label}
+
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
Task Management
+
+ Manage and track all campaign tasks and subtasks
+
+
+
+
setShowCreateModal(true)}>
+
+ New Task
+
+
+
+
+ {/* View Mode Toggle */}
+
+ {renderViewModeButton('board', , 'Board')}
+ {renderViewModeButton('list',
, 'List')}
+ {renderViewModeButton('analytics', , 'Analytics')}
+
+
+ {/* Task Statistics */}
+
+
+
+
+
+
+ {tasks.filter((task) => task.status === 'DONE').length}
+
+
+
+
+
+
+ {tasks.filter((task) => task.status === 'IN_PROGRESS').length}
+
+
+
+
+
+
+ {
+ tasks.filter((task) => {
+ if (!task.dueDate) return false;
+ return new Date(task.dueDate) < new Date();
+ }).length
+ }
+
+
+
+
+ {/* View Content */}
+ {viewMode === 'board' && (
+
setShowCreateModal(true)}
+ onTaskClick={handleTaskClick}
+ />
+ )}
+
+ {viewMode === 'list' && (
+ setShowCreateModal(true)}
+ onTaskClick={handleTaskClick}
+ onBulkAction={handleBulkAction}
+ />
+ )}
+
+ {viewMode === 'analytics' && }
+
+ {/* Task Creation Modal */}
+
+
+ );
+}
diff --git a/components/campaigns/task-management/task-management-page.tsx b/components/campaigns/task-management/task-management-page.tsx
new file mode 100644
index 00000000..3c9d76d1
--- /dev/null
+++ b/components/campaigns/task-management/task-management-page.tsx
@@ -0,0 +1,206 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'sonner';
+import { TaskManagementContainer } from './task-management-container';
+import type {
+ Campaign,
+ CampaignTask,
+ CampaignMember,
+ CreateTaskData,
+ UpdateTaskData,
+} from '@/types/campaign';
+
+interface TaskManagementPageProps {
+ orgId: string;
+ campaignId: string;
+ campaign: Campaign;
+}
+
+export function TaskManagementPage({ orgId, campaignId, campaign }: TaskManagementPageProps) {
+ const router = useRouter();
+ const [tasks, setTasks] = useState([]);
+ const [campaignMembers, setCampaignMembers] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Fetch tasks and members on component mount
+ useEffect(() => {
+ fetchTasks();
+ fetchCampaignMembers();
+ }, [orgId, campaignId]);
+
+ const fetchTasks = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/tasks`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch tasks');
+ }
+ const data = await response.json();
+ setTasks(data.tasks || []);
+ } catch (error) {
+ console.error('Error fetching tasks:', error);
+ toast.error('Failed to fetch tasks');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchCampaignMembers = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/members`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch campaign members');
+ }
+ const data = await response.json();
+ setCampaignMembers(data.members || []);
+ } catch (error) {
+ console.error('Error fetching campaign members:', error);
+ toast.error('Failed to fetch campaign members');
+ }
+ };
+
+ const handleTaskCreate = async (taskData: CreateTaskData) => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/tasks`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(taskData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to create task');
+ }
+
+ const newTask = await response.json();
+ setTasks((prev) => [...prev, newTask.task]);
+ toast.success('Task created successfully');
+ } catch (error) {
+ console.error('Error creating task:', error);
+ toast.error('Failed to create task');
+ throw error;
+ }
+ };
+
+ const handleTaskUpdate = async (taskId: string, data: UpdateTaskData) => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/tasks/${taskId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update task');
+ }
+
+ const updatedTask = await response.json();
+ setTasks((prev) =>
+ prev.map((task) => (task.id === taskId ? { ...task, ...updatedTask.task } : task))
+ );
+ toast.success('Task updated successfully');
+ } catch (error) {
+ console.error('Error updating task:', error);
+ toast.error('Failed to update task');
+ throw error;
+ }
+ };
+
+ const handleTaskDelete = async (taskId: string) => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}/tasks/${taskId}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete task');
+ }
+
+ setTasks((prev) => prev.filter((task) => task.id !== taskId));
+ toast.success('Task deleted successfully');
+ } catch (error) {
+ console.error('Error deleting task:', error);
+ toast.error('Failed to delete task');
+ throw error;
+ }
+ };
+
+ const handleBulkAction = async (action: string, taskIds: string[]) => {
+ try {
+ let successMessage = '';
+
+ switch (action) {
+ case 'status':
+ // Update status for all selected tasks
+ await Promise.all(
+ taskIds.map((taskId) => handleTaskUpdate(taskId, { status: 'IN_PROGRESS' }))
+ );
+ successMessage = 'Tasks status updated successfully';
+ break;
+
+ case 'assign':
+ // In a real app, you'd show a modal to select assignee
+ toast.info('Bulk assign functionality coming soon');
+ return;
+
+ case 'delete':
+ // Delete all selected tasks
+ await Promise.all(taskIds.map((taskId) => handleTaskDelete(taskId)));
+ successMessage = 'Tasks deleted successfully';
+ break;
+
+ default:
+ toast.error('Unknown bulk action');
+ return;
+ }
+
+ toast.success(successMessage);
+ } catch (error) {
+ console.error('Error performing bulk action:', error);
+ toast.error('Failed to perform bulk action');
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading task management...
+
+
+ );
+ }
+
+ return (
+
+ {/* Page Header */}
+
+
+
Task Management
+
+ Manage tasks for campaign: {campaign.title}
+
+
+
+ router.push(`/${orgId}/campaigns/${campaignId}`)}
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+ >
+ Back to Campaign
+
+
+
+
+ {/* Task Management Container */}
+
+
+ );
+}
diff --git a/components/common/campaigns/campaign-details-panel.tsx b/components/common/campaigns/campaign-details-panel.tsx
new file mode 100644
index 00000000..2edf9bf2
--- /dev/null
+++ b/components/common/campaigns/campaign-details-panel.tsx
@@ -0,0 +1,297 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Separator } from '@/components/ui/separator';
+import { X, Layers, FileText, Users, Calendar, Clock, Edit, Trash2 } from 'lucide-react';
+import Link from 'next/link';
+
+interface CampaignDetails {
+ id: string;
+ name: string;
+ description?: string;
+ health: 'ON_TRACK' | 'AT_RISK' | 'OFF_TRACK';
+ totalTasks: number;
+ pic: string[];
+ startDate: string;
+ endDate: string;
+ status: 'PLAN' | 'DO' | 'DONE' | 'CANCELED';
+ createdAt: string;
+ updatedAt: string;
+ contents?: any[];
+ schedules?: any[];
+}
+
+interface CampaignDetailsPanelProps {
+ campaignId: string;
+ orgId: string;
+ onClose: () => void;
+}
+
+export default function CampaignDetailsPanel({
+ campaignId,
+ orgId,
+ onClose,
+}: CampaignDetailsPanelProps) {
+ const [campaign, setCampaign] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchCampaignDetails();
+ }, [campaignId, orgId]);
+
+ const fetchCampaignDetails = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns/${campaignId}`);
+ if (response.ok) {
+ const data = await response.json();
+ setCampaign(data);
+ } else {
+ console.error(
+ 'Failed to fetch campaign details:',
+ response.status,
+ response.statusText
+ );
+ // Set a fallback campaign object to prevent crashes
+ setCampaign({
+ id: campaignId,
+ name: 'Campaign Not Found',
+ description: 'Unable to load campaign details',
+ health: 'OFF_TRACK',
+ totalTasks: 0,
+ pic: [],
+ startDate: '',
+ endDate: '',
+ status: 'PLAN',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ contents: [],
+ schedules: [],
+ });
+ }
+ } catch (error) {
+ console.error('Error fetching campaign details:', error);
+ // Set a fallback campaign object to prevent crashes
+ setCampaign({
+ id: campaignId,
+ name: 'Error Loading Campaign',
+ description: 'Failed to load campaign details due to network error',
+ health: 'OFF_TRACK',
+ totalTasks: 0,
+ pic: [],
+ startDate: '',
+ endDate: '',
+ status: 'PLAN',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ contents: [],
+ schedules: [],
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getHealthColor = (health: string) => {
+ switch (health) {
+ case 'ON_TRACK':
+ return 'default';
+ case 'AT_RISK':
+ return 'destructive';
+ case 'OFF_TRACK':
+ return 'secondary';
+ default:
+ return 'default';
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'PLAN':
+ return 'secondary';
+ case 'DO':
+ return 'default';
+ case 'DONE':
+ return 'default';
+ case 'CANCELED':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!campaign) {
+ return null;
+ }
+
+ return (
+
+ {/* Header */}
+
+
Campaign Details
+
+
+
+
+
+ {/* Content */}
+
+ {/* Basic Info */}
+
+
+
+
+ {campaign.name || 'Unnamed Campaign'}
+
+
+
+ {campaign.description && (
+ {campaign.description}
+ )}
+
+
+
+ {(campaign.health || 'ON_TRACK').replace('_', ' ')}
+
+
+ {campaign.status || 'PLAN'}
+
+
+
+
+
+ {/* Timeline */}
+
+
+
+
+ Timeline
+
+
+
+
+
+ Start:{' '}
+ {campaign.startDate
+ ? new Date(campaign.startDate).toLocaleDateString()
+ : 'Not set'}
+
+
+ End:{' '}
+ {campaign.endDate
+ ? new Date(campaign.endDate).toLocaleDateString()
+ : 'Not set'}
+
+
+
+
+
+ {/* Team */}
+
+
+
+
+ Team ({(campaign.pic || []).length})
+
+
+
+
+ {(campaign.pic || []).map((member, index) => (
+
+ ))}
+
+
+
+
+ {/* Tasks & Content */}
+
+
+
+
+ Tasks & Content ({campaign.totalTasks || 0})
+
+
+
+
+ {(campaign.contents || []).length > 0 ? (
+ (campaign.contents || []).slice(0, 5).map((content: any) => (
+
+
+ {content.title || 'Untitled Content'}
+
+
+ {content.status || 'Unknown'}
+
+
+ ))
+ ) : (
+
No tasks yet
+ )}
+
+
+
+
+ {/* Schedules */}
+
+
+
+
+ Schedules ({campaign.schedules?.length || 0})
+
+
+
+
+ {campaign.schedules && campaign.schedules.length > 0 ? (
+ campaign.schedules.slice(0, 3).map((schedule: any) => (
+
+
+ {schedule.name || 'Unnamed Schedule'}
+
+
+ {schedule.runAt
+ ? new Date(schedule.runAt).toLocaleDateString()
+ : 'No date set'}
+
+
+ ))
+ ) : (
+
No schedules yet
+ )}
+
+
+
+
+ {/* Actions */}
+
+
+
+
+ Edit Campaign
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/common/campaigns/campaign-line.tsx b/components/common/campaigns/campaign-line.tsx
new file mode 100644
index 00000000..8ccef01f
--- /dev/null
+++ b/components/common/campaigns/campaign-line.tsx
@@ -0,0 +1,134 @@
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Layers, FileText, Users, Calendar, ChevronRight } from 'lucide-react';
+
+interface Campaign {
+ id: string;
+ name: string;
+ description?: string;
+ health: 'ON_TRACK' | 'AT_RISK' | 'OFF_TRACK';
+ totalTasks: number;
+ pic: string[];
+ startDate: string;
+ endDate: string;
+ status: 'PLAN' | 'DO' | 'DONE' | 'CANCELED';
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface CampaignLineProps {
+ campaign: Campaign;
+ orgId: string;
+ onOpenDetails?: (campaignId: string) => void;
+}
+
+export default function CampaignLine({ campaign, orgId, onOpenDetails }: CampaignLineProps) {
+ const getHealthColor = (health: string) => {
+ switch (health) {
+ case 'ON_TRACK':
+ return 'default';
+ case 'AT_RISK':
+ return 'destructive';
+ case 'OFF_TRACK':
+ return 'secondary';
+ default:
+ return 'default';
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'PLAN':
+ return 'secondary';
+ case 'DO':
+ return 'default';
+ case 'DONE':
+ return 'default';
+ case 'CANCELED':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+ };
+
+ const formatDate = (date: Date) => {
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+ };
+
+ return (
+
+ {/* Title */}
+
+
+
+
+ {campaign.name || 'Unnamed Campaign'}
+
+ {campaign.description && (
+
+ {campaign.description}
+
+ )}
+
+
+
+ {/* Health */}
+
+
+ {(campaign.health || 'ON_TRACK').replace('_', ' ')}
+
+
+
+ {/* Total Tasks */}
+
+
+ {campaign.totalTasks || 0}
+
+
+ {/* PIC */}
+
+
+
+ {(campaign.pic || []).length > 1
+ ? `${(campaign.pic || [])[0]} +${(campaign.pic || []).length - 1}`
+ : (campaign.pic || [])[0] || 'Unassigned'}
+
+
+
+ {/* Timeline */}
+
+
+
+ {campaign.startDate ? formatDate(new Date(campaign.startDate)) : 'Not set'} →{' '}
+ {campaign.endDate ? formatDate(new Date(campaign.endDate)) : 'Not set'}
+
+
+
+ {/* Status */}
+
+
+ {campaign.status || 'PLAN'}
+
+
+
+ {/* Actions */}
+
+ campaign.id && onOpenDetails?.(campaign.id)}
+ className="hover:bg-accent"
+ >
+
+
+
+
+ );
+}
diff --git a/components/common/campaigns/campaigns.tsx b/components/common/campaigns/campaigns.tsx
new file mode 100644
index 00000000..3cd90815
--- /dev/null
+++ b/components/common/campaigns/campaigns.tsx
@@ -0,0 +1,153 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useParams } from 'next/navigation';
+import CampaignLine from './campaign-line';
+import CampaignDetailsPanel from './campaign-details-panel';
+
+interface Campaign {
+ id: string;
+ name: string;
+ description?: string;
+ health: 'ON_TRACK' | 'AT_RISK' | 'OFF_TRACK';
+ totalTasks: number;
+ pic: string[];
+ startDate: string;
+ endDate: string;
+ status: 'PLAN' | 'DO' | 'DONE' | 'CANCELED';
+ createdAt: string;
+ updatedAt: string;
+}
+
+export default function Campaigns() {
+ const params = useParams();
+ const orgId = params.orgId as string;
+ const [campaigns, setCampaigns] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedCampaignId, setSelectedCampaignId] = useState(null);
+
+ useEffect(() => {
+ fetchCampaigns();
+ }, [orgId]);
+
+ const fetchCampaigns = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns`);
+ if (response.ok) {
+ const data = await response.json();
+ // Transform the data to match our new interface
+ const transformedCampaigns = data.map((campaign: any) => ({
+ id: campaign.id,
+ name: campaign.name || 'Unnamed Campaign',
+ description: campaign.description,
+ health: campaign.health || 'ON_TRACK',
+ totalTasks: campaign.contents?.length || 0,
+ pic: campaign.members?.map(
+ (member: any) => member.user?.name || member.user?.email || 'Unknown User'
+ ) || ['Unassigned'],
+ startDate: campaign.startDate || campaign.createdAt,
+ endDate: campaign.endDate || campaign.updatedAt,
+ status: campaign.status || 'PLAN',
+ createdAt: campaign.createdAt,
+ updatedAt: campaign.updatedAt,
+ }));
+ setCampaigns(transformedCampaigns);
+ }
+ } catch (error) {
+ console.error('Error fetching campaigns:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleOpenDetails = (campaignId: string) => {
+ setSelectedCampaignId(campaignId);
+ };
+
+ const handleCloseDetails = () => {
+ setSelectedCampaignId(null);
+ };
+
+ if (loading) {
+ return (
+
+
+
Title
+
Health
+
Total Tasks
+
PIC
+
Timeline
+
Status
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+
+
Title
+
Health
+
Total Tasks
+
PIC
+
Timeline
+
Status
+
+
+
+ {campaigns.length === 0 ? (
+
+
+
No campaigns yet
+
+ Create your first campaign to get started
+
+
+
+ ) : (
+ campaigns.map((campaign) => (
+
+ ))
+ )}
+
+
+ {/* Right Panel */}
+ {selectedCampaignId && (
+
+ )}
+
+ );
+}
diff --git a/components/common/settings/language-switcher.tsx b/components/common/settings/language-switcher.tsx
new file mode 100644
index 00000000..5df11257
--- /dev/null
+++ b/components/common/settings/language-switcher.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { useLocale, useTranslations } from 'next-intl';
+import { useRouter, usePathname } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { Languages } from 'lucide-react';
+import { locales } from '@/lib/i18n';
+
+export default function LanguageSwitcher() {
+ const t = useTranslations('common');
+ const locale = useLocale();
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const switchLocale = (newLocale: string) => {
+ // Store the locale preference
+ localStorage.setItem('locale', newLocale);
+
+ // For now, reload the page to apply the new locale
+ // In production, you might want to implement proper locale routing
+ window.location.reload();
+ };
+
+ return (
+
+
+
+
+
+
{t('language')}
+
Choose your preferred language
+
+
+
+
+
+ {locale === 'en' ? t('english') : t('vietnamese')}
+
+
+
+ switchLocale('en')}
+ className={locale === 'en' ? 'bg-accent' : ''}
+ >
+ {t('english')}
+
+ switchLocale('vi')}
+ className={locale === 'vi' ? 'bg-accent' : ''}
+ >
+ {t('vietnamese')}
+
+
+
+
+
+ );
+}
diff --git a/components/common/settings/settings.tsx b/components/common/settings/settings.tsx
index 661be11a..309ea11d 100644
--- a/components/common/settings/settings.tsx
+++ b/components/common/settings/settings.tsx
@@ -24,6 +24,7 @@ import {
SiTablecheck,
} from 'react-icons/si';
import { Button } from '@/components/ui/button';
+import LanguageSwitcher from './language-switcher';
interface Feature {
icon: ReactNode;
@@ -243,6 +244,13 @@ export default function Settings() {
+
+
+
Language & Region
+
+
+
+
Explore features
diff --git a/components/content/content-card.tsx b/components/content/content-card.tsx
new file mode 100644
index 00000000..c6ae72b9
--- /dev/null
+++ b/components/content/content-card.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { FileText, Calendar, MoreHorizontal, Eye } from 'lucide-react';
+import Link from 'next/link';
+import { formatDistanceToNow } from 'date-fns';
+
+interface Content {
+ id: string;
+ title: string;
+ body?: string;
+ status: string;
+ createdAt: string;
+ campaign: {
+ id: string;
+ name: string;
+ };
+}
+
+interface ContentCardProps {
+ content: Content;
+ orgId: string;
+}
+
+const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'DRAFT':
+ return 'secondary';
+ case 'SUBMITTED':
+ return 'default';
+ case 'APPROVED':
+ return 'default';
+ case 'PUBLISHED':
+ return 'default';
+ case 'REJECTED':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+};
+
+export function ContentCard({ content, orgId }: ContentCardProps) {
+ const createdDate = new Date(content.createdAt);
+
+ return (
+
+
+
+
+
+ {content.title}
+
+
+ {content.campaign.name}
+
+
+
+
+
+
+
+
+
+ {content.body && (
+
+ {content.body.replace(/<[^>]*>/g, '')} {/* Remove HTML tags */}
+
+ )}
+
+
+
{content.status}
+
+
+ {formatDistanceToNow(createdDate, { addSuffix: true })}
+
+
+
+
+
+
+
+ View Details
+
+
+
+ Edit
+
+
+
+
+ );
+}
diff --git a/components/content/content-detail.tsx b/components/content/content-detail.tsx
new file mode 100644
index 00000000..1d46bdfc
--- /dev/null
+++ b/components/content/content-detail.tsx
@@ -0,0 +1,462 @@
+'use client';
+
+import { useState } from 'react';
+import { useParams } from 'next/navigation';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ ArrowLeft,
+ Edit,
+ Trash2,
+ Eye,
+ Calendar,
+ FileText,
+ BarChart3,
+ TrendingUp,
+ Users,
+ Clock,
+} from 'lucide-react';
+import Link from 'next/link';
+import { formatDistanceToNow } from 'date-fns';
+import { toast } from 'sonner';
+
+interface Content {
+ id: string;
+ title: string;
+ body: string | null;
+ status: string;
+ createdAt: Date | string;
+ updatedAt: Date | string;
+ campaign: {
+ id: string;
+ name: string;
+ };
+ assets: any[];
+}
+
+interface ContentDetailProps {
+ content: Content;
+}
+
+export function ContentDetail({ content }: ContentDetailProps) {
+ const params = useParams();
+ const orgId = params.orgId as string;
+ const [loading, setLoading] = useState(false);
+ const [activeTab, setActiveTab] = useState('overview');
+
+ const handleDelete = async () => {
+ if (!confirm('Are you sure you want to delete this content? This action cannot be undone.')) {
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const response = await fetch(`/api/${orgId}/content/${content.id}`, {
+ method: 'DELETE',
+ });
+
+ if (response.ok) {
+ toast.success('Content deleted successfully');
+ window.location.href = `/${orgId}/content`;
+ } else {
+ const error = await response.json();
+ toast.error(error.error || 'Failed to delete content');
+ }
+ } catch (error) {
+ console.error('Error deleting content:', error);
+ toast.error('An unexpected error occurred');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const createdDate = new Date(content.createdAt);
+ const updatedDate = new Date(content.updatedAt);
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'DRAFT':
+ return 'secondary';
+ case 'SUBMITTED':
+ return 'default';
+ case 'APPROVED':
+ return 'default';
+ case 'PUBLISHED':
+ return 'default';
+ case 'REJECTED':
+ return 'destructive';
+ default:
+ return 'secondary';
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'DRAFT':
+ return '📝';
+ case 'SUBMITTED':
+ return '📤';
+ case 'APPROVED':
+ return '✅';
+ case 'PUBLISHED':
+ return '🚀';
+ case 'REJECTED':
+ return '❌';
+ default:
+ return '📄';
+ }
+ };
+
+ const getContentStats = () => {
+ // Mock stats - in real app, these would come from analytics API
+ return {
+ views: Math.floor(Math.random() * 1000) + 100,
+ engagement: Math.floor(Math.random() * 100) + 10,
+ shares: Math.floor(Math.random() * 50) + 5,
+ comments: Math.floor(Math.random() * 20) + 2,
+ };
+ };
+
+ const contentStats = getContentStats();
+
+ return (
+
+
+
+
+
+
+ Back to Content
+
+
+
+
{content.title}
+
+ {getStatusIcon(content.status)}
+ {content.status}
+ •
+ {content.campaign.name}
+ •
+
+ Created {formatDistanceToNow(createdDate, { addSuffix: true })}
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+
+ {loading ? 'Deleting...' : 'Delete'}
+
+
+
+
+ {/* Overview Cards */}
+
+
+
+
+
+
+
Views
+
{contentStats.views}
+
+
+
+
+
+
+
+
+
+
+
Engagement
+
{contentStats.engagement}%
+
+
+
+
+
+
+
+
+
+
+
Shares
+
{contentStats.shares}
+
+
+
+
+
+
+
+
+
+
+
Last Updated
+
+ {formatDistanceToNow(updatedDate, { addSuffix: true })}
+
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ Overview
+ Content
+ Analytics
+ Assets ({content.assets?.length || 0})
+
+
+
+
+
+
+ 📋 Content Information
+
+
+
+
+ Title:
+ {content.title}
+
+
+ Status:
+
+ {content.status}
+
+
+
+ Campaign:
+
+ {content.campaign.name}
+
+
+
+ Created:
+ {createdDate.toLocaleDateString()}
+
+
+ Last Updated:
+ {updatedDate.toLocaleDateString()}
+
+
+
+
+
+
+
+ 📊 Quick Stats
+
+
+
+
+ Views
+ {contentStats.views}
+
+
+ Engagement Rate
+
+ {contentStats.engagement}%
+
+
+
+ Shares
+ {contentStats.shares}
+
+
+ Comments
+ {contentStats.comments}
+
+
+
+
+
+
+
+
+
+
+ 📄 Content Body
+
+
+ {content.body ? (
+
+ ) : (
+
+
+
No content body available
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Performance Metrics
+
+
+
+
+
+
Views
+
+
+
{contentStats.views}
+
+
+
+
Engagement
+
+
+
+ {contentStats.engagement}%
+
+
+
+
+
Shares
+
+
+
{contentStats.shares}
+
+
+
+
+
+
+
+
+
+
+ Content Timeline
+
+
+
+
+
+ Created
+
+ {createdDate.toLocaleDateString()}
+
+
+
+ Last Updated
+
+ {updatedDate.toLocaleDateString()}
+
+
+
+ Age
+
+ {formatDistanceToNow(createdDate)}
+
+
+
+
+
+
+
+
+
+ 📈 Engagement Trends
+ Content performance over time
+
+
+
+
+
Detailed analytics charts coming soon
+
+ Track views, engagement, and conversion metrics over time
+
+
+
+
+
+
+
+
+
📎 Attached Assets
+
+
+ Add Asset
+
+
+
+ {content.assets && content.assets.length > 0 ? (
+
+ {content.assets.map((asset: any) => (
+
+
+
+
+
+
+ {asset.name || 'Untitled Asset'}
+
+
+ {asset.type || 'Unknown type'}
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
No assets attached
+
+ Attach images, documents, or other files to this content
+
+
+
+ Add First Asset
+
+
+ )}
+
+
+
+ );
+}
diff --git a/components/content/content-editor.tsx b/components/content/content-editor.tsx
new file mode 100644
index 00000000..02bcca13
--- /dev/null
+++ b/components/content/content-editor.tsx
@@ -0,0 +1,319 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useSession } from 'next-auth/react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Save, Image, Plus, Calendar, Clock } from 'lucide-react';
+import { AssetLibrary } from '@/components/assets/asset-library';
+import { ScheduleContentModal } from '@/components/schedules/schedule-content-modal';
+import { RichTextEditor } from './rich-text-editor';
+
+interface Content {
+ id: string;
+ title: string;
+ body: string;
+ campaignId: string;
+}
+
+interface Campaign {
+ id: string;
+ name: string;
+}
+
+interface Asset {
+ id: string;
+ url: string;
+ type: string;
+ createdAt: string;
+ content: {
+ id: string;
+ title: string;
+ };
+}
+
+interface ContentEditorProps {
+ orgId: string;
+ contentId?: string;
+ onSave?: (content: Content) => void;
+}
+
+export function ContentEditor({ orgId, contentId, onSave }: ContentEditorProps) {
+ const { data: session } = useSession();
+ const [title, setTitle] = useState('');
+ const [body, setBody] = useState('');
+ const [campaignId, setCampaignId] = useState('');
+ const [campaigns, setCampaigns] = useState
([]);
+ const [assets, setAssets] = useState([]);
+ const [selectedAssets, setSelectedAssets] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [assetDialogOpen, setAssetDialogOpen] = useState(false);
+
+ // Fetch campaigns
+ useEffect(() => {
+ const fetchCampaigns = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns`);
+ if (response.ok) {
+ const data = await response.json();
+ setCampaigns(data);
+ }
+ } catch (error) {
+ console.error('Error fetching campaigns:', error);
+ }
+ };
+
+ if (session?.user) {
+ fetchCampaigns();
+ }
+ }, [session, orgId]);
+
+ // Fetch content if editing
+ useEffect(() => {
+ if (contentId) {
+ const fetchContent = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/content/${contentId}`);
+ if (response.ok) {
+ const content = await response.json();
+ setTitle(content.title);
+ setBody(content.body || '');
+ setCampaignId(content.campaignId);
+ setSelectedAssets(content.assets || []);
+ }
+ } catch (error) {
+ console.error('Error fetching content:', error);
+ }
+ };
+
+ fetchContent();
+ }
+ }, [contentId, orgId]);
+
+ // Fetch assets for the selected campaign
+ useEffect(() => {
+ if (campaignId) {
+ const fetchAssets = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/assets?contentId=${campaignId}`);
+ if (response.ok) {
+ const data = await response.json();
+ setAssets(data);
+ }
+ } catch (error) {
+ console.error('Error fetching assets:', error);
+ }
+ };
+
+ fetchAssets();
+ }
+ }, [campaignId, orgId]);
+
+ const handleSave = async () => {
+ if (!title.trim() || !campaignId) return;
+
+ setLoading(true);
+ try {
+ const contentData = {
+ title: title.trim(),
+ body: body.trim(),
+ campaignId,
+ };
+
+ const url = contentId ? `/api/${orgId}/content/${contentId}` : `/api/${orgId}/content`;
+
+ const response = await fetch(url, {
+ method: contentId ? 'PUT' : 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(contentData),
+ });
+
+ if (response.ok) {
+ const savedContent = await response.json();
+ onSave?.(savedContent);
+ } else {
+ const error = await response.json();
+ alert(`Failed to save content: ${error.error}`);
+ }
+ } catch (error) {
+ console.error('Error saving content:', error);
+ alert('Failed to save content. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAssetSelect = (asset: Asset) => {
+ if (!selectedAssets.find((a) => a.id === asset.id)) {
+ setSelectedAssets([...selectedAssets, asset]);
+ }
+ setAssetDialogOpen(false);
+ };
+
+ const removeAsset = (assetId: string) => {
+ setSelectedAssets(selectedAssets.filter((asset) => asset.id !== assetId));
+ };
+
+ return (
+
+
+
+ {contentId ? 'Edit Content' : 'Create New Content'}
+
+
+
+ {loading ? 'Saving...' : 'Save Content'}
+
+
+
+
+
+
+ Title
+ setTitle(e.target.value)}
+ placeholder="Enter content title"
+ className="mt-1"
+ />
+
+
+
+ Campaign
+
+
+
+
+
+ {campaigns.map((campaign) => (
+
+ {campaign.name}
+
+ ))}
+
+
+
+
+
+ Content Body
+
+
+
+
+
+
+
+
+ Assets
+
+
+
+
+ Add Asset
+
+
+
+
+ Select Assets
+
+
+
+
+
+
+
+ {selectedAssets.length === 0 ? (
+
+ No assets selected. Click "Add Asset" to choose from your library.
+
+ ) : (
+
+ {selectedAssets.map((asset) => (
+
+
+
+ {asset.content.title}
+
+ {asset.type.split('/')[0]}
+
+
+
removeAsset(asset.id)}
+ >
+ ×
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ Schedule Publication
+
+
+
+
+ Schedule this content for automatic publication at a specific date and time.
+
+
+
+ Schedule Content
+
+ }
+ onScheduleCreated={() => {
+ // Optional: Show success message or refresh data
+ console.log('Content scheduled successfully');
+ }}
+ />
+
+
+
+
+
+ );
+}
diff --git a/components/content/content-list.tsx b/components/content/content-list.tsx
new file mode 100644
index 00000000..21bb0095
--- /dev/null
+++ b/components/content/content-list.tsx
@@ -0,0 +1,180 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useParams } from 'next/navigation';
+import { ContentCard } from './content-card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Search, Plus, Filter } from 'lucide-react';
+import Link from 'next/link';
+
+interface Content {
+ id: string;
+ title: string;
+ body?: string;
+ status: string;
+ createdAt: string;
+ campaign: {
+ id: string;
+ name: string;
+ };
+}
+
+interface ContentListProps {
+ orgId: string;
+}
+
+export function ContentList({ orgId }: ContentListProps) {
+ const [contents, setContents] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [campaignFilter, setCampaignFilter] = useState('all');
+ const [campaigns, setCampaigns] = useState([]);
+
+ useEffect(() => {
+ fetchContents();
+ fetchCampaigns();
+ }, [orgId]);
+
+ const fetchContents = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/content`);
+ if (response.ok) {
+ const data = await response.json();
+ setContents(data.data || []);
+ }
+ } catch (error) {
+ console.error('Error fetching contents:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchCampaigns = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns`);
+ if (response.ok) {
+ const data = await response.json();
+ setCampaigns(data);
+ }
+ } catch (error) {
+ console.error('Error fetching campaigns:', error);
+ }
+ };
+
+ const filteredContents = contents.filter((content) => {
+ const matchesSearch =
+ content.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ content.body?.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesStatus = statusFilter === 'all' || content.status === statusFilter;
+ const matchesCampaign = campaignFilter === 'all' || content.campaign.id === campaignFilter;
+
+ return matchesSearch && matchesStatus && matchesCampaign;
+ });
+
+ if (loading) {
+ return (
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+ New Content
+
+
+
+
+
+
+
+ Filters:
+
+
+
+
+
+
+
+ All Status
+ Draft
+ Submitted
+ Approved
+ Published
+ Rejected
+
+
+
+
+
+
+
+
+ All Campaigns
+ {campaigns.map((campaign) => (
+
+ {campaign.name}
+
+ ))}
+
+
+
+
+ {filteredContents.length === 0 ? (
+
+
+ {searchTerm || statusFilter !== 'all' || campaignFilter !== 'all'
+ ? 'No content found'
+ : 'No content yet'}
+
+
+ {searchTerm || statusFilter !== 'all' || campaignFilter !== 'all'
+ ? 'Try adjusting your filters or search terms'
+ : 'Create your first content piece to get started'}
+
+ {!searchTerm && statusFilter === 'all' && campaignFilter === 'all' && (
+
+
+
+ Create Content
+
+
+ )}
+
+ ) : (
+
+ {filteredContents.map((content) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/content/create-content-button.tsx b/components/content/create-content-button.tsx
new file mode 100644
index 00000000..f914241f
--- /dev/null
+++ b/components/content/create-content-button.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Plus } from 'lucide-react';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+
+export function CreateContentButton({ orgId }: { orgId: string }) {
+ return (
+
+
+
+ New Content
+
+
+ );
+}
diff --git a/components/content/rich-text-editor.tsx b/components/content/rich-text-editor.tsx
new file mode 100644
index 00000000..58ab96cd
--- /dev/null
+++ b/components/content/rich-text-editor.tsx
@@ -0,0 +1,168 @@
+'use client';
+
+import { useEditor, EditorContent } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+import { Button } from '@/components/ui/button';
+import {
+ Bold,
+ Italic,
+ List,
+ ListOrdered,
+ Quote,
+ Heading1,
+ Heading2,
+ Undo,
+ Redo,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface RichTextEditorProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+const MenuBar = ({ editor }: { editor: any }) => {
+ if (!editor) {
+ return null;
+ }
+
+ return (
+
+
editor.chain().focus().toggleBold().run()}
+ disabled={!editor.can().chain().focus().toggleBold().run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
editor.chain().focus().toggleItalic().run()}
+ disabled={!editor.can().chain().focus().toggleItalic().run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
editor.chain().focus().toggleHeading({ level: 1 }).run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
editor.chain().focus().toggleHeading({ level: 2 }).run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
editor.chain().focus().toggleBulletList().run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
editor.chain().focus().toggleOrderedList().run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
editor.chain().focus().toggleBlockquote().run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+
+
editor.chain().focus().undo().run()}
+ disabled={!editor.can().chain().focus().undo().run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
editor.chain().focus().redo().run()}
+ disabled={!editor.can().chain().focus().redo().run()}
+ className="h-8 w-8 p-0"
+ >
+
+
+
+ );
+};
+
+export function RichTextEditor({ value, onChange, placeholder }: RichTextEditorProps) {
+ const editor = useEditor({
+ extensions: [StarterKit],
+ content: value,
+ immediatelyRender: false, // Fix for SSR hydration issues
+ editorProps: {
+ attributes: {
+ class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none',
+ },
+ },
+ onUpdate: ({ editor }) => {
+ onChange(editor.getHTML());
+ },
+ });
+
+ // Update content when value prop changes
+ if (editor && editor.getHTML() !== value) {
+ editor.commands.setContent(value);
+ }
+
+ return (
+
+
+
+
+ {!editor?.getText() && (
+
+ {placeholder || 'Start writing your content...'}
+
+ )}
+
+
+ );
+}
diff --git a/components/dashboards/admin-dashboard.tsx b/components/dashboards/admin-dashboard.tsx
new file mode 100644
index 00000000..e34bad6c
--- /dev/null
+++ b/components/dashboards/admin-dashboard.tsx
@@ -0,0 +1,417 @@
+'use client';
+
+import React from 'react';
+import { useTranslations } from 'next-intl';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ Users,
+ Building,
+ Settings,
+ Shield,
+ Activity,
+ UserPlus,
+ Crown,
+ AlertTriangle,
+ CheckCircle,
+ Clock,
+} from 'lucide-react';
+
+interface AdminDashboardProps {
+ orgId: string;
+}
+
+export function AdminDashboard({ orgId }: AdminDashboardProps) {
+ const t = useTranslations('dashboard');
+
+ // Mock data - in real app, this would come from API
+ const organizationStats = {
+ totalUsers: 45,
+ activeUsers: 38,
+ totalCampaigns: 12,
+ activeCampaigns: 8,
+ totalContent: 156,
+ pendingApprovals: 23,
+ };
+
+ const users = [
+ {
+ id: '1',
+ name: 'Sarah Johnson',
+ email: 'sarah@company.com',
+ role: 'CREATOR',
+ status: 'active',
+ lastActive: '2024-07-10',
+ campaigns: 3,
+ },
+ {
+ id: '2',
+ name: 'Mike Chen',
+ email: 'mike@company.com',
+ role: 'BRAND_OWNER',
+ status: 'active',
+ lastActive: '2024-07-09',
+ campaigns: 5,
+ },
+ {
+ id: '3',
+ name: 'Emma Davis',
+ email: 'emma@company.com',
+ role: 'CREATOR',
+ status: 'inactive',
+ lastActive: '2024-06-15',
+ campaigns: 2,
+ },
+ ];
+
+ const pendingInvites = [
+ {
+ id: '1',
+ email: 'john.doe@company.com',
+ role: 'CREATOR',
+ invitedBy: 'Sarah Johnson',
+ invitedAt: '2024-07-08',
+ status: 'pending',
+ },
+ {
+ id: '2',
+ email: 'jane.smith@agency.com',
+ role: 'BRAND_OWNER',
+ invitedBy: 'Mike Chen',
+ invitedAt: '2024-07-07',
+ status: 'pending',
+ },
+ ];
+
+ const systemAlerts = [
+ {
+ id: '1',
+ type: 'warning',
+ message: '5 users have not logged in for 30+ days',
+ timestamp: '2024-07-10T10:30:00Z',
+ },
+ {
+ id: '2',
+ type: 'info',
+ message: 'Monthly analytics report is ready',
+ timestamp: '2024-07-10T09:00:00Z',
+ },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
{t('admin_title')}
+
{t('admin_description')}
+
+
+
+ {t('system_settings')}
+
+
+
+ {/* Organization Overview */}
+
+
+
+ {t('total_users')}
+
+
+
+ {organizationStats.totalUsers}
+
+ {organizationStats.activeUsers} {t('active_users')}
+
+
+
+
+
+ {t('active_campaigns')}
+
+
+
+ {organizationStats.activeCampaigns}
+
+ of {organizationStats.totalCampaigns} total
+
+
+
+
+
+ {t('content_pieces')}
+
+
+
+ {organizationStats.totalContent}
+ +12% this month
+
+
+
+
+ {t('pending_approvals')}
+
+
+
+ {organizationStats.pendingApprovals}
+ {t('requires_attention')}
+
+
+
+
+ {/* Main Content */}
+
+
+ {t('user_management')}
+ {t('organization')}
+ {t('system')}
+
+
+
+
+
+
+ {t('team_members')}
+ {t('manage_user_roles')}
+
+
+
+
+
+ {t('user')}
+ {t('role')}
+ {t('status')}
+ {t('actions')}
+
+
+
+ {users.map((user) => (
+
+
+
+
+
+ {user.name
+ .split(' ')
+ .map((n) => n[0])
+ .join('')}
+
+
+
+
{user.name}
+
+ {user.email}
+
+
+
+
+
+
+ {user.role === 'BRAND_OWNER'
+ ? t('brand_owner')
+ : user.role === 'CREATOR'
+ ? t('creator')
+ : t('admin')}
+
+
+
+
+ {user.status}
+
+
+
+
+ {t('edit')}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ {t('pending_invites')}
+ {t('users_waiting_join')}
+
+
+ {pendingInvites.map((invite) => (
+
+
+
{invite.email}
+
+ {t('invited_by')} {invite.invitedBy} • {invite.invitedAt}
+
+
+
+ {invite.role}
+
+ {t('resend')}
+
+
+
+ ))}
+
+
+ {t('invite_new_user')}
+
+
+
+
+
+
+
+
+
+
+ Organization Settings
+ Configure organization-wide settings
+
+
+
+
Organization Name
+
Acme Marketing Inc.
+
+
+
Default User Role
+
Creator
+
+
+
Content Approval Required
+
Yes
+
+
+
+ Edit Settings
+
+
+
+
+
+
+ Role Permissions
+ Manage what each role can access
+
+
+
+
+
+
+ Admin
+
+
Full Access
+
+
+
+
+ Brand Owner
+
+
Limited Access
+
+
+
+
+ Creator
+
+
Content Access
+
+
+
+
+ Manage Permissions
+
+
+
+
+
+
+
+
+
+ System Alerts
+ Important notifications and system status
+
+
+ {systemAlerts.map((alert) => (
+
+ {alert.type === 'warning' ? (
+
+ ) : (
+
+ )}
+
+
{alert.message}
+
+ {new Date(alert.timestamp).toLocaleString()}
+
+
+
+ View Details
+
+
+ ))}
+
+
+
+
+
+
+ System Health
+
+
+
+
+ All systems operational
+
+
+
+
+
+ API Usage
+
+
+ 94%
+ of monthly limit
+
+
+
+
+ Storage
+
+
+ 67%
+ of allocated space
+
+
+
+
+
+
+ );
+}
diff --git a/components/dashboards/brand-dashboard.tsx b/components/dashboards/brand-dashboard.tsx
new file mode 100644
index 00000000..8967c0a8
--- /dev/null
+++ b/components/dashboards/brand-dashboard.tsx
@@ -0,0 +1,303 @@
+'use client';
+
+import React from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Skeleton } from '@/components/ui/skeleton';
+import {
+ BarChart3,
+ TrendingUp,
+ Users,
+ Calendar,
+ CheckCircle,
+ Clock,
+ AlertTriangle,
+ Eye,
+ ThumbsUp,
+ MessageSquare,
+ Target,
+ MousePointer,
+ Activity,
+} from 'lucide-react';
+import { useAnalytics } from '@/hooks/use-analytics';
+import { AnalyticsCharts } from '@/components/analytics/analytics-charts';
+
+interface BrandDashboardProps {
+ orgId: string;
+}
+
+export function BrandDashboard({ orgId }: BrandDashboardProps) {
+ const { metrics, loading, error, trackEvent } = useAnalytics(orgId);
+
+ // Mock data for campaigns and approvals (would come from separate APIs)
+ const campaigns = [
+ {
+ id: '1',
+ name: 'Summer Campaign 2024',
+ status: 'active',
+ progress: 75,
+ creators: 5,
+ contentCount: 24,
+ budget: 15000,
+ spent: 11250,
+ },
+ {
+ id: '2',
+ name: 'Product Launch',
+ status: 'planning',
+ progress: 30,
+ creators: 3,
+ contentCount: 8,
+ budget: 8000,
+ spent: 2400,
+ },
+ ];
+
+ const pendingApprovals = [
+ {
+ id: '1',
+ title: 'Social Media Post - Summer Vibes',
+ creator: 'Sarah Johnson',
+ creatorAvatar: '/avatars/sarah.jpg',
+ campaign: 'Summer Campaign 2024',
+ submittedAt: '2024-07-10',
+ type: 'social',
+ },
+ {
+ id: '2',
+ title: 'Blog Post - Product Features',
+ creator: 'Mike Chen',
+ creatorAvatar: '/avatars/mike.jpg',
+ campaign: 'Product Launch',
+ submittedAt: '2024-07-08',
+ type: 'blog',
+ },
+ ];
+
+ if (loading) {
+ return (
+
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
Error loading analytics: {error}
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Brand Dashboard
+
+ Monitor campaigns, approve content, and track performance
+
+
+
+
+ View Analytics
+
+
+
+ {/* Key Metrics */}
+
+
+
+ Impressions
+
+
+
+
+ {metrics?.impressions.toLocaleString() || 0}
+
+ Total content impressions
+
+
+
+
+ Clicks
+
+
+
+ {metrics?.clicks.toLocaleString() || 0}
+ Total user interactions
+
+
+
+
+ CTR
+
+
+
+ {metrics?.ctr.toFixed(2) || 0}%
+ Click-through rate
+
+
+
+
+ ROI
+
+
+
+ {metrics?.roi.toFixed(1) || 0}%
+ Return on investment
+
+
+
+
+ {/* Main Content */}
+
+
+ Campaign Summaries
+ Approval Queue
+ Analytics
+
+
+
+
+
+ Campaign Performance
+
+ Overview of all active campaigns and their progress
+
+
+
+ {campaigns.map((campaign) => (
+
+
+
+
{campaign.name}
+
+ {campaign.creators} creators
+ {campaign.contentCount} pieces
+
+ {campaign.status}
+
+
+
+
+ View Details
+
+
+
+
+ Progress
+ {campaign.progress}%
+
+
+
+
+
+ Budget: ${campaign.spent.toLocaleString()} / $
+ {campaign.budget.toLocaleString()}
+
+
+ {((campaign.spent / campaign.budget) * 100).toFixed(1)}% spent
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Content Approval Queue
+
+ Review and approve content submissions from creators
+
+
+
+ {pendingApprovals.map((item) => (
+
+
+
+
+ {item.creator
+ .split(' ')
+ .map((n) => n[0])
+ .join('')}
+
+
+
+
{item.title}
+
+ {item.creator} • {item.campaign} • Submitted {item.submittedAt}
+
+
+
{item.type}
+
+
+
+ Preview
+
+
+
+ Approve
+
+
+
+ ))}
+ {pendingApprovals.length === 0 && (
+
+
+
No pending approvals
+
+ )}
+
+
+
+
+
+ {metrics && }
+
+
+
+ );
+}
diff --git a/components/dashboards/creator-dashboard.tsx b/components/dashboards/creator-dashboard.tsx
new file mode 100644
index 00000000..5617580b
--- /dev/null
+++ b/components/dashboards/creator-dashboard.tsx
@@ -0,0 +1,706 @@
+'use client';
+
+import React, { useState } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import {
+ Calendar,
+ FileText,
+ Sparkles,
+ TrendingUp,
+ Clock,
+ CheckCircle,
+ AlertCircle,
+ Loader2,
+ BarChart3,
+ Eye,
+ MousePointer,
+ Target,
+} from 'lucide-react';
+import { useAnalytics } from '@/hooks/use-analytics';
+
+interface CreatorDashboardProps {
+ orgId: string;
+}
+
+export function CreatorDashboard({ orgId }: CreatorDashboardProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [ideas, setIdeas] = useState([]);
+ const [summary, setSummary] = useState('');
+ const [translatedContent, setTranslatedContent] = useState('');
+ const [ideaTopic, setIdeaTopic] = useState('');
+ const [contentToSummarize, setContentToSummarize] = useState('');
+ const [contentToTranslate, setContentToTranslate] = useState('');
+ const [targetLanguage, setTargetLanguage] = useState('Spanish');
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+ const [generationPrompt, setGenerationPrompt] = useState('');
+ const [selectedCampaign, setSelectedCampaign] = useState('');
+ const [generatedContent, setGeneratedContent] = useState('');
+
+ const { metrics, loading: analyticsLoading, trackEvent } = useAnalytics(orgId);
+
+ // Mock data - in real app, this would come from API
+ const campaigns = [
+ {
+ id: '1',
+ name: 'Summer Campaign 2024',
+ status: 'active',
+ progress: 75,
+ dueDate: '2024-08-15',
+ contentCount: 12,
+ },
+ {
+ id: '2',
+ name: 'Product Launch',
+ status: 'draft',
+ progress: 30,
+ dueDate: '2024-09-01',
+ contentCount: 5,
+ },
+ ];
+
+ const recentContent = [
+ {
+ id: '1',
+ title: 'Social Media Post - Summer Vibes',
+ status: 'approved',
+ campaign: 'Summer Campaign 2024',
+ createdAt: '2024-07-10',
+ },
+ {
+ id: '2',
+ title: 'Blog Post - Product Features',
+ status: 'pending',
+ campaign: 'Product Launch',
+ createdAt: '2024-07-08',
+ },
+ ];
+
+ const generateIdeas = async () => {
+ if (!ideaTopic.trim()) return;
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/${orgId}/content/ideas`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ topic: ideaTopic, count: 5, type: 'general' }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setIdeas(data.ideas);
+ }
+ } catch (error) {
+ console.error('Error generating ideas:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const summarizeContent = async () => {
+ if (!contentToSummarize.trim()) return;
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/${orgId}/content/summarize`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content: contentToSummarize, length: 'brief' }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setSummary(data.summary);
+ }
+ } catch (error) {
+ console.error('Error summarizing content:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const translateContent = async () => {
+ if (!contentToTranslate.trim()) return;
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/${orgId}/content/translate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ content: contentToTranslate,
+ targetLanguage,
+ }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setTranslatedContent(data.translatedContent);
+ }
+ } catch (error) {
+ console.error('Error translating content:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const generateNewContent = async () => {
+ if (!generationPrompt.trim() || !selectedCampaign) return;
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/${orgId}/content/generate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ prompt: generationPrompt,
+ campaignId: selectedCampaign,
+ }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setGeneratedContent(data.body);
+ setIsCreateDialogOpen(false);
+ // Reset form
+ setGenerationPrompt('');
+ setSelectedCampaign('');
+ // Refresh the page or update the content list
+ window.location.reload();
+ }
+ } catch (error) {
+ console.error('Error generating content:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Creator Dashboard
+
+ Manage your campaigns and create amazing content
+
+
+
+
+ AI Assistant
+
+
+
+ {/* Quick Stats */}
+
+
+
+ Active Campaigns
+
+
+
+ 3
+ 2 due this month
+
+
+
+
+ Content Created
+
+
+
+ 24
+ +12% from last month
+
+
+
+
+ Approval Rate
+
+
+
+ 92%
+ +5% from last month
+
+
+
+
+ Pending Reviews
+
+
+
+ 5
+ 3 urgent
+
+
+
+
+ {/* Main Content */}
+
+
+ Campaign Overview
+ Content Studio
+ Analytics
+ AI Assistant
+
+
+
+
+
+ Active Campaigns
+
+ Track progress and deadlines for your campaigns
+
+
+
+ {campaigns.map((campaign) => (
+
+
+
+
{campaign.name}
+
+ {campaign.status}
+
+
+
+ {campaign.contentCount} pieces of content
+ Due: {campaign.dueDate}
+
+
+
+
+ View Details
+
+
+ ))}
+
+
+
+
+
+
+
+ Content Studio
+ Create and manage your content pieces
+
+
+
+
Recent Content
+
+
+
+
+ Create AI Content
+
+
+
+
+ Create AI-Powered Content
+
+ Generate new content using AI based on your prompt and selected
+ campaign.
+
+
+
+
+ Select Campaign
+
+
+
+
+
+ {campaigns.map((campaign) => (
+
+ {campaign.name}
+
+ ))}
+
+
+
+
+ Content Prompt
+ setGenerationPrompt(e.target.value)}
+ rows={4}
+ />
+
+
+ setIsCreateDialogOpen(false)}
+ >
+ Cancel
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ Generate Content
+
+
+
+
+
+
+ {recentContent.map((content) => (
+
+
+
{content.title}
+
{content.campaign}
+
+
+ {content.status === 'approved' ? (
+
+ ) : (
+
+ )}
+
+ {content.status}
+
+
+
+ Edit
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Content Views
+
+
+
+
+ {metrics?.views.toLocaleString() || 0}
+
+ Total content views
+
+
+
+
+ Clicks
+
+
+
+
+ {metrics?.clicks.toLocaleString() || 0}
+
+ User interactions
+
+
+
+
+ CTR
+
+
+
+ {metrics?.ctr.toFixed(2) || 0}%
+ Click-through rate
+
+
+
+
+ Total Events
+
+
+
+
+ {metrics?.totalEvents.toLocaleString() || 0}
+
+ All tracked events
+
+
+
+
+
+
+ Content Performance
+ Analytics for your created content
+
+
+ {analyticsLoading ? (
+
+
+
Loading analytics...
+
+ ) : metrics?.campaignMetrics && metrics.campaignMetrics.length > 0 ? (
+
+ {metrics.campaignMetrics.map((campaign) => (
+
+
{campaign.name}
+
+
+
+ Campaign Events
+
+
+ {campaign.totalEvents}
+
+
+
+
+ Content Pieces
+
+
+ {campaign.contentCount}
+
+
+
+ {campaign.contentMetrics.length > 0 && (
+
+
+ Content Breakdown:
+
+
+ {campaign.contentMetrics.slice(0, 3).map((content) => (
+
+
+ {content.title}
+
+ {content.events} events
+
+ ))}
+
+
+ )}
+
+ ))}
+
+ ) : (
+
+
+
No analytics data available yet
+
+ Start creating content to see performance metrics
+
+
+ )}
+
+
+
+
+
+
+ {/* Content Ideas Generator */}
+
+
+ Content Ideas Generator
+
+ Generate creative content ideas for your campaigns
+
+
+
+
+ Topic
+ setIdeaTopic(e.target.value)}
+ />
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ Generate Ideas
+
+ {ideas.length > 0 && (
+
+
Generated Ideas:
+
+ {ideas.map((idea, index) => (
+
+ {idea}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Content Summarizer */}
+
+
+ Content Summarizer
+
+ Summarize long content into concise versions
+
+
+
+
+ Content to Summarize
+ setContentToSummarize(e.target.value)}
+ rows={4}
+ />
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ Summarize Content
+
+ {summary && (
+
+ )}
+
+
+
+ {/* Content Translator */}
+
+
+ Content Translator
+ Translate content to different languages
+
+
+
+ Content to Translate
+ setContentToTranslate(e.target.value)}
+ rows={4}
+ />
+
+
+ Target Language
+
+
+
+
+
+ Spanish
+ French
+ German
+ Italian
+ Portuguese
+ Chinese
+ Japanese
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ Translate Content
+
+ {translatedContent && (
+
+
Translated Content:
+
+ {translatedContent}
+
+
+ )}
+
+
+
+ {/* Content Generation */}
+
+
+ AI Content Generation
+ Generate new content based on prompts
+
+
+
+
Content Generation
+
+ Use the "Create New Content" button in the Content Studio tab to
+ generate AI-powered content for your campaigns.
+
+
+
+
+
+ Go to Content Studio
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/layout/headers/campaigns/header-nav.tsx b/components/layout/headers/campaigns/header-nav.tsx
new file mode 100644
index 00000000..f97896fb
--- /dev/null
+++ b/components/layout/headers/campaigns/header-nav.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { SidebarTrigger } from '@/components/ui/sidebar';
+import { Plus } from 'lucide-react';
+import { useParams } from 'next/navigation';
+import Link from 'next/link';
+import { useState, useEffect } from 'react';
+
+export default function HeaderNav() {
+ const params = useParams();
+ const orgId = params.orgId as string;
+ const [campaignCount, setCampaignCount] = useState(0);
+
+ useEffect(() => {
+ fetchCampaignCount();
+ }, [orgId]);
+
+ const fetchCampaignCount = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns`);
+ if (response.ok) {
+ const data = await response.json();
+ setCampaignCount(data.length);
+ }
+ } catch (error) {
+ console.error('Error fetching campaign count:', error);
+ }
+ };
+
+ return (
+
+
+
+
+ Campaigns
+ {campaignCount}
+
+
+
+
+
+
+ Create campaign
+
+
+
+
+ );
+}
diff --git a/components/layout/headers/campaigns/header-options.tsx b/components/layout/headers/campaigns/header-options.tsx
new file mode 100644
index 00000000..dafd8f84
--- /dev/null
+++ b/components/layout/headers/campaigns/header-options.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Filter, Layout } from 'lucide-react';
+
+export default function HeaderOptions() {
+ return (
+
+
+
+
+ Filter
+
+
+
+
+
+ Display
+
+
+
+ );
+}
diff --git a/components/layout/headers/campaigns/header.tsx b/components/layout/headers/campaigns/header.tsx
new file mode 100644
index 00000000..bc5a1244
--- /dev/null
+++ b/components/layout/headers/campaigns/header.tsx
@@ -0,0 +1,11 @@
+import HeaderNav from './header-nav';
+import HeaderOptions from './header-options';
+
+export default function Header() {
+ return (
+
+
+
+
+ );
+}
diff --git a/components/layout/sidebar/app-sidebar.tsx b/components/layout/sidebar/app-sidebar.tsx
index f035e6d3..6515f8b6 100644
--- a/components/layout/sidebar/app-sidebar.tsx
+++ b/components/layout/sidebar/app-sidebar.tsx
@@ -2,6 +2,7 @@
import { RiGithubLine } from '@remixicon/react';
import * as React from 'react';
+import { useTranslations } from 'next-intl';
import { HelpButton } from '@/components/layout/sidebar/help-button';
import { NavInbox } from '@/components/layout/sidebar/nav-inbox';
@@ -22,6 +23,9 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
const [open, setOpen] = React.useState(true);
const pathname = usePathname();
const isSettings = pathname.includes('/settings');
+ const isOrgPage = pathname.match(/^\/[^\/]+$/); // Matches /orgId pattern
+ const t = useTranslations('sidebar');
+
return (
{isSettings ? : }
@@ -32,6 +36,10 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
>
+ ) : isOrgPage ? (
+ <>
+
+ >
) : (
<>
@@ -52,12 +60,9 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
- Open-source layouts by lndev-ui
-
-
- Collection of beautifully crafted open-source layouts UI built with
- shadcn/ui.
+ {t('open_source_layouts')}
+
{t('layouts_description')}
) {
target="_blank"
rel="noopener noreferrer"
>
- square.lndev.me
+ {t('visit_square')}
diff --git a/components/layout/sidebar/nav-workspace.tsx b/components/layout/sidebar/nav-workspace.tsx
index 7f1396ef..4c6ab030 100644
--- a/components/layout/sidebar/nav-workspace.tsx
+++ b/components/layout/sidebar/nav-workspace.tsx
@@ -1,6 +1,20 @@
'use client';
-import { Layers, LayoutList, MoreHorizontal } from 'lucide-react';
+import {
+ Layers,
+ LayoutList,
+ MoreHorizontal,
+ Home,
+ FileText,
+ BarChart3,
+ Users,
+ Settings,
+ Sparkles,
+ Image,
+ Calendar,
+} from 'lucide-react';
+import { useParams } from 'next/navigation';
+import { useTranslations } from 'next-intl';
import {
DropdownMenu,
@@ -17,18 +31,104 @@ import {
SidebarMenuItem,
} from '@/components/ui/sidebar';
import Link from 'next/link';
-import { workspaceItems } from '@/mock-data/side-bar-nav';
-import { RiPresentationLine } from '@remixicon/react';
+import { OrgRole } from '@prisma/client';
+import { useEffect, useState } from 'react';
+
+interface NavItem {
+ name: string;
+ url: string;
+ icon: React.ComponentType;
+ roles: OrgRole[];
+}
export function NavWorkspace() {
+ const params = useParams();
+ const orgId = params.orgId as string;
+ const [userRole, setUserRole] = useState(null);
+ const t = useTranslations('navigation');
+
+ useEffect(() => {
+ // Fetch user role - in a real app, this would be done server-side or through a context
+ const fetchUserRole = async () => {
+ try {
+ const response = await fetch(`/api/me/role?orgId=${orgId}`);
+ if (response.ok) {
+ const data = await response.json();
+ setUserRole(data.role);
+ }
+ } catch (error) {
+ console.error('Failed to fetch user role:', error);
+ }
+ };
+
+ if (orgId) {
+ fetchUserRole();
+ }
+ }, [orgId]);
+
+ const navigationItems: NavItem[] = [
+ {
+ name: t('dashboard'),
+ url: '',
+ icon: Home,
+ roles: [OrgRole.ADMIN, OrgRole.BRAND_OWNER, OrgRole.CREATOR],
+ },
+ {
+ name: t('campaigns'),
+ url: '/campaigns',
+ icon: FileText,
+ roles: [OrgRole.ADMIN, OrgRole.BRAND_OWNER, OrgRole.CREATOR],
+ },
+ {
+ name: t('content_studio'),
+ url: '/content',
+ icon: Sparkles,
+ roles: [OrgRole.ADMIN, OrgRole.BRAND_OWNER, OrgRole.CREATOR],
+ },
+ {
+ name: t('schedules'),
+ url: '/schedules',
+ icon: Calendar,
+ roles: [OrgRole.ADMIN, OrgRole.BRAND_OWNER, OrgRole.CREATOR],
+ },
+ {
+ name: t('assets'),
+ url: '/assets',
+ icon: Image,
+ roles: [OrgRole.ADMIN, OrgRole.BRAND_OWNER, OrgRole.CREATOR],
+ },
+ {
+ name: t('analytics'),
+ url: '/analytics',
+ icon: BarChart3,
+ roles: [OrgRole.ADMIN, OrgRole.BRAND_OWNER],
+ },
+ {
+ name: t('members'),
+ url: '/members',
+ icon: Users,
+ roles: [OrgRole.ADMIN, OrgRole.BRAND_OWNER],
+ },
+ {
+ name: t('settings'),
+ url: '/settings',
+ icon: Settings,
+ roles: [OrgRole.ADMIN],
+ },
+ ];
+
+ const filteredItems = navigationItems.filter(
+ (item) => userRole && item.roles.includes(userRole)
+ );
+
return (
- Workspace
+ {t('workspace')}
- {workspaceItems.map((item) => (
+ {filteredItems.map((item) => (
-
+
{item.name}
@@ -41,23 +141,19 @@ export function NavWorkspace() {
- More
+ {t('more')}
-
-
- Initiatives
-
- Views
+ {t('views')}
- Customize sidebar
+ {t('customize_sidebar')}
diff --git a/components/layout/sidebar/org-switcher.tsx b/components/layout/sidebar/org-switcher.tsx
index 873904af..3d6e61db 100644
--- a/components/layout/sidebar/org-switcher.tsx
+++ b/components/layout/sidebar/org-switcher.tsx
@@ -80,7 +80,9 @@ export function OrgSwitcher() {
lndev-ui
- Create or join workspace
+
+ Create or join workspace
+
Add an account
diff --git a/components/layout/theme-provider.tsx b/components/layout/theme-provider.tsx
index 7249aedb..f68bc3e7 100644
--- a/components/layout/theme-provider.tsx
+++ b/components/layout/theme-provider.tsx
@@ -3,10 +3,13 @@
import * as React from 'react';
import { ThemeProvider as NextThemesProvider, ThemeProviderProps } from 'next-themes';
-export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+export const ThemeProvider = React.memo(function ThemeProvider({
+ children,
+ ...props
+}: ThemeProviderProps) {
return (
{children}
);
-}
+});
diff --git a/components/schedules/schedule-calendar.tsx b/components/schedules/schedule-calendar.tsx
new file mode 100644
index 00000000..e106b73b
--- /dev/null
+++ b/components/schedules/schedule-calendar.tsx
@@ -0,0 +1,184 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { DayPicker } from 'react-day-picker';
+import { format } from 'date-fns';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Calendar, Clock, Plus } from 'lucide-react';
+import { ScheduleContentModal } from './schedule-content-modal';
+import 'react-day-picker/dist/style.css';
+
+interface Schedule {
+ id: string;
+ date: string;
+ status: string;
+ campaign: {
+ id: string;
+ name: string;
+ };
+ content?: {
+ id: string;
+ title: string;
+ };
+}
+
+interface ScheduleCalendarProps {
+ orgId: string;
+ onDateSelect?: (date: Date) => void;
+ onScheduleCreate?: (date: Date) => void;
+}
+
+export function ScheduleCalendar({ orgId, onDateSelect, onScheduleCreate }: ScheduleCalendarProps) {
+ const [schedules, setSchedules] = useState([]);
+ const [selectedDate, setSelectedDate] = useState();
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchSchedules();
+ }, [orgId]);
+
+ const fetchSchedules = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/schedules`);
+ if (response.ok) {
+ const data = await response.json();
+ setSchedules(data);
+ }
+ } catch (error) {
+ console.error('Error fetching schedules:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getScheduledDates = () => {
+ return schedules.map((schedule) => new Date(schedule.date));
+ };
+
+ const getSchedulesForDate = (date: Date) => {
+ return schedules.filter(
+ (schedule) => format(new Date(schedule.date), 'yyyy-MM-dd') === format(date, 'yyyy-MM-dd')
+ );
+ };
+
+ const handleDateSelect = (date: Date | undefined) => {
+ setSelectedDate(date);
+ if (date && onDateSelect) {
+ onDateSelect(date);
+ }
+ };
+
+ const handleScheduleCreated = () => {
+ fetchSchedules(); // Refresh the schedules
+ };
+
+ const modifiers = {
+ scheduled: getScheduledDates(),
+ };
+
+ const modifiersStyles = {
+ scheduled: {
+ backgroundColor: '#3b82f6',
+ color: 'white',
+ fontWeight: 'bold',
+ },
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ Schedule Calendar
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Schedule Calendar
+
+
+
+
+
+
+
+ Schedule Content
+
+ }
+ onScheduleCreated={handleScheduleCreated}
+ />
+
+
+
+
+
+
+
+
+ {selectedDate ? format(selectedDate, 'MMM dd, yyyy') : 'Select a Date'}
+
+
+
+ {selectedDate ? (
+
+ {getSchedulesForDate(selectedDate).length > 0 ? (
+ getSchedulesForDate(selectedDate).map((schedule) => (
+
+
+
+ {schedule.status}
+
+
+ {format(new Date(schedule.date), 'HH:mm')}
+
+
+
{schedule.campaign.name}
+ {schedule.content && (
+
+ {schedule.content.title}
+
+ )}
+
+ ))
+ ) : (
+
No schedules for this date
+ )}
+
+ ) : (
+ Select a date to view schedules
+ )}
+
+
+
+ );
+}
diff --git a/components/schedules/schedule-content-modal.tsx b/components/schedules/schedule-content-modal.tsx
new file mode 100644
index 00000000..a907d6bb
--- /dev/null
+++ b/components/schedules/schedule-content-modal.tsx
@@ -0,0 +1,296 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { format } from 'date-fns';
+import { CalendarIcon, Clock } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Calendar } from '@/components/ui/calendar';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { createScheduleSchema } from '@/lib/schemas';
+
+interface Content {
+ id: string;
+ title: string;
+ campaign: {
+ id: string;
+ name: string;
+ };
+}
+
+interface Campaign {
+ id: string;
+ name: string;
+ contents: Content[];
+}
+
+interface ScheduleContentModalProps {
+ orgId: string;
+ trigger: React.ReactNode;
+ onScheduleCreated?: () => void;
+}
+
+export function ScheduleContentModal({
+ orgId,
+ trigger,
+ onScheduleCreated,
+}: ScheduleContentModalProps) {
+ const [open, setOpen] = useState(false);
+ const [campaigns, setCampaigns] = useState([]);
+ const [selectedCampaign, setSelectedCampaign] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(createScheduleSchema),
+ defaultValues: {
+ runAt: '',
+ status: 'PENDING',
+ campaignId: '',
+ contentId: '',
+ },
+ });
+
+ useEffect(() => {
+ if (open) {
+ fetchCampaigns();
+ }
+ }, [open]);
+
+ const fetchCampaigns = async () => {
+ try {
+ const response = await fetch(`/api/${orgId}/campaigns`);
+ if (response.ok) {
+ const data = await response.json();
+ setCampaigns(data);
+ }
+ } catch (error) {
+ console.error('Error fetching campaigns:', error);
+ }
+ };
+
+ const onSubmit = async (data: any) => {
+ setLoading(true);
+ try {
+ const response = await fetch(`/api/${orgId}/schedules`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+
+ if (response.ok) {
+ setOpen(false);
+ form.reset();
+ onScheduleCreated?.();
+ } else {
+ const error = await response.json();
+ console.error('Error creating schedule:', error);
+ }
+ } catch (error) {
+ console.error('Error creating schedule:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCampaignChange = (campaignId: string) => {
+ const campaign = campaigns.find((c) => c.id === campaignId);
+ setSelectedCampaign(campaign || null);
+ form.setValue('campaignId', campaignId);
+ form.setValue('contentId', ''); // Reset content selection
+ };
+
+ return (
+
+ {trigger}
+
+
+ Schedule Content Publication
+
+
+
+
+ (
+
+ Campaign
+
+
+
+
+
+
+
+ {campaigns.map((campaign) => (
+
+ {campaign.name}
+
+ ))}
+
+
+
+
+ )}
+ />
+
+ {selectedCampaign && selectedCampaign.contents.length > 0 && (
+ (
+
+ Content (Optional)
+
+
+
+
+
+
+
+ {selectedCampaign.contents.map((content) => (
+
+ {content.title}
+
+ ))}
+
+
+
+
+ )}
+ />
+ )}
+
+ (
+
+ Publication Date & Time
+
+
+
+
+ {field.value ? (
+ format(new Date(field.value), "PPP 'at' p")
+ ) : (
+ Pick a date and time
+ )}
+
+
+
+
+
+ {
+ if (date) {
+ // Set time to current time if no time is set
+ const now = new Date();
+ date.setHours(now.getHours(), now.getMinutes());
+ field.onChange(date.toISOString());
+ }
+ }}
+ disabled={(date) =>
+ date < new Date(new Date().setHours(0, 0, 0, 0))
+ }
+ initialFocus
+ />
+
+
+
+ {
+ if (field.value) {
+ const date = new Date(field.value);
+ const [hours, minutes] = e.target.value.split(':');
+ date.setHours(parseInt(hours), parseInt(minutes));
+ field.onChange(date.toISOString());
+ }
+ }}
+ />
+
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Status
+
+
+
+
+
+
+
+ Pending
+ Published
+
+
+
+
+ )}
+ />
+
+
+ setOpen(false)}>
+ Cancel
+
+
+ {loading ? 'Scheduling...' : 'Schedule Content'}
+
+
+
+
+
+
+ );
+}
diff --git a/components/ui/page-header.tsx b/components/ui/page-header.tsx
new file mode 100644
index 00000000..7a77b92c
--- /dev/null
+++ b/components/ui/page-header.tsx
@@ -0,0 +1,19 @@
+import { ReactNode } from 'react';
+
+interface PageHeaderProps {
+ title: string;
+ description?: string;
+ action?: ReactNode;
+}
+
+export function PageHeader({ title, description, action }: PageHeaderProps) {
+ return (
+
+
+
{title}
+ {description &&
{description}
}
+
+ {action &&
{action}
}
+
+ );
+}
diff --git a/db/seed.ts b/db/seed.ts
new file mode 100644
index 00000000..90dd10c4
--- /dev/null
+++ b/db/seed.ts
@@ -0,0 +1,331 @@
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+async function main() {
+ console.log('🌱 Seeding database...');
+
+ // Create organization
+ const organization = await prisma.organization.upsert({
+ where: { id: 'org_default_seed' },
+ update: {},
+ create: {
+ id: 'org_default_seed',
+ name: 'Default Organization',
+ },
+ });
+
+ // Create users
+ const user1 = await prisma.user.upsert({
+ where: { email: 'john@example.com' },
+ update: {},
+ create: {
+ email: 'john@example.com',
+ name: 'John Doe',
+ },
+ });
+
+ const user2 = await prisma.user.upsert({
+ where: { email: 'jane@example.com' },
+ update: {},
+ create: {
+ email: 'jane@example.com',
+ name: 'Jane Smith',
+ },
+ });
+
+ const user3 = await prisma.user.upsert({
+ where: { email: 'bob@example.com' },
+ update: {},
+ create: {
+ email: 'bob@example.com',
+ name: 'Bob Johnson',
+ },
+ });
+
+ // Create memberships
+ await prisma.membership.upsert({
+ where: { userId_organizationId: { userId: user1.id, organizationId: organization.id } },
+ update: {},
+ create: {
+ userId: user1.id,
+ organizationId: organization.id,
+ role: 'ADMIN',
+ },
+ });
+
+ await prisma.membership.upsert({
+ where: { userId_organizationId: { userId: user2.id, organizationId: organization.id } },
+ update: {},
+ create: {
+ userId: user2.id,
+ organizationId: organization.id,
+ role: 'BRAND_OWNER',
+ },
+ });
+
+ await prisma.membership.upsert({
+ where: { userId_organizationId: { userId: user3.id, organizationId: organization.id } },
+ update: {},
+ create: {
+ userId: user3.id,
+ organizationId: organization.id,
+ role: 'CREATOR',
+ },
+ });
+
+ // Create campaigns
+ const campaign1 = await prisma.campaign.create({
+ data: {
+ name: 'Summer Marketing Campaign 2024',
+ summary: 'Boost summer sales with targeted social media campaigns',
+ description:
+ 'A comprehensive marketing campaign targeting young adults during summer months. Focus on social media platforms and influencer partnerships.',
+ organizationId: organization.id,
+ leadId: user1.id,
+ health: 'ON_TRACK',
+ status: 'PLANNING',
+ priority: 'HIGH',
+ startDate: new Date('2024-06-01'),
+ targetDate: new Date('2024-08-31'),
+ },
+ });
+
+ const campaign2 = await prisma.campaign.create({
+ data: {
+ name: 'Product Launch Q4',
+ summary: 'Launch new product line with multi-channel marketing',
+ description:
+ 'Strategic launch campaign for our new product line. Includes PR, social media, and influencer marketing.',
+ organizationId: organization.id,
+ leadId: user2.id,
+ health: 'AT_RISK',
+ status: 'READY',
+ priority: 'URGENT',
+ startDate: new Date('2024-10-01'),
+ targetDate: new Date('2024-12-31'),
+ },
+ });
+
+ const campaign3 = await prisma.campaign.create({
+ data: {
+ name: 'Brand Awareness Campaign',
+ summary: 'Increase brand recognition in target markets',
+ description:
+ 'Long-term brand awareness campaign focusing on content marketing and thought leadership.',
+ organizationId: organization.id,
+ leadId: user3.id,
+ health: 'OFF_TRACK',
+ status: 'DRAFT',
+ priority: 'MEDIUM',
+ startDate: new Date('2024-09-01'),
+ targetDate: new Date('2025-02-28'),
+ },
+ });
+
+ // Create campaign members
+ await prisma.campaignMember.createMany({
+ data: [
+ {
+ campaignId: campaign1.id,
+ userId: user1.id,
+ role: 'OWNER',
+ },
+ {
+ campaignId: campaign1.id,
+ userId: user2.id,
+ role: 'MANAGER',
+ },
+ {
+ campaignId: campaign1.id,
+ userId: user3.id,
+ role: 'MEMBER',
+ },
+ {
+ campaignId: campaign2.id,
+ userId: user2.id,
+ role: 'OWNER',
+ },
+ {
+ campaignId: campaign2.id,
+ userId: user1.id,
+ role: 'MANAGER',
+ },
+ {
+ campaignId: campaign3.id,
+ userId: user3.id,
+ role: 'OWNER',
+ },
+ ],
+ });
+
+ // Create campaign labels
+ await prisma.campaignLabel.createMany({
+ data: [
+ {
+ campaignId: campaign1.id,
+ name: 'Summer',
+ color: '#FF6B6B',
+ },
+ {
+ campaignId: campaign1.id,
+ name: 'Social Media',
+ color: '#4ECDC4',
+ },
+ {
+ campaignId: campaign2.id,
+ name: 'Product Launch',
+ color: '#45B7D1',
+ },
+ {
+ campaignId: campaign2.id,
+ name: 'Q4',
+ color: '#96CEB4',
+ },
+ {
+ campaignId: campaign3.id,
+ name: 'Brand',
+ color: '#FFEAA7',
+ },
+ ],
+ });
+
+ // Create campaign tasks
+ const task1 = await prisma.campaignTask.create({
+ data: {
+ title: 'Design social media assets',
+ description: 'Create visual assets for Instagram, Facebook, and Twitter',
+ campaignId: campaign1.id,
+ assigneeId: user3.id,
+ status: 'IN_PROGRESS',
+ priority: 'HIGH',
+ dueDate: new Date('2024-06-15'),
+ },
+ });
+
+ const task2 = await prisma.campaignTask.create({
+ data: {
+ title: 'Plan influencer partnerships',
+ description: 'Research and contact potential influencers for collaboration',
+ campaignId: campaign1.id,
+ assigneeId: user2.id,
+ status: 'TODO',
+ priority: 'MEDIUM',
+ dueDate: new Date('2024-06-20'),
+ },
+ });
+
+ const task3 = await prisma.campaignTask.create({
+ data: {
+ title: 'Create campaign timeline',
+ description: 'Develop detailed timeline for campaign execution',
+ campaignId: campaign1.id,
+ assigneeId: user1.id,
+ status: 'DONE',
+ priority: 'LOW',
+ dueDate: new Date('2024-06-10'),
+ },
+ });
+
+ // Create subtasks
+ await prisma.campaignTask.create({
+ data: {
+ title: 'Instagram post designs',
+ description: 'Design 10 Instagram posts for the campaign',
+ campaignId: campaign1.id,
+ assigneeId: user3.id,
+ status: 'TODO',
+ priority: 'HIGH',
+ dueDate: new Date('2024-06-12'),
+ parentTaskId: task1.id,
+ },
+ });
+
+ await prisma.campaignTask.create({
+ data: {
+ title: 'Facebook banner designs',
+ description: 'Create Facebook banner and cover images',
+ campaignId: campaign1.id,
+ assigneeId: user3.id,
+ status: 'TODO',
+ priority: 'MEDIUM',
+ dueDate: new Date('2024-06-14'),
+ parentTaskId: task1.id,
+ },
+ });
+
+ // Create campaign milestones
+ await prisma.campaignMilestone.createMany({
+ data: [
+ {
+ campaignId: campaign1.id,
+ title: 'Campaign Planning Complete',
+ description: 'All planning documents and strategies finalized',
+ dueDate: new Date('2024-06-15'),
+ completedAt: new Date('2024-06-10'),
+ },
+ {
+ campaignId: campaign1.id,
+ title: 'Asset Creation Complete',
+ description: 'All visual and content assets created',
+ dueDate: new Date('2024-06-30'),
+ },
+ {
+ campaignId: campaign1.id,
+ title: 'Campaign Launch',
+ description: 'Campaign goes live across all channels',
+ dueDate: new Date('2024-07-01'),
+ },
+ {
+ campaignId: campaign1.id,
+ title: 'Campaign Completion',
+ description: 'Campaign ends and results are analyzed',
+ dueDate: new Date('2024-08-31'),
+ },
+ ],
+ });
+
+ // Create content for campaigns
+ const content1 = await prisma.content.create({
+ data: {
+ title: 'Summer Vibes Social Media Post',
+ body: 'Get ready for summer with our amazing deals! 🌞☀️ #SummerVibes #Marketing',
+ campaignId: campaign1.id,
+ status: 'APPROVED',
+ },
+ });
+
+ const content2 = await prisma.content.create({
+ data: {
+ title: 'Product Launch Announcement',
+ body: 'Exciting news! Our new product line is launching soon. Stay tuned for updates! 🚀',
+ campaignId: campaign2.id,
+ status: 'DRAFT',
+ },
+ });
+
+ // Create schedules
+ await prisma.schedule.create({
+ data: {
+ name: 'Summer Campaign Launch',
+ campaignId: campaign1.id,
+ contentId: content1.id,
+ channel: 'INSTAGRAM',
+ runAt: new Date('2024-07-01T10:00:00Z'),
+ timezone: 'UTC',
+ status: 'PENDING',
+ },
+ });
+
+ console.log('✅ Database seeded successfully!');
+ console.log(`📊 Created ${organization.name} with campaigns and team members`);
+}
+
+main()
+ .catch((e) => {
+ console.error('❌ Error seeding database:', e);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..2ee90b2f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,33 @@
+version: '3.8'
+
+services:
+ app:
+ build: .
+ ports:
+ - "3000:3000"
+ environment:
+ - DATABASE_URL=postgresql://postgres:postgres@db:5432/circle_dev?schema=public
+ - NEXTAUTH_URL=http://localhost:3000
+ - NEXTAUTH_SECRET=changeme-in-production
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
+ - NODE_ENV=production
+ depends_on:
+ - db
+ volumes:
+ - .:/app
+ - /app/node_modules
+ command: pnpm start
+
+ db:
+ image: postgres:15
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=postgres
+ - POSTGRES_DB=circle_dev
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+
+volumes:
+ postgres_data:
\ No newline at end of file
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 00000000..5df0ecc7
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,691 @@
+# Contributing to AiM Platform
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Development Setup](#development-setup)
+- [Development Workflow](#development-workflow)
+- [Code Standards](#code-standards)
+- [Testing](#testing)
+- [Pull Request Process](#pull-request-process)
+- [Code Review Guidelines](#code-review-guidelines)
+
+## Overview
+
+Cảm ơn bạn đã quan tâm đến việc đóng góp cho AiM Platform! Document này sẽ hướng dẫn bạn cách setup development environment và contribute code một cách hiệu quả.
+
+### What We're Building
+
+AiM Platform là một AI-powered marketing platform cho phép Creators, Brand Owners, và Admins quản lý campaigns, tạo content với AI assistance, và track performance analytics.
+
+### How to Contribute
+
+- 🐛 **Bug Reports**: Report bugs và issues
+- 💡 **Feature Requests**: Đề xuất tính năng mới
+- 🔧 **Code Contributions**: Fix bugs, implement features
+- 📚 **Documentation**: Improve docs và examples
+- 🧪 **Testing**: Write tests và improve test coverage
+
+## Development Setup
+
+### 🛠️ Prerequisites
+
+#### Required Software
+
+- **Node.js**: Version 18.17+ (LTS recommended)
+- **pnpm**: Version 8.0+ (faster than npm)
+- **PostgreSQL**: Version 14+ (for database)
+- **Git**: Version 2.30+
+
+#### System Requirements
+
+- **RAM**: Minimum 8GB, Recommended 16GB+
+- **Storage**: 10GB+ free space
+- **OS**: Windows 10+, macOS 10.15+, Ubuntu 18.04+
+
+### 🚀 Quick Start
+
+#### 1. Clone Repository
+
+```bash
+git clone https://github.com/your-org/aim-platform.git
+cd aim-platform
+```
+
+#### 2. Install Dependencies
+
+```bash
+# Install pnpm if you haven't
+npm install -g pnpm
+
+# Install project dependencies
+pnpm install
+```
+
+#### 3. Environment Setup
+
+```bash
+# Copy environment template
+cp .env.example .env
+
+# Edit .env with your configuration
+# DATABASE_URL, NEXTAUTH_SECRET, etc.
+```
+
+#### 4. Database Setup
+
+```bash
+# Generate Prisma client
+pnpm prisma generate
+
+# Push schema to database
+pnpm prisma db push
+
+# Seed database with sample data
+pnpm db:seed
+```
+
+#### 5. Start Development Server
+
+```bash
+# Start development server
+pnpm dev
+
+# Open http://localhost:3000
+```
+
+### 🔧 Development Tools
+
+#### VS Code Extensions (Recommended)
+
+```json
+{
+ "recommendations": [
+ "bradlc.vscode-tailwindcss",
+ "esbenp.prettier-vscode",
+ "ms-vscode.vscode-typescript-next",
+ "prisma.prisma",
+ "ms-vscode.vscode-json",
+ "formulahendry.auto-rename-tag",
+ "christian-kohler.path-intellisense"
+ ]
+}
+```
+
+#### Git Hooks (Automatic)
+
+```bash
+# Pre-commit hooks are automatically installed
+# They will run linting and formatting before commits
+```
+
+## Development Workflow
+
+### 🌿 Branch Strategy
+
+#### Branch Naming Convention
+
+```bash
+# Feature branches
+feature/description-of-feature
+feature/user-authentication
+
+# Bug fix branches
+fix/description-of-bug
+fix/login-validation-error
+
+# Hotfix branches
+hotfix/critical-security-fix
+
+# Documentation branches
+docs/update-api-documentation
+```
+
+#### Branch Workflow
+
+```bash
+# 1. Create feature branch from main
+git checkout main
+git pull origin main
+git checkout -b feature/your-feature-name
+
+# 2. Make changes and commit
+git add .
+git commit -m "feat: add user authentication system"
+
+# 3. Push branch and create PR
+git push origin feature/your-feature-name
+# Create Pull Request on GitHub
+```
+
+### 📝 Commit Message Convention
+
+#### Conventional Commits Format
+
+```bash
+# Format: type(scope): description
+
+# Examples:
+feat(auth): add NextAuth.js integration
+fix(api): resolve campaign creation validation error
+docs(api): update campaigns API documentation
+test(auth): add authentication test coverage
+refactor(ui): simplify component structure
+style(ui): fix button alignment issues
+perf(api): optimize database queries
+chore(deps): update dependencies to latest versions
+```
+
+#### Commit Types
+
+- **feat**: New feature
+- **fix**: Bug fix
+- **docs**: Documentation changes
+- **style**: Code style changes (formatting, etc.)
+- **refactor**: Code refactoring
+- **test**: Adding or updating tests
+- **chore**: Maintenance tasks
+- **perf**: Performance improvements
+
+### 🔄 Pull Request Process
+
+#### 1. Create Pull Request
+
+- Fork repository (if external contributor)
+- Create feature branch from main
+- Make changes following coding standards
+- Write tests for new functionality
+- Update documentation if needed
+
+#### 2. PR Description Template
+
+```markdown
+## Description
+
+Brief description of changes
+
+## Type of Change
+
+- [ ] Bug fix
+- [ ] New feature
+- [ ] Breaking change
+- [ ] Documentation update
+
+## Testing
+
+- [ ] Unit tests pass
+- [ ] Integration tests pass
+- [ ] Manual testing completed
+
+## Checklist
+
+- [ ] Code follows style guidelines
+- [ ] Self-review completed
+- [ ] Documentation updated
+- [ ] Tests added/updated
+- [ ] No console.log statements
+- [ ] No sensitive data in logs
+```
+
+#### 3. Code Review Process
+
+- **Automated Checks**: CI/CD pipeline runs tests
+- **Peer Review**: At least one team member reviews
+- **Address Feedback**: Make requested changes
+- **Merge**: Once approved and all checks pass
+
+## Code Standards
+
+### 🎨 Code Style
+
+#### TypeScript Guidelines
+
+```typescript
+// Use strict TypeScript
+// tsconfig.json: "strict": true
+
+// Prefer interfaces over types for objects
+interface User {
+ id: string;
+ email: string;
+ name: string;
+}
+
+// Use type for unions, intersections, etc.
+type UserRole = 'admin' | 'creator' | 'brand_owner';
+type UserWithRole = User & { role: UserRole };
+
+// Prefer const assertions
+const PERMISSIONS = {
+ READ: 'read',
+ WRITE: 'write',
+} as const;
+
+// Use proper error handling
+try {
+ const result = await riskyOperation();
+ return result;
+} catch (error) {
+ logger.error('Operation failed', { error: error.message });
+ throw new BusinessLogicError('Operation failed', error);
+}
+```
+
+#### React Guidelines
+
+```typescript
+// Use functional components with hooks
+export function UserProfile({ userId }: { userId: string }) {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchUser(userId).then(setUser).finally(() => setLoading(false));
+ }, [userId]);
+
+ if (loading) return ;
+ if (!user) return ;
+
+ return (
+
+
{user.name}
+
{user.email}
+
+ );
+}
+
+// Use proper prop types
+interface ButtonProps {
+ variant?: 'primary' | 'secondary' | 'danger';
+ size?: 'sm' | 'md' | 'lg';
+ disabled?: boolean;
+ onClick?: () => void;
+ children: React.ReactNode;
+}
+```
+
+#### File Organization
+
+```bash
+# Component structure
+components/
+├── ui/ # Reusable UI components
+│ ├── button.tsx
+│ ├── input.tsx
+│ └── modal.tsx
+├── forms/ # Form components
+│ ├── campaign-form.tsx
+│ └── user-form.tsx
+├── layout/ # Layout components
+│ ├── header.tsx
+│ ├── sidebar.tsx
+│ └── footer.tsx
+└── features/ # Feature-specific components
+ ├── campaigns/
+ ├── content/
+ └── analytics/
+
+# API structure
+app/api/
+├── [orgId]/ # Organization-scoped APIs
+│ ├── campaigns/
+│ ├── content/
+│ └── analytics/
+├── auth/ # Authentication APIs
+└── health/ # Health check APIs
+```
+
+### 🔒 Security Guidelines
+
+#### Input Validation
+
+```typescript
+// Always validate input data
+import { z } from 'zod';
+
+const createUserSchema = z.object({
+ email: z.string().email('Invalid email format'),
+ password: z.string().min(8, 'Password too short'),
+ name: z.string().min(1, 'Name is required'),
+});
+
+export async function POST(request: NextRequest) {
+ const body = await request.json();
+ const validation = createUserSchema.safeParse(body);
+
+ if (!validation.success) {
+ return NextResponse.json(
+ {
+ error: 'E_VALIDATION',
+ details: validation.error.errors,
+ },
+ { status: 400 }
+ );
+ }
+
+ // Proceed with validated data
+ const user = await createUser(validation.data);
+ return NextResponse.json(user);
+}
+```
+
+#### Authentication & Authorization
+
+```typescript
+// Always check permissions
+export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ try {
+ await requirePermission(session.user.id, 'campaign:delete');
+
+ const campaign = await deleteCampaign(params.id);
+ return NextResponse.json(campaign);
+ } catch (error) {
+ if (error instanceof PermissionError) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ throw error;
+ }
+}
+```
+
+### 📚 Documentation Standards
+
+#### Code Documentation
+
+```typescript
+/**
+ * Creates a new campaign for the specified organization
+ * @param data - Campaign creation data
+ * @param userId - ID of the user creating the campaign
+ * @param orgId - Organization ID
+ * @returns Promise resolving to the created campaign
+ * @throws {ValidationError} When campaign data is invalid
+ * @throws {PermissionError} When user lacks required permissions
+ */
+export async function createCampaign(
+ data: CreateCampaignRequest,
+ userId: string,
+ orgId: string
+): Promise {
+ // Implementation...
+}
+```
+
+#### API Documentation
+
+```typescript
+/**
+ * @api {post} /api/[orgId]/campaigns Create Campaign
+ * @apiName CreateCampaign
+ * @apiGroup Campaigns
+ * @apiPermission campaign:create
+ *
+ * @apiParam {String} name Campaign name (required)
+ * @apiParam {String} [description] Campaign description
+ * @apiParam {Date} startDate Campaign start date
+ * @apiParam {Date} endDate Campaign end date
+ *
+ * @apiSuccess {Campaign} campaign Created campaign object
+ * @apiError {String} E_VALIDATION Validation error
+ * @apiError {String} E_FORBIDDEN Insufficient permissions
+ */
+```
+
+## Testing
+
+### 🧪 Testing Strategy
+
+#### Test Types
+
+- **Unit Tests**: Individual functions và components
+- **Integration Tests**: API endpoints và database operations
+- **E2E Tests**: Complete user workflows
+- **Performance Tests**: Load testing và optimization
+
+#### Test Setup
+
+```bash
+# Run all tests
+pnpm test
+
+# Run tests in watch mode
+pnpm test:watch
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run E2E tests
+pnpm test:e2e
+
+# Run E2E tests with UI
+pnpm test:e2e:ui
+```
+
+#### Writing Tests
+
+```typescript
+// Unit test example
+import { render, screen } from '@testing-library/react';
+import { UserProfile } from './UserProfile';
+
+describe('UserProfile', () => {
+ it('should display user information', () => {
+ const mockUser = {
+ id: '1',
+ name: 'John Doe',
+ email: 'john@example.com'
+ };
+
+ render( );
+
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ expect(screen.getByText('john@example.com')).toBeInTheDocument();
+ });
+
+ it('should show loading state', () => {
+ render( );
+
+ expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
+ });
+});
+
+// API test example
+import { createMocks } from 'node-mocks-http';
+import { GET } from './route';
+
+describe('/api/campaigns', () => {
+ it('should return campaigns for authenticated user', async () => {
+ const { req, res } = createMocks({
+ method: 'GET',
+ headers: {
+ 'authorization': 'Bearer valid-token'
+ }
+ });
+
+ await GET(req, { params: { orgId: 'org-123' } });
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(JSON.parse(res._getData())).toHaveProperty('campaigns');
+ });
+});
+```
+
+### 📊 Test Coverage
+
+#### Coverage Requirements
+
+- **Unit Tests**: Minimum 80% coverage
+- **Critical Paths**: 100% coverage required
+- **API Endpoints**: All endpoints must have tests
+- **Components**: Core components must have tests
+
+#### Coverage Report
+
+```bash
+# Generate coverage report
+pnpm test:coverage
+
+# View coverage in browser
+open coverage/lcov-report/index.html
+```
+
+## Pull Request Process
+
+### 🔍 Pre-PR Checklist
+
+#### Code Quality
+
+- [ ] Code follows style guidelines
+- [ ] All tests pass
+- [ ] No console.log statements
+- [ ] No sensitive data in code
+- [ ] Proper error handling implemented
+- [ ] Input validation added
+
+#### Documentation
+
+- [ ] Code is properly documented
+- [ ] API documentation updated
+- [ ] README updated if needed
+- [ ] Changelog updated
+
+#### Testing
+
+- [ ] Unit tests written
+- [ ] Integration tests added
+- [ ] E2E tests updated
+- [ ] Test coverage adequate
+
+### 📋 PR Review Process
+
+#### Automated Checks
+
+- **Linting**: ESLint và Prettier checks
+- **Type Checking**: TypeScript compilation
+- **Tests**: Unit và integration tests
+- **Build**: Production build verification
+- **Security**: Security vulnerability scan
+
+#### Manual Review
+
+- **Code Quality**: Readability và maintainability
+- **Security**: Authentication, authorization, validation
+- **Performance**: Efficient algorithms và queries
+- **Testing**: Adequate test coverage
+- **Documentation**: Clear và complete
+
+#### Review Guidelines
+
+```markdown
+# Code Review Checklist
+
+## Functionality
+
+- [ ] Does the code do what it's supposed to do?
+- [ ] Are edge cases handled?
+- [ ] Is error handling appropriate?
+
+## Code Quality
+
+- [ ] Is the code readable and maintainable?
+- [ ] Are there any code smells?
+- [ ] Is the code following established patterns?
+
+## Security
+
+- [ ] Are inputs properly validated?
+- [ ] Are permissions checked?
+- [ ] Is sensitive data protected?
+
+## Testing
+
+- [ ] Are tests comprehensive?
+- [ ] Do tests cover edge cases?
+- [ ] Are tests maintainable?
+
+## Documentation
+
+- [ ] Is the code self-documenting?
+- [ ] Are complex parts explained?
+- [ ] Is API documentation updated?
+```
+
+### 🚀 Deployment Process
+
+#### Staging Deployment
+
+```bash
+# Staging deployment is automatic on PR merge
+# Tests must pass before deployment
+# Manual approval required for production
+```
+
+#### Production Deployment
+
+```bash
+# Production deployment requires:
+# 1. All tests passing
+# 2. Code review approval
+# 3. Security review approval
+# 4. Manual deployment approval
+```
+
+## Getting Help
+
+### 💬 Communication Channels
+
+#### Development Team
+
+- **Slack**: #aim-platform-dev
+- **Email**: dev@aim-platform.com
+- **GitHub Issues**: Bug reports và feature requests
+- **GitHub Discussions**: General questions và discussions
+
+#### Documentation
+
+- **API Docs**: `/docs/api/`
+- **Architecture**: `/docs/architecture.md`
+- **Security**: `/docs/SECURITY.md`
+- **Contributing**: This document
+
+### 🆘 Common Issues
+
+#### Setup Issues
+
+```bash
+# Database connection failed
+# Check DATABASE_URL in .env
+# Ensure PostgreSQL is running
+
+# Dependencies installation failed
+# Clear pnpm cache: pnpm store prune
+# Delete node_modules and reinstall
+
+# Build errors
+# Check TypeScript errors: pnpm typecheck
+# Verify all imports are correct
+```
+
+#### Development Issues
+
+```bash
+# Tests failing
+# Run tests individually: pnpm test -- --testNamePattern="test name"
+# Check test environment setup
+
+# API endpoints not working
+# Verify middleware configuration
+# Check authentication setup
+# Review API route handlers
+```
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Development Team_
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 00000000..43e9d963
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,1012 @@
+# Security Documentation
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Authentication](#authentication)
+- [Authorization & RBAC](#authorization--rbac)
+- [Input Validation](#input-validation)
+- [Data Protection](#data-protection)
+- [API Security](#api-security)
+- [Infrastructure Security](#infrastructure-security)
+- [Security Best Practices](#security-best-practices)
+
+## Overview
+
+Security là top priority cho AiM Platform. Document này cover tất cả security measures, từ authentication và authorization đến data protection và infrastructure security.
+
+### Security Principles
+
+1. **Defense in Depth**: Multiple layers of security
+2. **Least Privilege**: Users chỉ có access cần thiết
+3. **Zero Trust**: Verify everything, trust nothing
+4. **Security by Design**: Security built into every layer
+5. **Regular Audits**: Continuous security assessment
+
+## Authentication
+
+### 🔐 NextAuth.js Implementation
+
+#### Configuration
+
+```typescript
+// lib/auth.ts
+import NextAuth from 'next-auth';
+import CredentialsProvider from 'next-auth/providers/credentials';
+import { PrismaAdapter } from '@auth/prisma-adapter';
+import prisma from '@/lib/prisma';
+import bcrypt from 'bcryptjs';
+
+export const { handlers, auth, signIn, signOut } = NextAuth({
+ adapter: PrismaAdapter(prisma),
+ session: {
+ strategy: 'jwt',
+ maxAge: 24 * 60 * 60, // 24 hours
+ },
+ providers: [
+ CredentialsProvider({
+ name: 'credentials',
+ credentials: {
+ email: { label: 'Email', type: 'email' },
+ password: { label: 'Password', type: 'password' },
+ },
+ async authorize(credentials) {
+ if (!credentials?.email || !credentials?.password) {
+ return null;
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { email: credentials.email },
+ include: { memberships: true },
+ });
+
+ if (!user || !user.password) {
+ return null;
+ }
+
+ const isValidPassword = await bcrypt.compare(credentials.password, user.password);
+
+ if (!isValidPassword) {
+ return null;
+ }
+
+ return {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ role: user.memberships[0]?.role || 'CREATOR',
+ };
+ },
+ }),
+ ],
+ callbacks: {
+ async jwt({ token, user }) {
+ if (user) {
+ token.role = user.role;
+ token.organizationId = user.organizationId;
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ if (token) {
+ session.user.id = token.sub!;
+ session.user.role = token.role;
+ session.user.organizationId = token.organizationId;
+ }
+ return session;
+ },
+ },
+ pages: {
+ signIn: '/auth/signin',
+ error: '/auth/error',
+ },
+});
+```
+
+#### Password Security
+
+```typescript
+// lib/auth-utils.ts
+import bcrypt from 'bcryptjs';
+
+export const PASSWORD_REQUIREMENTS = {
+ minLength: 8,
+ requireUppercase: true,
+ requireLowercase: true,
+ requireNumbers: true,
+ requireSpecialChars: true,
+};
+
+export function validatePassword(password: string): boolean {
+ const hasMinLength = password.length >= PASSWORD_REQUIREMENTS.minLength;
+ const hasUppercase = /[A-Z]/.test(password);
+ const hasLowercase = /[a-z]/.test(password);
+ const hasNumbers = /\d/.test(password);
+ const hasSpecialChars = /[!@#$%^&*(),.?":{}|<>]/.test(password);
+
+ return hasMinLength && hasUppercase && hasLowercase && hasNumbers && hasSpecialChars;
+}
+
+export async function hashPassword(password: string): Promise {
+ const saltRounds = 12;
+ return bcrypt.hash(password, saltRounds);
+}
+
+export async function verifyPassword(password: string, hash: string): Promise {
+ return bcrypt.compare(password, hash);
+}
+```
+
+### 🚪 Session Management
+
+#### Session Security
+
+```typescript
+// Session configuration
+export const SESSION_CONFIG = {
+ maxAge: 24 * 60 * 60, // 24 hours
+ updateAge: 60 * 60, // 1 hour
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ sameSite: 'lax' as const,
+ path: '/',
+};
+
+// Session validation middleware
+export function validateSession(session: any): boolean {
+ if (!session?.user?.id) return false;
+ if (!session?.user?.email) return false;
+ if (!session?.user?.role) return false;
+
+ const validRoles = ['ADMIN', 'BRAND_OWNER', 'CREATOR'];
+ if (!validRoles.includes(session.user.role)) return false;
+
+ return true;
+}
+```
+
+## Authorization & RBAC
+
+### 🔒 Role-Based Access Control
+
+#### Permission System
+
+```typescript
+// lib/rbac.ts
+export const PERMISSIONS = {
+ // Campaign permissions
+ CAMPAIGN_CREATE: 'campaign:create',
+ CAMPAIGN_READ: 'campaign:read',
+ CAMPAIGN_UPDATE: 'campaign:update',
+ CAMPAIGN_DELETE: 'campaign:delete',
+
+ // Content permissions
+ CONTENT_CREATE: 'content:create',
+ CONTENT_READ: 'content:read',
+ CONTENT_UPDATE: 'content:update',
+ CONTENT_DELETE: 'content:delete',
+ CONTENT_APPROVE: 'content:approve',
+
+ // User management
+ USER_READ: 'user:read',
+ USER_CREATE: 'user:create',
+ USER_UPDATE: 'user:update',
+ USER_DELETE: 'user:delete',
+
+ // Organization management
+ ORG_READ: 'org:read',
+ ORG_UPDATE: 'org:update',
+ ORG_DELETE: 'org:delete',
+
+ // Analytics
+ ANALYTICS_READ: 'analytics:read',
+ ANALYTICS_EXPORT: 'analytics:export',
+} as const;
+
+export const ROLE_PERMISSIONS = {
+ CREATOR: [
+ PERMISSIONS.CAMPAIGN_READ,
+ PERMISSIONS.CONTENT_CREATE,
+ PERMISSIONS.CONTENT_READ,
+ PERMISSIONS.CONTENT_UPDATE,
+ PERMISSIONS.CONTENT_DELETE,
+ PERMISSIONS.ANALYTICS_READ,
+ ],
+ BRAND_OWNER: [
+ PERMISSIONS.CAMPAIGN_CREATE,
+ PERMISSIONS.CAMPAIGN_READ,
+ PERMISSIONS.CAMPAIGN_UPDATE,
+ PERMISSIONS.CAMPAIGN_DELETE,
+ PERMISSIONS.CONTENT_CREATE,
+ PERMISSIONS.CONTENT_READ,
+ PERMISSIONS.CONTENT_UPDATE,
+ PERMISSIONS.CONTENT_DELETE,
+ PERMISSIONS.CONTENT_APPROVE,
+ PERMISSIONS.USER_READ,
+ PERMISSIONS.ANALYTICS_READ,
+ PERMISSIONS.ANALYTICS_EXPORT,
+ ],
+ ADMIN: [...Object.values(PERMISSIONS)],
+} as const;
+```
+
+#### Permission Checking
+
+```typescript
+// lib/permissions.ts
+export async function hasPermission(
+ userId: string,
+ orgId: string,
+ permission: string
+): Promise {
+ const membership = await prisma.membership.findFirst({
+ where: {
+ userId,
+ organizationId: orgId,
+ },
+ });
+
+ if (!membership) return false;
+
+ const userPermissions = ROLE_PERMISSIONS[membership.role] || [];
+ return userPermissions.includes(permission as any);
+}
+
+export async function requirePermission(
+ userId: string,
+ orgId: string,
+ permission: string
+): Promise {
+ const hasAccess = await hasPermission(userId, orgId, permission);
+
+ if (!hasAccess) {
+ throw new PermissionError(`Insufficient permissions: ${permission}`, userId, permission);
+ }
+}
+
+// Usage in API routes
+export async function GET(request: NextRequest, { params }: { params: { orgId: string } }) {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ try {
+ await requirePermission(session.user.id, params.orgId, PERMISSIONS.CAMPAIGN_READ);
+
+ // Proceed with operation
+ const campaigns = await prisma.campaign.findMany({
+ where: { organizationId: params.orgId },
+ });
+
+ return NextResponse.json(campaigns);
+ } catch (error) {
+ if (error instanceof PermissionError) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ throw error;
+ }
+}
+```
+
+### 🛡️ Route Protection
+
+#### Middleware Protection
+
+```typescript
+// middleware.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+
+export async function middleware(request: NextRequest) {
+ const session = await auth();
+
+ // Public routes
+ const publicRoutes = ['/auth/signin', '/auth/signup', '/api/health'];
+ if (publicRoutes.some((route) => request.nextUrl.pathname.startsWith(route))) {
+ return NextResponse.next();
+ }
+
+ // API routes protection
+ if (request.nextUrl.pathname.startsWith('/api/')) {
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Organization-scoped API protection
+ if (request.nextUrl.pathname.startsWith('/api/')) {
+ const orgId = request.nextUrl.pathname.split('/')[2];
+ if (orgId && orgId !== '[orgId]') {
+ const hasAccess = await hasOrganizationAccess(session.user.id, orgId);
+ if (!hasAccess) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+ }
+ }
+ }
+
+ // Page routes protection
+ if (request.nextUrl.pathname.startsWith('/dashboard/')) {
+ if (!session?.user?.id) {
+ return NextResponse.redirect(new URL('/auth/signin', request.url));
+ }
+ }
+
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: [
+ '/api/:path*',
+ '/dashboard/:path*',
+ '/campaigns/:path*',
+ '/content/:path*',
+ '/assets/:path*',
+ '/analytics/:path*',
+ ],
+};
+```
+
+## Input Validation
+
+### ✅ Zod Schema Validation
+
+#### Request Validation
+
+```typescript
+// lib/schemas.ts
+import { z } from 'zod';
+
+export const createCampaignSchema = z
+ .object({
+ name: z
+ .string()
+ .min(1, 'Campaign name is required')
+ .max(100, 'Campaign name must be less than 100 characters')
+ .regex(/^[a-zA-Z0-9\s\-_]+$/, 'Campaign name contains invalid characters'),
+ description: z.string().max(500, 'Description must be less than 500 characters').optional(),
+ startDate: z
+ .string()
+ .datetime('Invalid start date format')
+ .refine((date) => new Date(date) > new Date(), 'Start date must be in the future'),
+ endDate: z.string().datetime('Invalid end date format'),
+ budget: z
+ .number()
+ .positive('Budget must be positive')
+ .max(1000000, 'Budget cannot exceed 1,000,000'),
+ })
+ .refine((data) => new Date(data.endDate) > new Date(data.startDate), {
+ message: 'End date must be after start date',
+ path: ['endDate'],
+ });
+
+export const createContentSchema = z.object({
+ title: z.string().min(1, 'Title is required').max(200, 'Title must be less than 200 characters'),
+ body: z.string().max(10000, 'Content body must be less than 10,000 characters').optional(),
+ campaignId: z.string().cuid('Invalid campaign ID'),
+ type: z.enum(['post', 'video', 'image', 'story'], {
+ errorMap: () => ({ message: 'Invalid content type' }),
+ }),
+});
+
+export const uploadAssetSchema = z.object({
+ name: z
+ .string()
+ .min(1, 'Asset name is required')
+ .max(100, 'Asset name must be less than 100 characters'),
+ description: z.string().max(500, 'Description must be less than 500 characters').optional(),
+ tags: z.array(z.string()).max(20, 'Maximum 20 tags allowed').optional(),
+ contentId: z.string().cuid('Invalid content ID').optional(),
+});
+```
+
+#### Validation Middleware
+
+```typescript
+// lib/validation.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { ZodSchema } from 'zod';
+
+export function validateRequest(
+ schema: ZodSchema,
+ data: any
+): { success: true; data: T } | { success: false; errors: string[] } {
+ try {
+ const validatedData = schema.parse(data);
+ return { success: true, data: validatedData };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const errors = error.errors.map((err) => `${err.path.join('.')}: ${err.message}`);
+ return { success: false, errors };
+ }
+ return { success: false, errors: ['Validation failed'] };
+ }
+}
+
+// Usage in API routes
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const validation = validateRequest(createCampaignSchema, body);
+
+ if (!validation.success) {
+ return NextResponse.json(
+ {
+ error: 'E_VALIDATION',
+ message: 'Validation failed',
+ details: validation.errors,
+ },
+ { status: 400 }
+ );
+ }
+
+ const campaign = await prisma.campaign.create({
+ data: validation.data,
+ });
+
+ return NextResponse.json(campaign, { status: 201 });
+ } catch (error) {
+ return NextResponse.json(
+ {
+ error: 'E_INTERNAL_ERROR',
+ message: 'Internal server error',
+ },
+ { status: 500 }
+ );
+ }
+}
+```
+
+### 🚫 Input Sanitization
+
+#### XSS Prevention
+
+```typescript
+// lib/sanitization.ts
+import DOMPurify from 'dompurify';
+
+export function sanitizeHtml(html: string): string {
+ return DOMPurify.sanitize(html, {
+ ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li', 'a'],
+ ALLOWED_ATTR: ['href', 'target'],
+ ALLOWED_URI_REGEXP:
+ /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
+ });
+}
+
+export function sanitizeText(text: string): string {
+ return text
+ .replace(/[<>]/g, '') // Remove < and >
+ .replace(/javascript:/gi, '') // Remove javascript: protocol
+ .replace(/on\w+=/gi, '') // Remove event handlers
+ .trim();
+}
+
+export function sanitizeFilename(filename: string): string {
+ return filename
+ .replace(/[^a-zA-Z0-9.-]/g, '_') // Replace invalid chars with underscore
+ .replace(/_{2,}/g, '_') // Replace multiple underscores with single
+ .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
+}
+```
+
+## Data Protection
+
+### 🔒 Data Encryption
+
+#### Sensitive Data Encryption
+
+```typescript
+// lib/encryption.ts
+import crypto from 'crypto';
+
+const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
+const ALGORITHM = 'aes-256-gcm';
+
+export function encrypt(text: string): { encryptedData: string; iv: string; authTag: string } {
+ const iv = crypto.randomBytes(16);
+ const cipher = crypto.createCipher(ALGORITHM, ENCRYPTION_KEY, iv);
+
+ let encrypted = cipher.update(text, 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+
+ const authTag = cipher.getAuthTag();
+
+ return {
+ encryptedData: encrypted,
+ iv: iv.toString('hex'),
+ authTag: authTag.toString('hex'),
+ };
+}
+
+export function decrypt(encryptedData: string, iv: string, authTag: string): string {
+ const decipher = crypto.createDecipher(ALGORITHM, ENCRYPTION_KEY, Buffer.from(iv, 'hex'));
+ decipher.setAuthTag(Buffer.from(authTag, 'hex'));
+
+ let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+
+ return decrypted;
+}
+
+// Usage for sensitive fields
+export async function createUserWithEncryptedData(data: CreateUserRequest) {
+ const encryptedPassword = encrypt(data.password);
+
+ return prisma.user.create({
+ data: {
+ ...data,
+ password: encryptedPassword.encryptedData,
+ passwordIv: encryptedPassword.iv,
+ passwordAuthTag: encryptedPassword.authTag,
+ },
+ });
+}
+```
+
+#### Database Field Encryption
+
+```typescript
+// prisma/schema.prisma
+model User {
+ id String @id @default(cuid())
+ email String @unique
+ password String // Encrypted password
+ passwordIv String // Initialization vector
+ passwordAuthTag String // Authentication tag
+ // ... other fields
+}
+```
+
+### 🚫 PII Protection
+
+#### PII Detection & Masking
+
+```typescript
+// lib/pii-protection.ts
+export const PII_PATTERNS = {
+ email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
+ phone: /\b(\+\d{1,3}[-.]?)?\(?\d{3}\)?[-.]?\d{3}[-.]?\d{4}\b/g,
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
+ creditCard: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g,
+};
+
+export function maskPII(text: string): string {
+ let maskedText = text;
+
+ // Mask email addresses
+ maskedText = maskedText.replace(PII_PATTERNS.email, (match) => {
+ const [local, domain] = match.split('@');
+ return `${local.charAt(0)}***@${domain}`;
+ });
+
+ // Mask phone numbers
+ maskedText = maskedText.replace(PII_PATTERNS.phone, '***-***-****');
+
+ // Mask SSN
+ maskedText = maskedText.replace(PII_PATTERNS.ssn, '***-**-****');
+
+ // Mask credit card numbers
+ maskedText = maskedText.replace(PII_PATTERNS.creditCard, '****-****-****-****');
+
+ return maskedText;
+}
+
+// Log PII-safe messages
+export function logSafe(message: string, data: any): void {
+ const safeData = JSON.parse(JSON.stringify(data), (key, value) => {
+ if (typeof value === 'string') {
+ return maskPII(value);
+ }
+ return value;
+ });
+
+ logger.info(message, safeData);
+}
+```
+
+## API Security
+
+### 🛡️ Rate Limiting
+
+#### Rate Limiting Implementation
+
+```typescript
+// lib/rate-limit.ts
+import { NextRequest, NextResponse } from 'next/server';
+
+interface RateLimitConfig {
+ windowMs: number;
+ maxRequests: number;
+ keyGenerator: (req: NextRequest) => string;
+}
+
+export class RateLimiter {
+ private requests = new Map();
+
+ constructor(private config: RateLimitConfig) {}
+
+ isAllowed(req: NextRequest): boolean {
+ const key = this.config.keyGenerator(req);
+ const now = Date.now();
+
+ if (!this.requests.has(key)) {
+ this.requests.set(key, { count: 1, resetTime: now + this.config.windowMs });
+ return true;
+ }
+
+ const record = this.requests.get(key)!;
+
+ if (now > record.resetTime) {
+ record.count = 1;
+ record.resetTime = now + this.config.windowMs;
+ return true;
+ }
+
+ if (record.count >= this.config.maxRequests) {
+ return false;
+ }
+
+ record.count++;
+ return true;
+ }
+
+ getRemaining(req: NextRequest): number {
+ const key = this.config.keyGenerator(req);
+ const record = this.requests.get(key);
+
+ if (!record) return this.config.maxRequests;
+
+ const now = Date.now();
+ if (now > record.resetTime) return this.config.maxRequests;
+
+ return Math.max(0, this.config.maxRequests - record.count);
+ }
+}
+
+// Rate limiting middleware
+export function createRateLimitMiddleware(config: RateLimitConfig) {
+ const limiter = new RateLimiter(config);
+
+ return function rateLimitMiddleware(req: NextRequest) {
+ if (!limiter.isAllowed(req)) {
+ const remaining = limiter.getRemaining(req);
+ const resetTime = new Date(Date.now() + config.windowMs);
+
+ return NextResponse.json(
+ {
+ error: 'E_RATE_LIMIT',
+ message: 'Too many requests',
+ retryAfter: resetTime.toISOString(),
+ },
+ {
+ status: 429,
+ headers: {
+ 'X-RateLimit-Remaining': remaining.toString(),
+ 'X-RateLimit-Reset': resetTime.toISOString(),
+ 'Retry-After': Math.ceil(config.windowMs / 1000).toString(),
+ },
+ }
+ );
+ }
+
+ return NextResponse.next();
+ };
+}
+
+// Usage
+export const apiRateLimit = createRateLimitMiddleware({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ maxRequests: 100, // 100 requests per window
+ keyGenerator: (req) => {
+ // Rate limit by IP and user ID if available
+ const ip = req.ip || req.headers.get('x-forwarded-for') || 'unknown';
+ const userId = req.headers.get('x-user-id') || 'anonymous';
+ return `${ip}:${userId}`;
+ },
+});
+```
+
+### 🔐 API Key Security
+
+#### API Key Management
+
+```typescript
+// lib/api-keys.ts
+export interface APIKey {
+ id: string;
+ name: string;
+ key: string;
+ userId: string;
+ organizationId: string;
+ permissions: string[];
+ expiresAt?: Date;
+ lastUsed?: Date;
+ createdAt: Date;
+}
+
+export async function validateAPIKey(key: string): Promise {
+ const apiKey = await prisma.apiKey.findUnique({
+ where: { key },
+ include: { user: true },
+ });
+
+ if (!apiKey) return null;
+
+ // Check expiration
+ if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
+ return null;
+ }
+
+ // Update last used
+ await prisma.apiKey.update({
+ where: { id: apiKey.id },
+ data: { lastUsed: new Date() },
+ });
+
+ return apiKey;
+}
+
+export async function requireAPIKey(req: NextRequest): Promise {
+ const authHeader = req.headers.get('authorization');
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ throw new Error('API key required');
+ }
+
+ const key = authHeader.substring(7);
+ const apiKey = await validateAPIKey(key);
+
+ if (!apiKey) {
+ throw new Error('Invalid API key');
+ }
+
+ return apiKey;
+}
+```
+
+## Infrastructure Security
+
+### 🔒 Environment Security
+
+#### Environment Variables
+
+```typescript
+// lib/env.ts
+import { z } from 'zod';
+
+const envSchema = z.object({
+ NODE_ENV: z.enum(['development', 'production', 'test']),
+ DATABASE_URL: z.string().url(),
+ NEXTAUTH_SECRET: z.string().min(32),
+ NEXTAUTH_URL: z.string().url(),
+ OPENAI_API_KEY: z.string().min(1),
+ UPLOADTHING_SECRET: z.string().min(1),
+ UPLOADTHING_APP_ID: z.string().min(1),
+ ENCRYPTION_KEY: z.string().length(32),
+ REDIS_URL: z.string().url().optional(),
+ SENTRY_DSN: z.string().url().optional(),
+});
+
+export const env = envSchema.parse(process.env);
+
+// Validate required environment variables
+export function validateEnvironment(): void {
+ const missingVars = [];
+
+ for (const [key, value] of Object.entries(env)) {
+ if (!value) {
+ missingVars.push(key);
+ }
+ }
+
+ if (missingVars.length > 0) {
+ throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
+ }
+}
+```
+
+#### Secrets Management
+
+```typescript
+// lib/secrets.ts
+export class SecretsManager {
+ private static instance: SecretsManager;
+ private secrets: Map = new Map();
+
+ private constructor() {}
+
+ static getInstance(): SecretsManager {
+ if (!SecretsManager.instance) {
+ SecretsManager.instance = new SecretsManager();
+ }
+ return SecretsManager.instance;
+ }
+
+ setSecret(key: string, value: string): void {
+ this.secrets.set(key, value);
+ }
+
+ getSecret(key: string): string | undefined {
+ return this.secrets.get(key);
+ }
+
+ hasSecret(key: string): boolean {
+ return this.secrets.has(key);
+ }
+
+ rotateSecret(key: string): string {
+ const newValue = this.generateSecureSecret();
+ this.secrets.set(key, newValue);
+ return newValue;
+ }
+
+ private generateSecureSecret(): string {
+ return crypto.randomBytes(32).toString('hex');
+ }
+}
+```
+
+### 🚫 Security Headers
+
+#### Security Headers Middleware
+
+```typescript
+// middleware.ts
+export function securityHeaders(response: NextResponse): NextResponse {
+ // Security headers
+ response.headers.set('X-Content-Type-Options', 'nosniff');
+ response.headers.set('X-Frame-Options', 'DENY');
+ response.headers.set('X-XSS-Protection', '1; mode=block');
+ response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
+ response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
+
+ // Content Security Policy
+ const csp = [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
+ "style-src 'self' 'unsafe-inline'",
+ "img-src 'self' data: https:",
+ "font-src 'self'",
+ "connect-src 'self'",
+ "frame-src 'none'",
+ "object-src 'none'",
+ ].join('; ');
+
+ response.headers.set('Content-Security-Policy', csp);
+
+ // HSTS (HTTP Strict Transport Security)
+ if (process.env.NODE_ENV === 'production') {
+ response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
+ }
+
+ return response;
+}
+```
+
+## Security Best Practices
+
+### 🔒 Development Security
+
+#### Code Security Guidelines
+
+1. **Never log sensitive data**: Passwords, tokens, PII
+2. **Use parameterized queries**: Prevent SQL injection
+3. **Validate all inputs**: Client and server-side validation
+4. **Implement proper error handling**: Don't expose system details
+5. **Use HTTPS in production**: Encrypt all communications
+6. **Regular security updates**: Keep dependencies updated
+7. **Security code reviews**: Review all security-related code
+
+#### Security Testing
+
+```typescript
+// tests/security.test.ts
+describe('Security Tests', () => {
+ test('should not expose sensitive data in error messages', async () => {
+ const response = await request(app).get('/api/users/invalid-id').expect(404);
+
+ expect(response.body.error).not.toContain('password');
+ expect(response.body.error).not.toContain('token');
+ });
+
+ test('should validate input data', async () => {
+ const response = await request(app)
+ .post('/api/campaigns')
+ .send({ name: '' })
+ .expect(400);
+
+ expect(response.body.error).toBe('E_VALIDATION');
+ });
+
+ test('should enforce rate limiting', async () => {
+ const requests = Array(101)
+ .fill(null)
+ .map(() => request(app).get('/api/campaigns'));
+
+ const responses = await Promise.all(requests);
+ const rateLimited = responses.filter((r) => r.status === 429);
+
+ expect(rateLimited.length).toBeGreaterThan(0);
+ });
+});
+```
+
+### 📊 Security Monitoring
+
+#### Security Event Logging
+
+```typescript
+// lib/security-monitoring.ts
+export interface SecurityEvent {
+ type:
+ | 'authentication_failure'
+ | 'permission_denied'
+ | 'rate_limit_exceeded'
+ | 'suspicious_activity';
+ userId?: string;
+ ipAddress: string;
+ userAgent: string;
+ details: any;
+ timestamp: Date;
+}
+
+export class SecurityMonitor {
+ async logSecurityEvent(event: SecurityEvent): Promise {
+ // Log to security log
+ logger.warn('Security event detected', event);
+
+ // Store in database for analysis
+ await prisma.securityEvent.create({
+ data: {
+ type: event.type,
+ userId: event.userId,
+ ipAddress: event.ipAddress,
+ userAgent: event.userAgent,
+ details: event.details,
+ timestamp: event.timestamp,
+ },
+ });
+
+ // Check for suspicious patterns
+ await this.analyzeSecurityPatterns(event);
+ }
+
+ private async analyzeSecurityPatterns(event: SecurityEvent): Promise {
+ // Check for multiple failed login attempts
+ if (event.type === 'authentication_failure') {
+ const recentFailures = await prisma.securityEvent.count({
+ where: {
+ type: 'authentication_failure',
+ ipAddress: event.ipAddress,
+ timestamp: {
+ gte: new Date(Date.now() - 15 * 60 * 1000), // Last 15 minutes
+ },
+ },
+ });
+
+ if (recentFailures > 5) {
+ await this.triggerSecurityAlert('Multiple authentication failures', {
+ ipAddress: event.ipAddress,
+ failureCount: recentFailures,
+ });
+ }
+ }
+ }
+
+ private async triggerSecurityAlert(message: string, context: any): Promise {
+ // Send to security team
+ logger.error('SECURITY ALERT', { message, context });
+
+ // Could also send to Slack, email, etc.
+ }
+}
+
+export const securityMonitor = new SecurityMonitor();
+```
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Security Team_
diff --git a/docs/SPEC.md b/docs/SPEC.md
new file mode 100644
index 00000000..a836e904
--- /dev/null
+++ b/docs/SPEC.md
@@ -0,0 +1,232 @@
+# AiM Platform - Product Specification
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [User Roles](#user-roles)
+- [Scope & MVP](#scope--mvp)
+- [User Journeys](#user-journeys)
+- [Scheduling UX](#scheduling-ux)
+- [Acceptance Criteria](#acceptance-criteria)
+- [Non-Goals](#non-goals)
+
+## Overview
+
+**AiM (AI-powered Marketing Platform)** là hệ thống quản lý nội dung và chiến dịch marketing được hỗ trợ bởi AI, phục vụ 3 nhóm người dùng chính trong một tổ chức.
+
+### Core Value Proposition
+
+- **Creators**: Tạo nội dung chất lượng cao với AI assistance
+- **Brands**: Quản lý chiến dịch hiệu quả, duyệt nội dung tập trung
+- **Admins**: Kiểm soát tổ chức và hệ thống toàn diện
+
+## User Roles
+
+### 🎨 Creator
+
+**Mục tiêu**: Tạo nội dung chất lượng cao cho campaigns
+
+- **Permissions**: Xem campaigns, tạo/sửa content, upload assets
+- **Dashboard**: Content studio, draft management, performance tracking
+- **Key Actions**: Content creation, AI generation, asset management
+
+### 🏢 Brand Owner
+
+**Mục tiêu**: Quản lý chiến dịch và duyệt nội dung
+
+- **Permissions**: Tạo/sửa campaigns, approve content, view analytics
+- **Dashboard**: Campaign overview, approval queue, budget tracking
+- **Key Actions**: Campaign management, content approval, scheduling
+
+### ⚙️ Admin
+
+**Mục tiêu**: Quản trị tổ chức và hệ thống
+
+- **Permissions**: User management, org settings, system monitoring
+- **Dashboard**: User management, organization settings, system health
+- **Key Actions**: User CRUD, role assignment, system configuration
+
+## Scope & MVP
+
+### ✅ MVP Features (Phase 1)
+
+1. **Authentication & RBAC**
+
+ - NextAuth integration với role-based access
+ - Organization-based multi-tenancy
+ - Permission system cho tất cả actions
+
+2. **Campaign Management**
+
+ - CRUD campaigns (Brand Owner + Admin)
+ - Campaign status tracking (Draft → Active → Completed)
+ - Basic campaign metrics
+
+3. **Content Creation & Management**
+
+ - Rich text editor cho content
+ - AI-assisted content generation
+ - Content approval workflow
+ - Asset upload và management
+
+4. **Scheduling System**
+
+ - Schedule content cho campaigns
+ - Calendar view với platform support
+ - Basic publishing automation
+
+5. **Analytics Foundation**
+ - Event tracking (content views, AI usage)
+ - Basic metrics dashboard
+ - Campaign performance tracking
+
+### 🔄 Future Features (Phase 2+)
+
+- Advanced AI features (translation, summarization)
+- Social media integration
+- Advanced analytics và reporting
+- Team collaboration tools
+- Mobile applications
+
+## User Journeys
+
+### 1. Campaign Creation Flow
+
+```
+Brand Owner → Create Campaign → Set Goals/Budget → Assign Creators →
+Creators Create Content → Submit for Review → Brand Approval →
+Schedule → Publish → Track Performance
+```
+
+### 2. Content Creation Flow
+
+```
+Creator → Select Campaign → AI-Assisted Content Creation →
+Preview & Edit → Save Draft → Submit for Review →
+Brand Review → Approve/Reject → Schedule → Publish
+```
+
+### 3. AI Integration Flow
+
+```
+User → Input Prompt → AI Service → Generate Content →
+User Review → Edit/Refine → Save → Submit
+```
+
+### 4. Asset Management Flow
+
+```
+User → Upload File → Process & Optimize → Add Metadata →
+Link to Content → Version Control → Archive
+```
+
+### 5. Scheduling Workflow
+
+```
+User → Open Schedule View → Toggle Draft Panel →
+Browse Draft Content → Drag & Drop to Time Slot →
+Confirm Details (Channel, Time, Timezone) → Create Schedule →
+Content Status → SCHEDULED → Automated Publishing
+```
+
+## Scheduling UX
+
+### 📅 Three View Modes
+
+1. **Day View**: 24-hour timeline với 15-minute slots
+2. **Week View**: 7-day grid với hourly rows
+3. **Month View**: Calendar layout với daily cells
+
+### 📝 Draft Panel
+
+- **Toggle**: Right-side panel hiển thị content có `status=DRAFT`
+- **Features**: Search, campaign filtering, channel icons
+- **Drag Source**: Content items có thể kéo thả vào grid
+
+### 🎯 Drag & Drop Flow
+
+1. **Drag Start**: Click và kéo draft content từ panel
+2. **Drop Target**: Thả vào time slot trên grid
+3. **Confirmation Sheet**: Chọn channel, time, timezone
+4. **Schedule Creation**: API call tạo Schedule
+5. **Status Update**: Content status → `SCHEDULED`
+
+### 🔧 Smart Features
+
+- **Conflict Detection**: Cảnh báo overlap cùng channel
+- **Past Time Warning**: Không cho phép schedule quá khứ
+- **Timezone Support**: `runAt` lưu UTC, `timezone` cho UI
+- **Performance**: Window-based loading (from/to dates)
+
+### 🎨 Visual Indicators
+
+- **Channel Icons**: 📘 Facebook, 📷 Instagram, 🐦 Twitter, etc.
+- **Status Colors**: Pending (blue), Published (green), Failed (red)
+- **Current Time**: Highlight "now" slot với badge
+- **Drop Zones**: Hover effects và visual feedback
+
+## Acceptance Criteria
+
+### 🔐 Authentication & Access
+
+- [ ] User login/logout với NextAuth
+- [ ] Role-based access control (Creator, Brand Owner, Admin)
+- [ ] Organization isolation và multi-tenancy
+
+### 📊 Campaign Management
+
+- [ ] CRUD campaigns với validation
+- [ ] Campaign status workflow
+- [ ] User assignment và permissions
+
+### ✍️ Content Management
+
+- [ ] Rich text editor với AI assistance
+- [ ] Content approval workflow
+- [ ] Asset upload và management
+- [ ] Version control và history
+
+### 📅 Scheduling System
+
+- [ ] Three view modes (Day/Week/Month) hoạt động
+- [ ] Draft panel toggle và content filtering
+- [ ] Drag & drop từ draft vào time slot
+- [ ] Schedule confirmation sheet
+- [ ] API integration tạo schedule
+- [ ] Content status update → SCHEDULED
+
+### 📈 Analytics & Monitoring
+
+- [ ] Event tracking foundation
+- [ ] Basic metrics dashboard
+- [ ] Campaign performance data
+
+### 🚀 Performance & UX
+
+- [ ] Responsive design cho mobile/desktop
+- [ ] Loading states và error handling
+- [ ] Keyboard navigation support
+- [ ] Accessibility compliance (WCAG 2.1)
+
+## Non-Goals
+
+### ❌ Out of Scope (Phase 1)
+
+- Social media platform integration
+- Advanced AI features (translation, summarization)
+- Team collaboration tools
+- Mobile applications
+- Advanced analytics và reporting
+- Multi-language support
+- Advanced workflow automation
+- Third-party integrations (beyond basic APIs)
+
+### 🔮 Future Considerations
+
+- Real-time collaboration
+- Advanced AI content optimization
+- Social media publishing automation
+- Advanced analytics và insights
+- Mobile app development
+- API marketplace
+- White-label solutions
diff --git a/docs/adr/0001-aim-architecture.md b/docs/adr/0001-aim-architecture.md
new file mode 100644
index 00000000..9e0621cc
--- /dev/null
+++ b/docs/adr/0001-aim-architecture.md
@@ -0,0 +1,251 @@
+# ADR-001: AiM Platform Architecture
+
+## Status
+
+Accepted
+
+## Context
+
+AiM Platform cần một kiến trúc rõ ràng để hỗ trợ:
+
+- Multi-tenant organization-based access
+- Role-based access control (RBAC)
+- AI-powered content generation
+- Campaign management và content scheduling
+- Real-time analytics và event tracking
+- Scalable file storage và asset management
+
+## Decision
+
+Chúng ta sẽ sử dụng **Next.js 15 App Router** với **API Routes** pattern, **Prisma ORM** cho database layer, và **modular architecture** với clear boundaries giữa các layers.
+
+### Architecture Overview
+
+```mermaid
+flowchart TD
+ subgraph Client
+ U[Users: Creator/Brand/Admin] -->|Browser| FE[Next.js 15 App]
+ FE -->|Fetch| API
+ FE -->|Auth Hooks| AuthClient
+ FE -->|State Mgmt| Zustand
+ end
+
+ subgraph Server
+ API -->|Route Protection| Middleware
+ API -->|Data Access| Services
+ API -->|Validation| Schemas
+
+ subgraph Services
+ AuthService[Auth Service]
+ CampaignService[Campaign Service]
+ ContentService[Content Service]
+ AssetService[Asset Service]
+ ScheduleService[Schedule Service]
+ AnalyticsService[Analytics Service]
+ AIService[AI Service]
+ end
+
+ Services -->|ORM| Prisma
+ Services -->|External APIs| External
+ end
+
+ subgraph Data
+ Prisma -->|PostgreSQL| DB[(Database)]
+ Prisma -->|Redis Cache| Cache[(Cache)]
+ end
+
+ subgraph External
+ AI[OpenAI API]
+ Storage[S3/UploadThing]
+ Social[Social Media APIs]
+ end
+
+ subgraph Infrastructure
+ Auth[NextAuth.js]
+ RBAC[RBAC Middleware]
+ Validation[Zod Schemas]
+ Monitoring[Health Checks]
+ end
+```
+
+### Module Boundaries
+
+#### 1. Presentation Layer (UI)
+
+- **Components**: Reusable UI components theo shadcn/ui pattern
+- **Pages**: Route-based pages với role-specific layouts
+- **Hooks**: Custom React hooks cho business logic
+- **State**: Zustand stores cho client-side state
+
+#### 2. API Layer (Routes)
+
+- **Route Handlers**: Next.js API routes với consistent patterns
+- **Middleware**: Authentication, RBAC, validation
+- **Response Format**: Standardized API responses với error handling
+
+#### 3. Service Layer (Business Logic)
+
+- **Domain Services**: Campaign, Content, Asset, Schedule, Analytics
+- **Cross-cutting Services**: Auth, RBAC, AI integration
+- **Validation**: Input validation với Zod schemas
+- **Error Handling**: Consistent error handling patterns
+
+#### 4. Data Layer (Persistence)
+
+- **Prisma Client**: Type-safe database access
+- **Migrations**: Database schema management
+- **Seeding**: Test data và initial setup
+- **Caching**: Redis cho frequently accessed data
+
+#### 5. Infrastructure Layer
+
+- **Authentication**: NextAuth.js với custom providers
+- **Authorization**: RBAC middleware với permission system
+- **Monitoring**: Health checks, logging, error tracking
+- **Configuration**: Environment-based configuration
+
+### Key Design Principles
+
+1. **Separation of Concerns**: Clear boundaries giữa UI, API, services, và data
+2. **Type Safety**: Full TypeScript coverage với Prisma-generated types
+3. **Security First**: RBAC at every layer, input validation, secure defaults
+4. **Performance**: Lazy loading, caching, pagination cho large datasets
+5. **Scalability**: Modular design cho easy extension và maintenance
+
+## Consequences
+
+### Positive
+
+- **Clear Architecture**: Developers có thể easily understand system structure
+- **Type Safety**: Prisma + TypeScript provide excellent developer experience
+- **Modularity**: Easy to add new features và modify existing ones
+- **Security**: RBAC implemented at multiple layers
+- **Performance**: Optimized data access và caching strategies
+
+### Negative
+
+- **Complexity**: Multi-layer architecture có thể complex cho simple features
+- **Learning Curve**: Team cần understand Prisma, Next.js patterns
+- **Overhead**: Additional abstraction layers có thể slow development initially
+
+### Risks & Mitigations
+
+- **Risk**: Over-engineering simple features
+ - **Mitigation**: Start simple, add complexity only when needed
+- **Risk**: Performance overhead từ multiple layers
+ - **Mitigation**: Profile và optimize critical paths
+- **Risk**: Database complexity với Prisma
+ - **Mitigation**: Comprehensive testing và migration strategy
+
+## Alternatives Considered
+
+### 1. Monolithic API
+
+- **Pros**: Simpler architecture, easier to understand
+- **Cons**: Harder to scale, difficult to maintain, tight coupling
+
+### 2. Microservices
+
+- **Pros**: Independent scaling, technology diversity
+- **Cons**: Overkill cho MVP, complex deployment, network overhead
+
+### 3. Serverless Functions
+
+- **Pros**: Auto-scaling, pay-per-use
+- **Cons**: Cold starts, vendor lock-in, complex debugging
+
+### 4. Traditional REST API
+
+- **Pros**: Familiar pattern, extensive tooling
+- **Cons**: Less type safety, manual validation, boilerplate code
+
+## Implementation Notes
+
+### Phase 1: Foundation
+
+- Setup Next.js 15 với App Router
+- Implement Prisma schema và migrations
+- Setup NextAuth.js với RBAC
+- Create basic service layer structure
+
+### Phase 2: Core Features
+
+- Implement Campaign, Content, Asset services
+- Add scheduling system
+- Setup analytics foundation
+- Implement AI integration
+
+### Phase 3: Enhancement
+
+- Add advanced features (recurring schedules, AI quality metrics)
+- Implement caching và performance optimization
+- Add comprehensive monitoring và logging
+- Setup CI/CD pipelines
+
+### Technology Stack
+
+- **Frontend**: Next.js 15, React 19, TypeScript 5
+- **UI**: shadcn/ui, Tailwind CSS 4, Radix UI
+- **Backend**: Next.js API Routes, Prisma 6
+- **Database**: PostgreSQL
+- **Authentication**: NextAuth.js 5
+- **State Management**: Zustand, React Query
+- **AI**: OpenAI API
+- **Storage**: UploadThing/S3
+- **Testing**: Jest, Playwright
+
+### Code Organization
+
+```
+app/
+├── (auth)/ # Authentication pages
+├── (dashboard)/ # Role-based dashboards
+├── api/ # API endpoints
+│ └── [orgId]/ # Organization-scoped APIs
+├── campaigns/ # Campaign pages
+├── content/ # Content editor
+├── calendar/ # Scheduling view
+├── assets/ # Asset library
+├── analytics/ # Analytics dashboards
+└── settings/ # User/org settings
+
+components/
+├── ui/ # shadcn/ui components
+├── forms/ # Form components
+├── dashboards/ # Dashboard widgets
+├── campaigns/ # Campaign components
+├── content/ # Content editor
+├── assets/ # Asset management
+├── analytics/ # Charts & metrics
+└── layout/ # Navigation & layout
+
+lib/
+├── prisma.ts # Database client
+├── auth.ts # Auth utilities
+├── rbac.ts # Role-based access control
+├── services/ # Business logic services
+│ ├── campaigns.ts
+│ ├── content.ts
+│ ├── assets.ts
+│ ├── schedules.ts
+│ ├── analytics.ts
+│ └── ai.ts
+├── schemas.ts # Zod validation schemas
+└── utils.ts # Common utilities
+```
+
+## References
+
+- [Next.js 15 Documentation](https://nextjs.org/docs)
+- [Prisma Documentation](https://www.prisma.io/docs)
+- [NextAuth.js Documentation](https://next-auth.js.org/)
+- [shadcn/ui Documentation](https://ui.shadcn.com/)
+- [AiM Platform Specification](./../SPEC.md)
+- [Data Model Documentation](./../data-model.md)
+
+---
+
+_Created: 2025-01-02_
+_Last Updated: 2025-01-02_
+_Author: AI-CTO Team_
+_Reviewers: Engineering Team_
diff --git a/docs/adr/0002-schedule-system-architecture.md b/docs/adr/0002-schedule-system-architecture.md
new file mode 100644
index 00000000..7e72657d
--- /dev/null
+++ b/docs/adr/0002-schedule-system-architecture.md
@@ -0,0 +1,106 @@
+# ADR-0002: Schedule System Architecture
+
+## Status
+
+Proposed
+
+## Context
+
+AiM Platform cần một hệ thống scheduling và calendar để quản lý việc publish content theo lịch trình. Hệ thống này phải hỗ trợ timezone, recurring schedules, và integration với content approval workflow.
+
+## Decision
+
+Implement comprehensive scheduling system với:
+
+- **Calendar View**: react-day-picker với month/week/day views
+- **Schedule Management**: Form-based creation với validation
+- **Recurring Schedules**: RRULE model cho complex patterns
+- **Timezone Support**: UTC storage, user preference display
+- **Integration**: Content approval workflow, notifications
+
+## Consequences
+
+### Positive
+
+- **User Experience**: Intuitive calendar interface
+- **Flexibility**: Support multiple schedule types
+- **Scalability**: Efficient database queries với indexes
+- **Internationalization**: Timezone handling cho global users
+
+### Negative
+
+- **Complexity**: RRULE parsing và timezone calculations
+- **Performance**: Calendar rendering với large datasets
+- **Storage**: Additional database tables và relationships
+- **Testing**: Complex timezone và date logic testing
+
+## Implementation Details
+
+### Database Schema
+
+```prisma
+model Schedule {
+ id String @id @default(cuid())
+ title String
+ description String?
+ platform String // Facebook, Instagram, etc.
+ scheduledAt DateTime // UTC timestamp
+ timezone String // User's timezone preference
+ rrule String? // RRULE for recurring schedules
+ status ScheduleStatus
+ contentId String?
+ content Content? @relation(fields: [contentId], references: [id])
+ campaignId String?
+ campaign Campaign? @relation(fields: [campaignId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+enum ScheduleStatus {
+ DRAFT
+ SCHEDULED
+ PUBLISHED
+ FAILED
+ CANCELLED
+}
+```
+
+### Key Components
+
+- **Calendar View**: `components/schedules/calendar-view.tsx`
+- **Schedule Form**: `components/schedules/schedule-form.tsx`
+- **Schedule Service**: `lib/schedule-service.ts`
+- **RRULE Parser**: `lib/rrule-parser.ts`
+- **Timezone Utils**: `lib/timezone-utils.ts`
+
+### Dependencies
+
+- `react-day-picker`: Calendar component
+- `date-fns`: Date manipulation utilities
+- `rrule`: RRULE parsing và generation
+
+## Migration Strategy
+
+1. **Phase 1**: Database schema changes
+2. **Phase 2**: Core scheduling functionality
+3. **Phase 3**: Calendar UI implementation
+4. **Phase 4**: Integration với content workflow
+
+## Rollback Plan
+
+- Database migration có thể revert với `pnpm prisma migrate reset`
+- Code changes có thể rollback với git
+- Dependencies có thể remove và reinstall
+
+## Success Metrics
+
+- Calendar render time < 500ms
+- Schedule creation < 2 seconds
+- Timezone handling accuracy 100%
+- Recurring schedule reliability 99.9%
+
+---
+
+_Created: 2025-01-02_
+_Author: Engineering Team_
+_Reviewers: Architecture Team_
diff --git a/docs/api/analytics.md b/docs/api/analytics.md
new file mode 100644
index 00000000..81e25a30
--- /dev/null
+++ b/docs/api/analytics.md
@@ -0,0 +1,611 @@
+# Analytics API
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Endpoints](#endpoints)
+- [Authentication](#authentication)
+- [Permissions](#permissions)
+- [Event Tracking](#event-tracking)
+- [Metrics & Aggregation](#metrics--aggregation)
+- [Request/Response Formats](#requestresponse-formats)
+- [Error Handling](#error-handling)
+- [Examples](#examples)
+
+## Overview
+
+Analytics API cho phép thu thập và truy vấn dữ liệu analytics cho campaigns, content, và user behavior. Hệ thống track events real-time và cung cấp aggregated metrics.
+
+### Base URL
+
+```
+/api/[orgId]/analytics
+```
+
+### Supported Operations
+
+- `POST /events` - Record analytics events
+- `GET /events` - Query events với filters
+- `GET /metrics` - Get aggregated metrics
+- `GET /summary` - Get summary dashboard data
+- `GET /export` - Export analytics data (CSV/JSON)
+
+### Analytics Categories
+
+- **Content Performance**: Views, clicks, engagement
+- **Campaign Metrics**: Reach, conversion, ROI
+- **User Behavior**: Login, actions, preferences
+- **AI Usage**: Generation requests, costs, quality
+- **System Health**: API performance, errors
+
+## Endpoints
+
+### 📊 Record Event
+
+```http
+POST /api/[orgId]/analytics/events
+```
+
+**Description**: Record analytics event
+
+**Request Body**: Event data với validation
+
+**Response**: Created event object
+
+**Permissions**: `VIEW_ANALYTICS` (Creator, Brand Owner, Admin)
+
+### 📋 Query Events
+
+```http
+GET /api/[orgId]/analytics/events
+```
+
+**Description**: Query analytics events với filters
+
+**Query Parameters**:
+
+- `eventType` (optional): Filter by event type
+- `contentId` (optional): Filter by content
+- `campaignId` (optional): Filter by campaign
+- `userId` (optional): Filter by user
+- `dateFrom` (optional): Filter from date (ISO string)
+- `dateTo` (optional): Filter to date (ISO string)
+- `page` (optional): Page number, default: 1
+- `limit` (optional): Items per page, default: 100
+
+**Response**: Array of events với pagination
+
+**Permissions**: `VIEW_ANALYTICS` (Creator, Brand Owner, Admin)
+
+### 📈 Get Metrics
+
+```http
+GET /api/[orgId]/analytics/metrics
+```
+
+**Description**: Get aggregated metrics cho organization
+
+**Query Parameters**:
+
+- `type` (required): Metric type (campaign, content, user, ai)
+- `campaignId` (optional): Filter by campaign
+- `contentId` (optional): Filter by content
+- `period` (optional): Time period (day, week, month, year)
+- `dateFrom` (optional): Custom date range start
+- `dateTo` (optional): Custom date range end
+
+**Response**: Aggregated metrics data
+
+**Permissions**: `VIEW_ANALYTICS` (Creator, Brand Owner, Admin)
+
+### 📊 Get Summary
+
+```http
+GET /api/[orgId]/analytics/summary
+```
+
+**Description**: Get summary dashboard data
+
+**Query Parameters**:
+
+- `period` (optional): Time period (7d, 30d, 90d)
+
+**Response**: Summary metrics cho dashboard
+
+**Permissions**: `VIEW_ANALYTICS` (Creator, Brand Owner, Admin)
+
+### 📥 Export Data
+
+```http
+GET /api/[orgId]/analytics/export
+```
+
+**Description**: Export analytics data
+
+**Query Parameters**:
+
+- `format` (optional): Export format (csv, json), default: csv
+- `eventType` (optional): Filter by event type
+- `dateFrom` (optional): Filter from date
+- `dateTo` (optional): Filter to date
+
+**Response**: File download (CSV/JSON)
+
+**Permissions**: `VIEW_ANALYTICS` (Brand Owner, Admin)
+
+## Authentication
+
+Tất cả endpoints yêu cầu authentication thông qua NextAuth session.
+
+```typescript
+const session = await auth();
+if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+}
+```
+
+## Permissions
+
+### Role-based Access Control
+
+```typescript
+const PERMISSIONS = {
+ VIEW_ANALYTICS: ['creator', 'brand_owner', 'admin'],
+};
+```
+
+### Permission Matrix
+
+| Action | Creator | Brand Owner | Admin |
+| --------------------------- | ------- | ----------- | ----- |
+| View Own Content Analytics | ✅ | ✅ | ✅ |
+| View Campaign Analytics | ✅ | ✅ | ✅ |
+| View Organization Analytics | ❌ | ✅ | ✅ |
+| Export Analytics Data | ❌ | ✅ | ✅ |
+
+## Event Tracking
+
+### 🎯 Event Types
+
+#### Content Events
+
+```typescript
+// Content performance tracking
+'content.view'; // Content được xem
+'content.click'; // Link được click
+'content.like'; // Content được like
+'content.share'; // Content được share
+'content.comment'; // Comment được tạo
+'content.bookmark'; // Content được bookmark
+```
+
+#### Campaign Events
+
+```typescript
+// Campaign engagement
+'campaign.view'; // Campaign page được xem
+'campaign.click'; // Campaign CTA được click
+'campaign.convert'; // Campaign conversion
+'campaign.impression'; // Campaign impression
+```
+
+#### User Events
+
+```typescript
+// User behavior tracking
+'user.login'; // User login
+'user.logout'; // User logout
+'user.action'; // Generic user action
+'user.preference'; // User preference change
+```
+
+#### AI Events
+
+```typescript
+// AI usage tracking
+'ai.generate'; // AI content generation
+'ai.translate'; // AI translation
+'ai.summarize'; // AI summarization
+'ai.quality_score'; // AI quality feedback
+```
+
+#### System Events
+
+```typescript
+// System health monitoring
+'system.error'; // System error occurred
+'system.performance'; // Performance metric
+'system.health'; // Health check result
+```
+
+### 📊 Event Schema
+
+```typescript
+interface AnalyticsEvent {
+ id: string;
+ event: string;
+ data?: Json;
+ userId?: string;
+ organizationId?: string;
+ campaignId?: string;
+ contentId?: string;
+ platform?: string;
+ userAgent?: string;
+ ipAddress?: string;
+ timestamp: DateTime;
+ createdAt: DateTime;
+
+ // Relations
+ user?: User;
+ organization?: Organization;
+ campaign?: Campaign;
+ content?: Content;
+}
+```
+
+### 🔍 Event Data Examples
+
+#### Content View Event
+
+```json
+{
+ "event": "content.view",
+ "contentId": "cont_xyz789",
+ "campaignId": "camp_abc123",
+ "data": {
+ "viewDuration": 45,
+ "scrollDepth": 0.8,
+ "referrer": "facebook.com",
+ "device": "mobile"
+ },
+ "platform": "web",
+ "userAgent": "Mozilla/5.0...",
+ "timestamp": "2025-01-02T10:00:00Z"
+}
+```
+
+#### AI Generation Event
+
+```json
+{
+ "event": "ai.generate",
+ "contentId": "cont_xyz789",
+ "campaignId": "camp_abc123",
+ "data": {
+ "model": "gpt-4",
+ "tokensUsed": 150,
+ "cost": 0.003,
+ "prompt": "Create a social media post about summer sale",
+ "quality_score": 8.5,
+ "generation_time": 2.3
+ },
+ "timestamp": "2025-01-02T10:00:00Z"
+}
+```
+
+## Metrics & Aggregation
+
+### 📈 Metric Types
+
+#### Content Metrics
+
+```typescript
+interface ContentMetrics {
+ totalViews: number;
+ uniqueViews: number;
+ averageViewDuration: number;
+ clickThroughRate: number;
+ engagementRate: number;
+ topPerformingContent: Array<{
+ contentId: string;
+ title: string;
+ views: number;
+ engagement: number;
+ }>;
+}
+```
+
+#### Campaign Metrics
+
+```typescript
+interface CampaignMetrics {
+ totalReach: number;
+ totalEngagement: number;
+ conversionRate: number;
+ costPerClick: number;
+ returnOnInvestment: number;
+ platformBreakdown: {
+ facebook: number;
+ instagram: number;
+ linkedin: number;
+ twitter: number;
+ };
+}
+```
+
+#### AI Usage Metrics
+
+```typescript
+interface AIUsageMetrics {
+ totalGenerations: number;
+ totalTokens: number;
+ totalCost: number;
+ averageQualityScore: number;
+ popularPrompts: Array<{
+ prompt: string;
+ usageCount: number;
+ averageQuality: number;
+ }>;
+}
+```
+
+### 🔢 Aggregation Periods
+
+- **Real-time**: Current session data
+- **Hourly**: Last 24 hours by hour
+- **Daily**: Last 30 days by day
+- **Weekly**: Last 12 weeks by week
+- **Monthly**: Last 12 months by month
+- **Custom**: User-defined date range
+
+## Request/Response Formats
+
+### Record Event Request
+
+```typescript
+interface RecordEventRequest {
+ event: string;
+ data?: Json;
+ campaignId?: string;
+ contentId?: string;
+ platform?: string;
+ userAgent?: string;
+ timestamp?: string; // ISO string, defaults to now
+}
+```
+
+### Query Events Response
+
+```typescript
+interface EventsResponse {
+ events: AnalyticsEvent[];
+ pagination: {
+ page: number;
+ limit: number;
+ total: number;
+ totalPages: number;
+ };
+ summary: {
+ totalEvents: number;
+ uniqueUsers: number;
+ dateRange: {
+ from: string;
+ to: string;
+ };
+ };
+}
+```
+
+### Metrics Response
+
+```typescript
+interface MetricsResponse {
+ type: string;
+ period: string;
+ dateRange: {
+ from: string;
+ to: string;
+ };
+ metrics: Json;
+ trends: {
+ previous: Json;
+ change: number;
+ changePercent: number;
+ };
+}
+```
+
+## Error Handling
+
+### HTTP Status Codes
+
+- `200` - Success
+- `201` - Created (POST)
+- `400` - Bad Request (validation errors)
+- `401` - Unauthorized (no session)
+- `403` - Forbidden (insufficient permissions)
+- `404` - Not Found
+- `422` - Unprocessable Entity (invalid event data)
+- `500` - Internal Server Error
+
+### Error Response Format
+
+```json
+{
+ "error": "E_INVALID_EVENT_TYPE",
+ "message": "Invalid event type: unknown_event",
+ "details": {
+ "eventType": "unknown_event",
+ "allowedTypes": ["content.view", "content.click", "ai.generate"]
+ }
+}
+```
+
+### Common Error Scenarios
+
+- **E_INVALID_EVENT_TYPE**: Event type không được support
+- **E_MISSING_REQUIRED_FIELDS**: Thiếu required fields
+- **E_INVALID_DATE_RANGE**: Date range không hợp lệ
+- **E_EXPORT_FAILED**: Export operation failed
+- **E_RATE_LIMIT**: Too many events trong short time
+
+## Examples
+
+### Record Content View Event
+
+```bash
+curl -X POST /api/org123/analytics/events \
+ -H "Content-Type: application/json" \
+ -d '{
+ "event": "content.view",
+ "contentId": "cont_xyz789",
+ "campaignId": "camp_abc123",
+ "data": {
+ "viewDuration": 45,
+ "scrollDepth": 0.8,
+ "referrer": "facebook.com"
+ },
+ "platform": "web"
+ }'
+```
+
+**Response**:
+
+```json
+{
+ "id": "event_abc123",
+ "event": "content.view",
+ "contentId": "cont_xyz789",
+ "campaignId": "camp_abc123",
+ "data": {
+ "viewDuration": 45,
+ "scrollDepth": 0.8,
+ "referrer": "facebook.com"
+ },
+ "platform": "web",
+ "timestamp": "2025-01-02T10:00:00Z",
+ "createdAt": "2025-01-02T10:00:00Z"
+}
+```
+
+### Get Campaign Metrics
+
+```bash
+curl "/api/org123/analytics/metrics?type=campaign&campaignId=camp_abc123&period=30d"
+```
+
+**Response**:
+
+```json
+{
+ "type": "campaign",
+ "period": "30d",
+ "dateRange": {
+ "from": "2024-12-03T00:00:00Z",
+ "to": "2025-01-02T23:59:59Z"
+ },
+ "metrics": {
+ "totalReach": 15420,
+ "totalEngagement": 1234,
+ "conversionRate": 0.08,
+ "costPerClick": 0.45,
+ "returnOnInvestment": 2.2
+ },
+ "trends": {
+ "previous": {
+ "totalReach": 12850,
+ "totalEngagement": 987
+ },
+ "change": 2570,
+ "changePercent": 20.0
+ }
+}
+```
+
+### Query Events
+
+```bash
+curl "/api/org123/analytics/events?eventType=content.view&contentId=cont_xyz789&dateFrom=2025-01-01&limit=20"
+```
+
+**Response**:
+
+```json
+{
+ "events": [
+ {
+ "id": "event_abc123",
+ "event": "content.view",
+ "contentId": "cont_xyz789",
+ "campaignId": "camp_abc123",
+ "data": {
+ "viewDuration": 45,
+ "scrollDepth": 0.8
+ },
+ "timestamp": "2025-01-02T10:00:00Z"
+ }
+ ],
+ "pagination": {
+ "page": 1,
+ "limit": 20,
+ "total": 1,
+ "totalPages": 1
+ },
+ "summary": {
+ "totalEvents": 1,
+ "uniqueUsers": 1,
+ "dateRange": {
+ "from": "2025-01-01T00:00:00Z",
+ "to": "2025-01-02T23:59:59Z"
+ }
+ }
+}
+```
+
+## Best Practices
+
+### 🔒 Security
+
+- Sanitize user input trong event data
+- Implement rate limiting cho event recording
+- Log suspicious event patterns
+- Validate event types và data schemas
+
+### 📊 Performance
+
+- Use batch processing cho high-volume events
+- Implement event buffering cho real-time tracking
+- Cache frequently accessed metrics
+- Use database indexing cho common queries
+
+### 🧪 Testing
+
+- Test event recording với different data types
+- Verify metrics aggregation accuracy
+- Test export functionality với large datasets
+- Monitor event processing performance
+
+## Gotchas
+
+### ⚠️ Common Issues
+
+1. **Event Volume**: High event volume có thể affect performance
+2. **Data Consistency**: Event data có thể change over time
+3. **Timezone Handling**: Always store timestamps in UTC
+4. **Storage Costs**: Analytics data có thể grow quickly
+
+### 💡 Tips
+
+- Implement event sampling cho high-volume scenarios
+- Use materialized views cho complex aggregations
+- Monitor analytics processing performance
+- Implement data retention policies
+
+### 🔧 Configuration
+
+```typescript
+// Analytics configuration
+const analyticsConfig = {
+ maxEventsPerMinute: 1000,
+ batchSize: 100,
+ retentionDays: 365,
+ enableRealTime: true,
+ samplingRate: 1.0, // 100% of events
+};
+```
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Engineering Team_
diff --git a/docs/api/assets.md b/docs/api/assets.md
new file mode 100644
index 00000000..09013687
--- /dev/null
+++ b/docs/api/assets.md
@@ -0,0 +1,424 @@
+# Assets API
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Endpoints](#endpoints)
+- [Authentication](#authentication)
+- [Permissions](#permissions)
+- [File Management](#file-management)
+- [Security](#security)
+- [Request/Response Formats](#requestresponse-formats)
+- [Error Handling](#error-handling)
+- [Examples](#examples)
+
+## Overview
+
+Assets API cho phép quản lý file uploads (images, videos, documents) và liên kết chúng với content. Hệ thống sử dụng UploadThing/S3 cho file storage với metadata tracking.
+
+### Base URL
+
+```
+/api/[orgId]/assets
+```
+
+### Supported Operations
+
+- `GET` - List assets
+- `POST` - Upload new asset
+- `GET /[id]` - Get asset details
+- `DELETE /[id]` - Delete asset
+- `POST /upload` - File upload endpoint
+
+### File Types Supported
+
+- **Images**: JPG, PNG, GIF, WebP, SVG
+- **Videos**: MP4, MOV, AVI, WebM
+- **Documents**: PDF, DOC, DOCX, TXT
+- **Other**: ZIP, RAR (with size limits)
+
+## Endpoints
+
+### 📋 List Assets
+
+```http
+GET /api/[orgId]/assets
+```
+
+**Description**: Lấy danh sách assets của organization
+
+**Query Parameters**:
+
+- `contentId` (optional): Filter by linked content
+- `type` (optional): Filter by file type
+- `tags` (optional): Filter by tags (comma-separated)
+- `page` (optional): Page number, default: 1
+- `limit` (optional): Items per page, default: 20
+- `search` (optional): Search by name/description
+
+**Response**: Array of assets với metadata
+
+**Permissions**: `MANAGE_CONTENT` (Creator, Brand Owner, Admin)
+
+### ➕ Upload Asset
+
+```http
+POST /api/[orgId]/assets/upload
+```
+
+**Description**: Upload file mới và tạo asset record
+
+**Request Body**: Multipart form data với file và metadata
+
+**Response**: Created asset object với upload URL
+
+**Permissions**: `MANAGE_CONTENT` (Creator, Brand Owner, Admin)
+
+### 👁️ Get Asset Details
+
+```http
+GET /api/[orgId]/assets/[id]
+```
+
+**Description**: Lấy thông tin chi tiết asset
+
+**Response**: Asset object với usage information
+
+**Permissions**: `MANAGE_CONTENT` (Creator, Brand Owner, Admin)
+
+### 🗑️ Delete Asset
+
+```http
+DELETE /api/[orgId]/assets/[id]
+```
+
+**Description**: Xóa asset (chỉ khi không được sử dụng)
+
+**Response**: Success confirmation
+
+**Permissions**: `MANAGE_CONTENT` (Creator, Brand Owner, Admin)
+
+### 🔗 Link Asset to Content
+
+```http
+POST /api/[orgId]/assets/[id]/link
+```
+
+**Description**: Link asset với content item
+
+**Request Body**: Content ID để link
+
+**Response**: Updated asset với content link
+
+**Permissions**: `MANAGE_CONTENT` (Creator, Brand Owner, Admin)
+
+## Authentication
+
+Tất cả endpoints yêu cầu authentication thông qua NextAuth session.
+
+```typescript
+const session = await auth();
+if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+}
+```
+
+## Permissions
+
+### Role-based Access Control
+
+```typescript
+const PERMISSIONS = {
+ MANAGE_CONTENT: ['creator', 'brand_owner', 'admin'],
+};
+```
+
+### Permission Matrix
+
+| Action | Creator | Brand Owner | Admin |
+| ------------- | ------- | ----------- | ----- |
+| Upload Assets | ✅ | ✅ | ✅ |
+| View Assets | ✅ | ✅ | ✅ |
+| Delete Assets | ✅ | ✅ | ✅ |
+| Link Assets | ✅ | ✅ | ✅ |
+
+## File Management
+
+### 📁 Upload Process
+
+1. **File Validation**: Check type, size, and security
+2. **Processing**: Generate thumbnails, extract metadata
+3. **Storage**: Upload to UploadThing/S3
+4. **Database**: Create asset record với metadata
+5. **Response**: Return asset object với access URLs
+
+### 🔍 File Processing
+
+- **Images**: Auto-generate thumbnails, optimize formats
+- **Videos**: Extract duration, generate preview frames
+- **Documents**: Extract text content, generate previews
+- **Metadata**: File size, dimensions, creation date
+
+### 🏷️ Tagging System
+
+```typescript
+interface Asset {
+ tags: string[]; // Searchable tags
+ metadata: Json; // Extended metadata
+}
+```
+
+**Common Tags**: campaign name, content type, platform, season
+
+## Security
+
+### 🛡️ File Validation
+
+```typescript
+// Allowed file types
+const ALLOWED_TYPES = {
+ images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
+ videos: ['video/mp4', 'video/mov', 'video/avi'],
+ documents: ['application/pdf', 'text/plain'],
+};
+
+// File size limits
+const SIZE_LIMITS = {
+ images: 10 * 1024 * 1024, // 10MB
+ videos: 100 * 1024 * 1024, // 100MB
+ documents: 25 * 1024 * 1024, // 25MB
+};
+```
+
+### 🔒 Access Control
+
+- **Organization Isolation**: Assets chỉ accessible bởi org members
+- **Content Linking**: Assets linked với content có additional access control
+- **Public URLs**: Signed URLs với expiration cho secure access
+
+### 🚫 Security Measures
+
+- **Virus Scanning**: Scan uploaded files (planned)
+- **File Type Validation**: Strict MIME type checking
+- **Size Limits**: Prevent abuse và storage costs
+- **Rate Limiting**: Limit upload frequency per user
+
+## Request/Response Formats
+
+### Asset Schema
+
+```typescript
+interface Asset {
+ id: string;
+ url: string;
+ name?: string;
+ type: string;
+ size: number;
+ description?: string;
+ tags: string[];
+ contentId: string;
+ uploadedById: string;
+ createdAt: DateTime;
+ updatedAt: DateTime;
+
+ // Relations
+ content?: Content;
+ uploadedBy?: User;
+
+ // Generated fields
+ thumbnail?: string;
+ metadata?: Json;
+ dimensions?: {
+ width?: number;
+ height?: number;
+ duration?: number;
+ };
+}
+```
+
+### Upload Request
+
+```typescript
+interface UploadRequest {
+ file: File;
+ name?: string;
+ description?: string;
+ tags?: string[];
+ contentId?: string;
+ metadata?: Json;
+}
+```
+
+### Asset Response
+
+```typescript
+interface AssetResponse {
+ id: string;
+ name: string;
+ url: string;
+ thumbnail?: string;
+ type: string;
+ size: number;
+ tags: string[];
+ createdAt: DateTime;
+ usage: {
+ linkedContent: number;
+ totalViews: number;
+ };
+}
+```
+
+## Error Handling
+
+### HTTP Status Codes
+
+- `200` - Success
+- `201` - Created (POST)
+- `400` - Bad Request (validation errors)
+- `401` - Unauthorized (no session)
+- `403` - Forbidden (insufficient permissions)
+- `404` - Not Found
+- `413` - Payload Too Large
+- `415` - Unsupported Media Type
+- `500` - Internal Server Error
+
+### Error Response Format
+
+```json
+{
+ "error": "E_FILE_TOO_LARGE",
+ "message": "File size exceeds maximum limit of 10MB",
+ "details": {
+ "fileSize": 15728640,
+ "maxSize": 10485760,
+ "fileType": "image/jpeg"
+ }
+}
+```
+
+### Common Error Scenarios
+
+- **E_FILE_TOO_LARGE**: File exceeds size limit
+- **E_INVALID_TYPE**: Unsupported file type
+- **E_UPLOAD_FAILED**: File upload failed
+- **E_ASSET_IN_USE**: Cannot delete asset that is linked to content
+- **E_STORAGE_QUOTA**: Organization storage quota exceeded
+
+## Examples
+
+### Upload Asset
+
+```bash
+curl -X POST /api/org123/assets/upload \
+ -H "Authorization: Bearer " \
+ -F "file=@summer-sale-banner.jpg" \
+ -F "name=Summer Sale Banner" \
+ -F "description=Main banner for summer campaign" \
+ -F "tags=summer,sale,banner" \
+ -F "contentId=cont_xyz789"
+```
+
+**Response**:
+
+```json
+{
+ "id": "asset_abc123",
+ "name": "Summer Sale Banner",
+ "url": "https://uploadthing.com/f/abc123",
+ "thumbnail": "https://uploadthing.com/f/abc123-thumb",
+ "type": "image/jpeg",
+ "size": 2048576,
+ "tags": ["summer", "sale", "banner"],
+ "contentId": "cont_xyz789",
+ "createdAt": "2025-01-02T10:00:00Z",
+ "usage": {
+ "linkedContent": 1,
+ "totalViews": 0
+ }
+}
+```
+
+### List Assets
+
+```bash
+curl "/api/org123/assets?type=image&tags=summer&page=1&limit=10"
+```
+
+**Response**:
+
+```json
+[
+ {
+ "id": "asset_abc123",
+ "name": "Summer Sale Banner",
+ "url": "https://uploadthing.com/f/abc123",
+ "thumbnail": "https://uploadthing.com/f/abc123-thumb",
+ "type": "image/jpeg",
+ "size": 2048576,
+ "tags": ["summer", "sale", "banner"],
+ "contentId": "cont_xyz789",
+ "createdAt": "2025-01-02T10:00:00Z",
+ "usage": {
+ "linkedContent": 1,
+ "totalViews": 15
+ }
+ }
+]
+```
+
+## Best Practices
+
+### 🔒 Security
+
+- Validate file types với MIME type checking
+- Implement file size limits per organization
+- Use signed URLs với expiration cho secure access
+- Log tất cả asset operations cho audit
+
+### 📊 Performance
+
+- Generate thumbnails cho images và videos
+- Use CDN cho frequently accessed assets
+- Implement lazy loading cho asset lists
+- Cache asset metadata và usage statistics
+
+### 🧪 Testing
+
+- Test với different file types và sizes
+- Verify file validation và error handling
+- Test asset linking và unlinking
+- Verify storage quota enforcement
+
+## Gotchas
+
+### ⚠️ Common Issues
+
+1. **File Type Mismatch**: MIME type vs file extension validation
+2. **Storage Costs**: Large files increase storage costs
+3. **Asset Dependencies**: Deleting assets affects linked content
+4. **Thumbnail Generation**: Some file types may fail thumbnail creation
+
+### 💡 Tips
+
+- Implement progressive upload cho large files
+- Use WebP format cho images để giảm size
+- Cache asset URLs để giảm API calls
+- Monitor storage usage per organization
+
+### 🔧 Configuration
+
+```typescript
+// UploadThing configuration
+const uploadConfig = {
+ maxFileSize: '10MB',
+ allowedFileTypes: ['image', 'video', 'document'],
+ generateThumbnails: true,
+ optimizeImages: true,
+};
+```
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Engineering Team_
diff --git a/docs/api/campaigns.md b/docs/api/campaigns.md
new file mode 100644
index 00000000..2a5ea190
--- /dev/null
+++ b/docs/api/campaigns.md
@@ -0,0 +1,308 @@
+# Campaigns API
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Endpoints](#endpoints)
+- [Authentication](#authentication)
+- [Permissions](#permissions)
+- [Request/Response Formats](#requestresponse-formats)
+- [Error Handling](#error-handling)
+- [Examples](#examples)
+
+## Overview
+
+Campaigns API cho phép quản lý marketing campaigns trong một organization. Mỗi campaign có thể chứa nhiều content items và schedules.
+
+### Base URL
+
+```
+/api/[orgId]/campaigns
+```
+
+### Supported Operations
+
+- `GET` - List campaigns
+- `POST` - Create new campaign
+- `GET /[id]` - Get campaign details
+- `PUT /[id]` - Update campaign
+- `DELETE /[id]` - Delete campaign
+
+## Endpoints
+
+### 📋 List Campaigns
+
+```http
+GET /api/[orgId]/campaigns
+```
+
+**Description**: Lấy danh sách campaigns của organization
+
+**Query Parameters**:
+
+- `page` (optional): Page number, default: 1
+- `limit` (optional): Items per page, default: 20
+- `search` (optional): Search campaigns by name/description
+- `status` (optional): Filter by status (planned)
+
+**Response**: Array of campaigns với content và schedule counts
+
+**Permissions**: `MANAGE_CONTENT` (Creator, Brand Owner, Admin)
+
+### ➕ Create Campaign
+
+```http
+POST /api/[orgId]/campaigns
+```
+
+**Description**: Tạo campaign mới
+
+**Request Body**: Campaign data theo schema validation
+
+**Response**: Created campaign object
+
+**Permissions**: `MANAGE_CAMPAIGNS` (Brand Owner, Admin)
+
+### 👁️ Get Campaign Details
+
+```http
+GET /api/[orgId]/campaigns/[id]
+```
+
+**Description**: Lấy thông tin chi tiết campaign
+
+**Response**: Campaign object với related content và schedules
+
+**Permissions**: `MANAGE_CONTENT` (Creator, Brand Owner, Admin)
+
+### ✏️ Update Campaign
+
+```http
+PUT /api/[orgId]/campaigns/[id]
+```
+
+**Description**: Cập nhật thông tin campaign
+
+**Request Body**: Updated campaign data
+
+**Response**: Updated campaign object
+
+**Permissions**: `MANAGE_CAMPAIGNS` (Brand Owner, Admin)
+
+### 🗑️ Delete Campaign
+
+```http
+DELETE /api/[orgId]/campaigns/[id]
+```
+
+**Description**: Xóa campaign và tất cả related content
+
+**Response**: Success confirmation
+
+**Permissions**: `MANAGE_CAMPAIGNS` (Brand Owner, Admin)
+
+## Authentication
+
+Tất cả endpoints yêu cầu authentication thông qua NextAuth session.
+
+```typescript
+// Session check
+const session = await auth();
+if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+}
+```
+
+## Permissions
+
+### Role-based Access Control
+
+```typescript
+// Campaign permissions
+const PERMISSIONS = {
+ MANAGE_CONTENT: ['creator', 'brand_owner', 'admin'],
+ MANAGE_CAMPAIGNS: ['brand_owner', 'admin'],
+};
+```
+
+### Permission Matrix
+
+| Role | View Campaigns | Create Campaigns | Edit Campaigns | Delete Campaigns |
+| ----------- | -------------- | ---------------- | -------------- | ---------------- |
+| Creator | ✅ | ❌ | ❌ | ❌ |
+| Brand Owner | ✅ | ✅ | ✅ | ✅ |
+| Admin | ✅ | ✅ | ✅ | ✅ |
+
+## Request/Response Formats
+
+### Campaign Schema
+
+```typescript
+interface Campaign {
+ id: string;
+ name: string;
+ description?: string;
+ organizationId: string;
+ createdAt: DateTime;
+ updatedAt: DateTime;
+
+ // Relations (when included)
+ contents?: Content[];
+ schedules?: Schedule[];
+ analyticsEvents?: AnalyticsEvent[];
+}
+```
+
+### Create Campaign Request
+
+```typescript
+interface CreateCampaignRequest {
+ name: string;
+ description?: string;
+}
+```
+
+### API Response Format
+
+```typescript
+interface ApiResponse {
+ // Success case
+ data?: T;
+
+ // Error case
+ error?: string;
+ message?: string;
+}
+```
+
+## Error Handling
+
+### HTTP Status Codes
+
+- `200` - Success
+- `201` - Created (POST)
+- `400` - Bad Request (validation errors)
+- `401` - Unauthorized (no session)
+- `403` - Forbidden (insufficient permissions)
+- `404` - Not Found
+- `500` - Internal Server Error
+
+### Error Response Format
+
+```json
+{
+ "error": "Error message",
+ "message": "Detailed error description"
+}
+```
+
+### Common Error Scenarios
+
+- **E_VALIDATION**: Invalid request data
+- **E_FORBIDDEN**: Insufficient permissions
+- **E_NOT_FOUND**: Campaign doesn't exist
+- **E_ORG_ACCESS**: User not member of organization
+
+## Examples
+
+### Create Campaign
+
+```bash
+curl -X POST /api/org123/campaigns \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Summer Sale 2025",
+ "description": "Promotional campaign for summer products"
+ }'
+```
+
+**Response**:
+
+```json
+{
+ "id": "camp_abc123",
+ "name": "Summer Sale 2025",
+ "description": "Promotional campaign for summer products",
+ "organizationId": "org123",
+ "createdAt": "2025-01-02T10:00:00Z",
+ "updatedAt": "2025-01-02T10:00:00Z"
+}
+```
+
+### List Campaigns
+
+```bash
+curl /api/org123/campaigns?page=1&limit=10
+```
+
+**Response**:
+
+```json
+[
+ {
+ "id": "camp_abc123",
+ "name": "Summer Sale 2025",
+ "description": "Promotional campaign for summer products",
+ "organizationId": "org123",
+ "createdAt": "2025-01-02T10:00:00Z",
+ "updatedAt": "2025-01-02T10:00:00Z",
+ "contents": [
+ {
+ "id": "cont_xyz789",
+ "title": "Summer Sale Announcement",
+ "body": "Get ready for amazing summer deals!",
+ "campaignId": "camp_abc123"
+ }
+ ],
+ "schedules": [
+ {
+ "id": "sched_def456",
+ "date": "2025-06-01T09:00:00Z",
+ "status": "scheduled",
+ "campaignId": "camp_abc123"
+ }
+ ]
+ }
+]
+```
+
+## Best Practices
+
+### 🔒 Security
+
+- Luôn check organization membership trước khi access
+- Validate input data với Zod schemas
+- Log tất cả campaign operations cho audit
+
+### 📊 Performance
+
+- Sử dụng pagination cho large datasets
+- Include relations chỉ khi cần thiết
+- Cache campaign data cho frequently accessed campaigns
+
+### 🧪 Testing
+
+- Test với different user roles
+- Verify permission checks
+- Test edge cases (empty campaigns, invalid IDs)
+
+## Gotchas
+
+### ⚠️ Common Issues
+
+1. **Organization Access**: User phải là member của organization
+2. **Permission Levels**: Different roles có different access levels
+3. **Cascade Deletes**: Deleting campaign removes all related content
+4. **Validation**: Name field required, description optional
+
+### 💡 Tips
+
+- Sử dụng `requirePermission` helper cho consistent permission checking
+- Include error handling cho database operations
+- Log user actions cho analytics và debugging
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Engineering Team_
diff --git a/docs/api/content.md b/docs/api/content.md
new file mode 100644
index 00000000..25e4f83c
--- /dev/null
+++ b/docs/api/content.md
@@ -0,0 +1,400 @@
+# Content API
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Endpoints](#endpoints)
+- [Data Models](#data-models)
+- [Authentication](#authentication)
+- [Error Handling](#error-handling)
+- [Examples](#examples)
+
+## Overview
+
+Content API quản lý việc tạo, chỉnh sửa, và quản lý nội dung marketing trong campaigns. API hỗ trợ AI-assisted content generation, approval workflow, và status management.
+
+### Key Features
+
+- **Content CRUD**: Create, read, update, delete content
+- **AI Integration**: AI-assisted content generation
+- **Status Workflow**: DRAFT → SUBMITTED → APPROVED → SCHEDULED → PUBLISHED
+- **Asset Management**: File attachments và media linking
+- **Campaign Integration**: Content thuộc về campaigns
+
+## Endpoints
+
+### 📋 List Content
+
+```http
+GET /api/[orgId]/content
+```
+
+**Query Parameters:**
+
+- `campaignId` (optional): Filter by campaign ID
+- `status` (optional): Filter by content status
+
+**Example:**
+
+```http
+GET /api/org_123/content?status=DRAFT&campaignId=camp_1
+```
+
+**Response:**
+
+```json
+{
+ "ok": true,
+ "data": [
+ {
+ "id": "content_456",
+ "title": "New Product Launch",
+ "body": "Exciting new product announcement...",
+ "status": "DRAFT",
+ "campaignId": "camp_1",
+ "campaign": {
+ "id": "camp_1",
+ "name": "Q1 Campaign"
+ },
+ "assets": [
+ {
+ "id": "asset_789",
+ "url": "https://storage.example.com/image.jpg",
+ "name": "Product Image",
+ "type": "image/jpeg",
+ "size": 1024000,
+ "description": "Product showcase image",
+ "tags": ["product", "launch", "marketing"]
+ }
+ ],
+ "createdAt": "2025-01-10T10:00:00.000Z",
+ "updatedAt": "2025-01-10T10:00:00.000Z"
+ }
+ ]
+}
+```
+
+### ➕ Create Content
+
+```http
+POST /api/[orgId]/content
+```
+
+**Request Body:**
+
+```typescript
+interface CreateContentRequest {
+ title: string; // Content title
+ body?: string; // Content body (rich text)
+ status?: ContentStatus; // Optional, defaults to DRAFT
+ campaignId: string; // Campaign ID
+}
+```
+
+**Example Request:**
+
+```json
+{
+ "title": "Summer Sale Announcement",
+ "body": "Get ready for amazing summer deals!",
+ "campaignId": "camp_1"
+}
+```
+
+**Response:**
+
+```json
+{
+ "ok": true,
+ "data": {
+ "id": "content_456",
+ "title": "Summer Sale Announcement",
+ "body": "Get ready for amazing summer deals!",
+ "status": "DRAFT",
+ "campaignId": "camp_1",
+ "campaign": {
+ "id": "camp_1",
+ "name": "Q1 Campaign"
+ },
+ "assets": [],
+ "createdAt": "2025-01-10T10:00:00.000Z",
+ "updatedAt": "2025-01-10T10:00:00.000Z"
+ }
+}
+```
+
+### 🤖 AI Content Generation
+
+```http
+POST /api/[orgId]/content/generate
+```
+
+**Request Body:**
+
+```typescript
+interface GenerateContentRequest {
+ prompt: string; // AI generation prompt
+ campaignId: string; // Campaign ID
+ contentType?: string; // Optional content type
+ tone?: string; // Optional tone (professional, casual, etc.)
+}
+```
+
+**Example Request:**
+
+```json
+{
+ "prompt": "Create a social media post about our new eco-friendly product line",
+ "campaignId": "camp_1",
+ "tone": "professional"
+}
+```
+
+**Response:**
+
+```json
+{
+ "ok": true,
+ "data": {
+ "id": "content_456",
+ "title": "Eco-Friendly Product Launch",
+ "body": "We're excited to announce our new eco-friendly product line...",
+ "status": "DRAFT",
+ "campaignId": "camp_1",
+ "aiGenerated": true,
+ "prompt": "Create a social media post about our new eco-friendly product line",
+ "createdAt": "2025-01-10T10:00:00.000Z",
+ "updatedAt": "2025-01-10T10:00:00.000Z"
+ }
+}
+```
+
+## Data Models
+
+### 📝 Content Entity
+
+```typescript
+interface Content {
+ id: string; // cuid
+ title: string; // Content title
+ body?: string; // Content body (rich text)
+ status: ContentStatus; // Current content status
+ campaignId: string; // Campaign reference
+ createdAt: Date; // Creation timestamp
+ updatedAt: Date; // Last update time
+}
+
+enum ContentStatus {
+ DRAFT // Initial draft state
+ SUBMITTED // Submitted for review
+ APPROVED // Approved by brand owner
+ SCHEDULED // Scheduled for publication
+ PUBLISHED // Successfully published
+ REJECTED // Rejected during review
+}
+```
+
+### 🖼️ Asset Entity
+
+```typescript
+interface Asset {
+ id: string; // cuid
+ url: string; // File storage URL
+ name?: string; // Display name
+ type: string; // MIME type
+ size?: number; // File size in bytes
+ description?: string; // Asset description
+ tags: string[]; // Searchable tags
+ contentId: string; // Content reference
+ createdAt: Date; // Upload timestamp
+}
+```
+
+### 🔍 Query Parameters
+
+```typescript
+interface ListContentQuery {
+ campaignId?: string; // Filter by campaign
+ status?: ContentStatus; // Filter by status
+}
+```
+
+## Authentication
+
+### 🔐 Required Permissions
+
+- **View Content**: `MANAGE_CONTENT` permission
+- **Create Content**: `MANAGE_CONTENT` permission
+- **Update Content**: `MANAGE_CONTENT` permission
+- **Delete Content**: `MANAGE_CONTENT` permission
+- **Approve Content**: `APPROVE_CONTENT` permission (Brand Owner, Admin)
+- **Roles**: `CREATOR`, `BRAND_OWNER`, `ADMIN`
+
+### 🛡️ Security Features
+
+- **Organization Isolation**: Chỉ truy cập content của org
+- **Campaign Validation**: Campaign phải thuộc về organization
+- **Role-based Access**: Permissions được check trước mỗi operation
+
+## Error Handling
+
+### ❌ Error Response Format
+
+```typescript
+interface ErrorResponse {
+ ok: false;
+ error: {
+ code: string; // Error code
+ message: string; // Human-readable message
+ details?: any; // Additional error details
+ };
+}
+```
+
+### 🚫 Error Codes
+
+#### `E_UNAUTHORIZED` (401)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_UNAUTHORIZED",
+ "message": "Unauthorized"
+ }
+}
+```
+
+**Cause**: User chưa đăng nhập hoặc session expired
+**Solution**: Re-authenticate user
+
+#### `E_FORBIDDEN` (403)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_FORBIDDEN",
+ "message": "Forbidden"
+ }
+}
+```
+
+**Cause**: User không có permission `MANAGE_CONTENT`
+**Solution**: Check user role và permissions
+
+#### `E_NOT_FOUND` (404)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_NOT_FOUND",
+ "message": "Campaign not found"
+ }
+}
+```
+
+**Cause**: Campaign không tồn tại
+**Solution**: Verify campaign ID
+
+#### `E_VALIDATION` (400)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_VALIDATION",
+ "message": "Title is required"
+ }
+}
+```
+
+**Cause**: Invalid request data (missing required fields)
+**Solution**: Validate request body
+
+#### `E_INTERNAL` (500)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_INTERNAL",
+ "message": "Internal server error"
+ }
+}
+```
+
+**Cause**: Server error hoặc database issue
+**Solution**: Contact support team
+
+## Examples
+
+### 📝 Create New Content
+
+```bash
+curl -X POST /api/org_123/content \
+ -H "Content-Type: application/json" \
+ -d '{
+ "title": "Product Launch Announcement",
+ "body": "We are excited to announce our latest product...",
+ "campaignId": "camp_1"
+ }'
+```
+
+### 🔍 Get Draft Content
+
+```bash
+curl "/api/org_123/content?status=DRAFT&campaignId=camp_1"
+```
+
+### 🤖 Generate AI Content
+
+```bash
+curl -X POST /api/org_123/content/generate \
+ -H "Content-Type: application/json" \
+ -d '{
+ "prompt": "Create a social media post about sustainability",
+ "campaignId": "camp_1",
+ "tone": "professional"
+ }'
+```
+
+## Business Logic
+
+### 📊 Content Status Workflow
+
+1. **DRAFT**: Initial content creation
+2. **SUBMITTED**: Creator submits for review
+3. **APPROVED**: Brand owner approves content
+4. **SCHEDULED**: Content scheduled for publication
+5. **PUBLISHED**: Content successfully published
+6. **REJECTED**: Content rejected during review
+
+### 🔄 Status Transitions
+
+- **DRAFT → SUBMITTED**: Creator action
+- **SUBMITTED → APPROVED/REJECTED**: Brand owner action
+- **APPROVED → SCHEDULED**: When schedule is created
+- **SCHEDULED → PUBLISHED**: After successful publishing
+- **Any → REJECTED**: Brand owner can reject at any stage
+
+### 🎯 AI Content Generation
+
+- **Input**: Prompt, campaign context, tone preferences
+- **Process**: AI service generates content based on prompt
+- **Output**: Generated content với status DRAFT
+- **Editing**: Creator can edit và refine AI-generated content
+
+### 📁 Asset Management
+
+- **Upload**: File upload với type/size validation
+- **Linking**: Assets được link với content
+- **Metadata**: Tags, descriptions cho searchability
+- **Storage**: Secure file storage với access control
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 2.0_
+_Maintainer: Engineering Team_
diff --git a/docs/api/schedules.md b/docs/api/schedules.md
new file mode 100644
index 00000000..22d6f95f
--- /dev/null
+++ b/docs/api/schedules.md
@@ -0,0 +1,364 @@
+# Schedules API
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Endpoints](#endpoints)
+- [Data Models](#data-models)
+- [Authentication](#authentication)
+- [Error Handling](#error-handling)
+- [Examples](#examples)
+
+## Overview
+
+Schedules API quản lý việc lên lịch xuất bản content trên các nền tảng social media. API hỗ trợ tạo, xem, và quản lý schedules với timezone support và conflict detection.
+
+### Key Features
+
+- **Multi-view Support**: Day, Week, Month calendar views
+- **Timezone Handling**: `runAt` lưu UTC, `timezone` cho display
+- **Conflict Detection**: Cảnh báo overlap cùng channel
+- **Status Management**: Content status tự động → SCHEDULED
+- **Filtering**: Theo channels, campaigns, date ranges
+
+## Endpoints
+
+### 📅 List Schedules
+
+```http
+GET /api/[orgId]/schedules
+```
+
+**Query Parameters:**
+
+- `from` (required): Start date (ISO 8601)
+- `to` (required): End date (ISO 8601)
+- `channels` (optional): Comma-separated channel list
+- `campaigns` (optional): Comma-separated campaign IDs
+
+**Example:**
+
+```http
+GET /api/org_123/schedules?from=2025-01-01T00:00:00Z&to=2025-01-31T23:59:59Z&channels=FACEBOOK,INSTAGRAM&campaigns=camp_1,camp_2
+```
+
+**Response:**
+
+```json
+{
+ "ok": true,
+ "data": [
+ {
+ "id": "sched_123",
+ "runAt": "2025-01-15T09:00:00.000Z",
+ "timezone": "America/New_York",
+ "channel": "FACEBOOK",
+ "status": "PENDING",
+ "campaignId": "camp_1",
+ "contentId": "content_456",
+ "campaign": {
+ "id": "camp_1",
+ "name": "Q1 Campaign"
+ },
+ "content": {
+ "id": "content_456",
+ "title": "New Product Launch",
+ "status": "SCHEDULED"
+ },
+ "createdAt": "2025-01-10T10:00:00.000Z",
+ "updatedAt": "2025-01-10T10:00:00.000Z"
+ }
+ ]
+}
+```
+
+### ➕ Create Schedule
+
+```http
+POST /api/[orgId]/schedules
+```
+
+**Request Body:**
+
+```typescript
+interface CreateScheduleRequest {
+ runAt: string; // ISO 8601 datetime (UTC)
+ timezone: string; // Timezone identifier
+ channel: Channel; // Social media platform
+ status?: ScheduleStatus; // Optional, defaults to PENDING
+ campaignId: string; // Campaign ID
+ contentId: string; // Content ID to schedule
+}
+```
+
+**Example Request:**
+
+```json
+{
+ "runAt": "2025-01-15T14:00:00.000Z",
+ "timezone": "America/New_York",
+ "channel": "FACEBOOK",
+ "campaignId": "camp_1",
+ "contentId": "content_456"
+}
+```
+
+**Response:**
+
+```json
+{
+ "ok": true,
+ "data": {
+ "id": "sched_123",
+ "runAt": "2025-01-15T14:00:00.000Z",
+ "timezone": "America/New_York",
+ "channel": "FACEBOOK",
+ "status": "PENDING",
+ "campaignId": "camp_1",
+ "contentId": "content_456",
+ "campaign": {
+ "id": "camp_1",
+ "name": "Q1 Campaign"
+ },
+ "content": {
+ "id": "content_456",
+ "title": "New Product Launch",
+ "status": "SCHEDULED"
+ },
+ "createdAt": "2025-01-10T10:00:00.000Z",
+ "updatedAt": "2025-01-10T10:00:00.000Z"
+ }
+}
+```
+
+## Data Models
+
+### 📊 Schedule Entity
+
+```typescript
+interface Schedule {
+ id: string; // cuid
+ runAt: Date; // UTC timestamp
+ timezone: string; // Display timezone
+ channel: Channel; // Social platform
+ status: ScheduleStatus; // Current status
+ campaignId: string; // Campaign reference
+ contentId?: string; // Content reference
+ createdAt: Date; // Creation timestamp
+ updatedAt: Date; // Last update time
+}
+
+enum Channel {
+ FACEBOOK // 📘 Facebook posts
+ INSTAGRAM // 📷 Instagram posts
+ TWITTER // 🐦 Twitter/X posts
+ YOUTUBE // 📺 YouTube videos
+ LINKEDIN // 💼 LinkedIn posts
+ TIKTOK // 🎵 TikTok videos
+ BLOG // 📝 Blog articles
+}
+
+enum ScheduleStatus {
+ PENDING // Scheduled, waiting to publish
+ PUBLISHED // Successfully published
+ FAILED // Publication failed
+ CANCELLED // Schedule cancelled
+}
+```
+
+### 🔍 Query Parameters
+
+```typescript
+interface ListSchedulesQuery {
+ from: string; // ISO 8601 start date
+ to: string; // ISO 8601 end date
+ channels?: string[]; // Filter by channels
+ campaigns?: string[]; // Filter by campaign IDs
+}
+```
+
+## Authentication
+
+### 🔐 Required Permissions
+
+- **View Schedules**: `MANAGE_SCHEDULES` permission
+- **Create Schedules**: `MANAGE_SCHEDULES` permission
+- **Roles**: `BRAND_OWNER`, `CREATOR`, `ADMIN`
+
+### 🛡️ Security Features
+
+- **Organization Isolation**: Chỉ truy cập schedules của org
+- **Campaign Validation**: Campaign phải thuộc về organization
+- **Content Validation**: Content phải thuộc về campaign
+- **Role-based Access**: Permissions được check trước mỗi operation
+
+## Error Handling
+
+### ❌ Error Response Format
+
+```typescript
+interface ErrorResponse {
+ ok: false;
+ error: {
+ code: string; // Error code
+ message: string; // Human-readable message
+ details?: any; // Additional error details
+ };
+}
+```
+
+### 🚫 Error Codes
+
+#### `E_UNAUTHORIZED` (401)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_UNAUTHORIZED",
+ "message": "Unauthorized"
+ }
+}
+```
+
+**Cause**: User chưa đăng nhập hoặc session expired
+**Solution**: Re-authenticate user
+
+#### `E_FORBIDDEN` (403)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_FORBIDDEN",
+ "message": "Forbidden"
+ }
+}
+```
+
+**Cause**: User không có permission `MANAGE_SCHEDULES`
+**Solution**: Check user role và permissions
+
+#### `E_NOT_FOUND` (404)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_NOT_FOUND",
+ "message": "Campaign not found"
+ }
+}
+```
+
+**Cause**: Campaign hoặc Content không tồn tại
+**Solution**: Verify campaign/content IDs
+
+#### `E_VALIDATION` (400)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_VALIDATION",
+ "message": "Invalid date format"
+ }
+}
+```
+
+**Cause**: Invalid request data (date format, required fields)
+**Solution**: Validate request body
+
+#### `E_CONFLICT_SLOT` (409)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_CONFLICT_SLOT",
+ "message": "Potential scheduling conflict detected",
+ "details": {
+ "conflictingScheduleId": "sched_789"
+ }
+ }
+}
+```
+
+**Cause**: Có schedule khác cùng channel trong khoảng thời gian gần
+**Solution**: Chọn thời gian khác hoặc channel khác
+
+#### `E_INTERNAL` (500)
+
+```json
+{
+ "ok": false,
+ "error": {
+ "code": "E_INTERNAL",
+ "message": "Internal server error"
+ }
+}
+```
+
+**Cause**: Server error hoặc database issue
+**Solution**: Contact support team
+
+## Examples
+
+### 📱 Schedule Facebook Post
+
+```bash
+curl -X POST /api/org_123/schedules \
+ -H "Content-Type: application/json" \
+ -d '{
+ "runAt": "2025-01-15T14:00:00.000Z",
+ "timezone": "America/New_York",
+ "channel": "FACEBOOK",
+ "campaignId": "camp_1",
+ "contentId": "content_456"
+ }'
+```
+
+### 📅 Get Week Schedules
+
+```bash
+curl "/api/org_123/schedules?from=2025-01-13T00:00:00Z&to=2025-01-19T23:59:59Z&channels=FACEBOOK,INSTAGRAM"
+```
+
+### 🔍 Filter by Campaign
+
+```bash
+curl "/api/org_123/schedules?from=2025-01-01T00:00:00Z&to=2025-01-31T23:59:59Z&campaigns=camp_1,camp_2"
+```
+
+## Business Logic
+
+### 🎯 Schedule Creation Flow
+
+1. **Validation**: Check permissions, validate request data
+2. **Conflict Detection**: Look for overlapping schedules (same channel, ±15 minutes)
+3. **Transaction**: Create schedule + update content status
+4. **Response**: Return created schedule với relationships
+
+### 📊 Content Status Update
+
+- **Before**: Content status = `DRAFT` hoặc `APPROVED`
+- **After**: Content status = `SCHEDULED`
+- **Trigger**: Khi tạo schedule thành công
+
+### ⚠️ Conflict Detection Rules
+
+- **Same Channel**: Không cho phép 2 schedules cùng channel trong ±15 phút
+- **Time Window**: ±15 minutes để tránh spam
+- **Status Check**: Chỉ check `PENDING` và `PUBLISHED` schedules
+
+### 🌐 Timezone Handling
+
+- **Storage**: `runAt` luôn lưu UTC timestamp
+- **Display**: `timezone` field để hiển thị local time
+- **Conversion**: Client-side conversion cho UI display
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 2.0_
+_Maintainer: Engineering Team_
diff --git a/docs/data-model.md b/docs/data-model.md
new file mode 100644
index 00000000..9c093db2
--- /dev/null
+++ b/docs/data-model.md
@@ -0,0 +1,570 @@
+# AiM Platform - Data Model
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Core Entities](#core-entities)
+- [NextAuth Integration](#nextauth-integration)
+- [Entity Relationships](#entity-relationships)
+- [Field Conventions](#field-conventions)
+- [Database Schema](#database-schema)
+
+## Overview
+
+AiM Platform sử dụng **PostgreSQL** làm database chính và **Prisma ORM** để quản lý data access. Hệ thống được thiết kế với multi-tenancy architecture, mỗi organization có data riêng biệt.
+
+### Tech Stack
+
+- **Database**: PostgreSQL 15+
+- **ORM**: Prisma 6
+- **Authentication**: NextAuth.js 5
+- **Validation**: Zod schemas
+
+## Core Entities
+
+### 👤 User
+
+**Mô tả**: Người dùng hệ thống với authentication và role management
+
+```typescript
+interface User {
+ id: string; // cuid
+ name: string; // Display name
+ email: string; // Unique email
+ password?: string; // Hashed password (optional for OAuth)
+ emailVerified?: Date; // Email verification timestamp
+ image?: string; // Profile image URL
+ createdAt: Date; // Account creation time
+ updatedAt: Date; // Last update time
+}
+```
+
+### 🏢 Organization
+
+**Mô tả**: Tổ chức/company sử dụng platform
+
+```typescript
+interface Organization {
+ id: string; // cuid
+ name: string; // Organization name
+ createdAt: Date; // Creation timestamp
+ updatedAt: Date; // Last update time
+}
+```
+
+### 🔗 Membership
+
+**Mô tả**: Quan hệ giữa User và Organization với role assignment
+
+```typescript
+interface Membership {
+ id: string; // cuid
+ userId: string; // Reference to User
+ organizationId: string; // Reference to Organization
+ role: OrgRole; // User role in organization
+ createdAt: Date; // Membership creation time
+ updatedAt: Date; // Last update time
+}
+
+enum OrgRole {
+ ADMIN // Full system access
+ BRAND_OWNER // Campaign & content management
+ CREATOR // Content creation only
+}
+```
+
+### 📢 Campaign
+
+**Mô tả**: Chiến dịch marketing với content và scheduling
+
+```typescript
+interface Campaign {
+ id: string; // cuid
+ name: string; // Campaign name
+ description?: string; // Campaign description
+ organizationId: string; // Reference to Organization
+ createdAt: Date; // Creation timestamp
+ updatedAt: Date; // Last update time
+}
+```
+
+### ✍️ Content
+
+**Mô tả**: Nội dung marketing với approval workflow và scheduling
+
+```typescript
+interface Content {
+ id: string; // cuid
+ title: string; // Content title
+ body?: string; // Content body (rich text)
+ status: ContentStatus; // Current content status
+ campaignId: string; // Reference to Campaign
+ createdAt: Date; // Creation timestamp
+ updatedAt: Date; // Last update time
+}
+
+enum ContentStatus {
+ DRAFT // Initial draft state
+ SUBMITTED // Submitted for review
+ APPROVED // Approved by brand owner
+ SCHEDULED // Scheduled for publication
+ PUBLISHED // Successfully published
+ REJECTED // Rejected during review
+}
+```
+
+### 📁 Asset
+
+**Mô tả**: File attachments (images, videos, documents) cho content
+
+```typescript
+interface Asset {
+ id: string; // cuid
+ url: string; // File storage URL
+ name?: string; // Display name
+ type: string; // MIME type
+ size?: number; // File size in bytes
+ description?: string; // Asset description
+ tags: string[]; // Searchable tags
+ contentId: string; // Reference to Content
+ createdAt: Date; // Upload timestamp
+}
+```
+
+### 🗓️ Schedule
+
+**Mô tả**: Lịch trình xuất bản content với platform và timezone support
+
+```typescript
+interface Schedule {
+ id: string; // cuid
+ runAt: Date; // UTC timestamp for publication
+ timezone: string; // Timezone for display (e.g., "America/New_York")
+ channel: Channel; // Social media platform
+ status: ScheduleStatus; // Current schedule status
+ campaignId: string; // Reference to Campaign
+ contentId?: string; // Reference to Content (optional)
+ createdAt: Date; // Creation timestamp
+ updatedAt: Date; // Last update time
+}
+
+enum Channel {
+ FACEBOOK // Facebook posts
+ INSTAGRAM // Instagram posts
+ TWITTER // Twitter/X posts
+ YOUTUBE // YouTube videos
+ LINKEDIN // LinkedIn posts
+ TIKTOK // TikTok videos
+ BLOG // Blog articles
+}
+
+enum ScheduleStatus {
+ PENDING // Scheduled, waiting to publish
+ PUBLISHED // Successfully published
+ FAILED // Publication failed
+ CANCELLED // Schedule cancelled
+}
+```
+
+### 📊 AnalyticsEvent
+
+**Mô tả**: Event tracking cho analytics và performance monitoring
+
+```typescript
+interface AnalyticsEvent {
+ id: string; // cuid
+ event: string; // Event type
+ data?: Json; // Event payload
+ userId?: string; // Reference to User
+ organizationId?: string; // Reference to Organization
+ campaignId?: string; // Reference to Campaign
+ contentId?: string; // Reference to Content
+ createdAt: Date; // Event timestamp
+}
+```
+
+## NextAuth Integration
+
+### 🔐 Account
+
+**Mô tả**: OAuth provider accounts cho NextAuth
+
+```typescript
+interface Account {
+ id: string; // cuid
+ userId: string; // Reference to User
+ type: string; // OAuth provider type
+ provider: string; // Provider name (google, github)
+ providerAccountId: string; // Provider user ID
+ refresh_token?: string; // OAuth refresh token
+ access_token?: string; // OAuth access token
+ expires_at?: number; // Token expiration
+ token_type?: string; // Token type
+ scope?: string; // OAuth scope
+ id_token?: string; // ID token
+ session_state?: string; // Session state
+ oauth_token_secret?: string; // OAuth token secret
+ oauth_token?: string; // OAuth token
+}
+```
+
+### 🎫 Session
+
+**Mô tả**: User sessions cho NextAuth
+
+```typescript
+interface Session {
+ id: string; // cuid
+ sessionToken: string; // Unique session token
+ userId: string; // Reference to User
+ expires: Date; // Session expiration
+}
+```
+
+### ✅ VerificationToken
+
+**Mô tả**: Email verification tokens
+
+```typescript
+interface VerificationToken {
+ identifier: string; // Email address
+ token: string; // Verification token
+ expires: Date; // Token expiration
+}
+```
+
+## Entity Relationships
+
+### 🔗 Relationship Diagram
+
+```mermaid
+erDiagram
+ User ||--o{ Membership : "belongs to"
+ Organization ||--o{ Membership : "has members"
+ Organization ||--o{ Campaign : "owns"
+ Campaign ||--o{ Content : "contains"
+ Campaign ||--o{ Schedule : "schedules"
+ Content ||--o{ Asset : "has"
+ Content ||--o{ Schedule : "scheduled in"
+ User ||--o{ AnalyticsEvent : "generates"
+ Organization ||--o{ AnalyticsEvent : "tracks"
+ Campaign ||--o{ AnalyticsEvent : "monitors"
+ Content ||--o{ AnalyticsEvent : "analyzes"
+
+ User {
+ string id PK
+ string name
+ string email UK
+ string password
+ date emailVerified
+ string image
+ date createdAt
+ date updatedAt
+ }
+
+ Organization {
+ string id PK
+ string name
+ date createdAt
+ date updatedAt
+ }
+
+ Membership {
+ string id PK
+ string userId FK
+ string organizationId FK
+ enum role
+ date createdAt
+ date updatedAt
+ }
+
+ Campaign {
+ string id PK
+ string name
+ string description
+ string organizationId FK
+ date createdAt
+ date updatedAt
+ }
+
+ Content {
+ string id PK
+ string title
+ string body
+ enum status
+ string campaignId FK
+ date createdAt
+ date updatedAt
+ }
+
+ Asset {
+ string id PK
+ string url
+ string name
+ string type
+ int size
+ string description
+ string[] tags
+ string contentId FK
+ date createdAt
+ }
+
+ Schedule {
+ string id PK
+ date runAt
+ string timezone
+ enum channel
+ enum status
+ string campaignId FK
+ string contentId FK
+ date createdAt
+ date updatedAt
+ }
+
+ AnalyticsEvent {
+ string id PK
+ string event
+ json data
+ string userId FK
+ string organizationId FK
+ string campaignId FK
+ string contentId FK
+ date createdAt
+ }
+```
+
+### 📍 Key Relationships
+
+#### User ↔ Organization (Many-to-Many)
+
+- **Through**: `Membership` table
+- **Constraint**: Unique `(userId, organizationId)` combination
+- **Role**: User có thể thuộc nhiều organization với role khác nhau
+
+#### Campaign → Organization (Many-to-One)
+
+- **Constraint**: Mỗi campaign thuộc về một organization
+- **Cascade**: Khi organization bị xóa, campaigns cũng bị xóa
+
+#### Content → Campaign (Many-to-One)
+
+- **Constraint**: Mỗi content thuộc về một campaign
+- **Workflow**: Content status flow: DRAFT → SUBMITTED → APPROVED → SCHEDULED → PUBLISHED
+
+#### Schedule → Content (Many-to-One)
+
+- **Constraint**: Mỗi schedule có thể liên kết với một content
+- **Status Sync**: Khi tạo schedule, content status tự động → SCHEDULED
+- **Timezone**: `runAt` lưu UTC, `timezone` chỉ để hiển thị
+
+## Field Conventions
+
+### 🆔 ID Fields
+
+- **Primary Keys**: Sử dụng `cuid` cho tất cả entities
+- **Foreign Keys**: Naming convention: `entityNameId` (e.g., `campaignId`, `contentId`)
+- **References**: Luôn có `onDelete` behavior được định nghĩa
+
+### 📅 Timestamp Fields
+
+- **Created**: `createdAt` - tự động set khi tạo record
+- **Updated**: `updatedAt` - tự động update khi modify record
+- **Format**: ISO 8601 datetime strings
+
+### 🔒 Status Fields
+
+- **Content Status**: Enum với workflow progression
+- **Schedule Status**: Enum với publication lifecycle
+- **Org Role**: Enum với permission hierarchy
+
+### 🌐 Timezone Handling
+
+- **Storage**: `runAt` luôn lưu UTC timestamp
+- **Display**: `timezone` field để hiển thị local time
+- **Conversion**: Client-side conversion cho UI display
+
+## Database Schema
+
+### 🗄️ Prisma Schema
+
+```prisma
+// Core models với relationships
+model User {
+ id String @id @default(cuid())
+ name String?
+ email String @unique
+ password String?
+ emailVerified DateTime?
+ image String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relationships
+ accounts Account[]
+ sessions Session[]
+ memberships Membership[]
+ analyticsEvents AnalyticsEvent[]
+}
+
+model Organization {
+ id String @id @default(cuid())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relationships
+ memberships Membership[]
+ campaigns Campaign[]
+ analyticsEvents AnalyticsEvent[]
+}
+
+model Membership {
+ id String @id @default(cuid())
+ userId String
+ organizationId String
+ role OrgRole
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relationships
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+ @@unique([userId, organizationId])
+}
+
+model Campaign {
+ id String @id @default(cuid())
+ name String
+ description String?
+ organizationId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relationships
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ contents Content[]
+ schedules Schedule[]
+ analyticsEvents AnalyticsEvent[]
+}
+
+model Content {
+ id String @id @default(cuid())
+ title String
+ body String?
+ status ContentStatus @default(DRAFT)
+ campaignId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relationships
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+ assets Asset[]
+ schedules Schedule[]
+ analyticsEvents AnalyticsEvent[]
+}
+
+model Asset {
+ id String @id @default(cuid())
+ url String
+ name String?
+ type String
+ size Int?
+ description String?
+ tags String[]
+ contentId String
+ createdAt DateTime @default(now())
+
+ // Relationships
+ content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
+}
+
+model Schedule {
+ id String @id @default(cuid())
+ runAt DateTime // UTC timestamp
+ timezone String // Display timezone
+ channel Channel // Social platform
+ status ScheduleStatus @default(PENDING)
+ campaignId String
+ contentId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relationships
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+ content Content? @relation(fields: [contentId], references: [id], onDelete: SetNull)
+}
+
+model AnalyticsEvent {
+ id String @id @default(cuid())
+ event String
+ data Json?
+ userId String?
+ organizationId String?
+ campaignId String?
+ contentId String?
+ createdAt DateTime @default(now())
+
+ // Relationships
+ user User? @relation(fields: [userId], references: [id])
+ organization Organization? @relation(fields: [organizationId], references: [id])
+ campaign Campaign? @relation(fields: [campaignId], references: [id])
+ content Content? @relation(fields: [contentId], references: [id])
+}
+
+// Enums
+enum OrgRole {
+ ADMIN
+ BRAND_OWNER
+ CREATOR
+}
+
+enum ContentStatus {
+ DRAFT
+ SUBMITTED
+ APPROVED
+ SCHEDULED
+ PUBLISHED
+ REJECTED
+}
+
+enum ScheduleStatus {
+ PENDING
+ PUBLISHED
+ FAILED
+ CANCELLED
+}
+
+enum Channel {
+ FACEBOOK
+ INSTAGRAM
+ TWITTER
+ YOUTUBE
+ LINKEDIN
+ TIKTOK
+ BLOG
+}
+```
+
+### 🔍 Indexes & Performance
+
+```sql
+-- Primary indexes (automatic)
+CREATE INDEX ON "User"("email");
+CREATE INDEX ON "Membership"("userId", "organizationId");
+CREATE INDEX ON "Campaign"("organizationId");
+CREATE INDEX ON "Content"("campaignId", "status");
+CREATE INDEX ON "Schedule"("runAt", "channel");
+CREATE INDEX ON "AnalyticsEvent"("createdAt", "event");
+
+-- Composite indexes for common queries
+CREATE INDEX ON "Schedule"("campaignId", "runAt");
+CREATE INDEX ON "Content"("status", "createdAt");
+CREATE INDEX ON "AnalyticsEvent"("organizationId", "createdAt");
+```
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 2.0_
+_Maintainer: Engineering Team_
diff --git a/docs/design/schedule/README.md b/docs/design/schedule/README.md
new file mode 100644
index 00000000..6a5eb8bf
--- /dev/null
+++ b/docs/design/schedule/README.md
@@ -0,0 +1,89 @@
+# Schedule Design Mockups
+
+This directory contains design mockups and screenshots for the Schedule interface.
+
+## 📁 File Structure
+
+```
+docs/design/schedule/
+├── README.md # This file
+├── day-view/ # Day view mockups
+│ ├── default.png # Default day view
+│ ├── with-draft-panel.png # Day view with draft panel open
+│ └── drag-drop.png # Drag and drop interaction
+├── week-view/ # Week view mockups
+│ ├── default.png # Default week view
+│ ├── with-draft-panel.png # Week view with draft panel
+│ └── show-draft.png # Week view showing draft content
+├── month-view/ # Month view mockups
+│ ├── default.png # Default month view
+│ └── with-schedules.png # Month view with scheduled content
+└── components/ # Component-specific mockups
+ ├── draft-panel.png # Draft panel component
+ ├── schedule-sheet.png # Schedule confirmation sheet
+ └── filters.png # Filter controls
+```
+
+## 🎨 Mockup References
+
+The following mockups are referenced in the documentation:
+
+### Day View
+
+- **Default**: `/mnt/data/Schedule _ Day View.png`
+- **With Draft Panel**: `/mnt/data/Schedule _ Day View _ Show Draft.png`
+- **Drag & Drop**: `/mnt/data/Schedule _ Day View _ Show Draft _ Drag.png`
+
+### Week View
+
+- **Default**: `/mnt/data/Schedule _ Week View.png`
+- **With Draft Panel**: `/mnt/data/Schedule _ Week View _ Show Draft.png`
+- **Show Draft**: `/mnt/data/Schedule _ Week View _ Show Draft-1.png`
+
+### Month View
+
+- **Default**: `/mnt/data/Schedule _ MonthView.png`
+- **With Schedules**: `/mnt/data/Schedule _ MonthView-1.png`
+- **Alternative**: `/mnt/data/Schedule _ MonthView-2.png`
+
+## 📱 Design Specifications
+
+### Color Palette
+
+- **Primary**: Blue (#3b82f6)
+- **Secondary**: Gray (#6b7280)
+- **Success**: Green (#10b981)
+- **Warning**: Yellow (#f59e0b)
+- **Error**: Red (#ef4444)
+
+### Typography
+
+- **Headings**: Inter, font-weight: 600-700
+- **Body**: Inter, font-weight: 400-500
+- **Monospace**: JetBrains Mono (for time displays)
+
+### Spacing
+
+- **Grid**: 4px base unit
+- **Padding**: 16px, 24px, 32px
+- **Margins**: 8px, 16px, 24px, 32px
+
+### Components
+
+- **Cards**: 8px border radius, 1px border
+- **Buttons**: 6px border radius, 8px padding
+- **Inputs**: 6px border radius, 12px padding
+
+## 🔗 Documentation Links
+
+These mockups are referenced in:
+
+- [Schedule UI Guide](../ui/schedule.md)
+- [Product Specification](../../SPEC.md)
+- [API Documentation](../../api/schedules.md)
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Design Team_
diff --git a/docs/playbooks/observability.md b/docs/playbooks/observability.md
new file mode 100644
index 00000000..d7a03e4f
--- /dev/null
+++ b/docs/playbooks/observability.md
@@ -0,0 +1,738 @@
+# Observability Playbook
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Logging Strategy](#logging-strategy)
+- [Error Tracking](#error-tracking)
+- [Health Checks](#health-checks)
+- [Performance Monitoring](#performance-monitoring)
+- [Alerting](#alerting)
+- [Best Practices](#best-practices)
+
+## Overview
+
+Observability là foundation cho maintaining system health, debugging issues, và optimizing performance. Playbook này cover logging, error tracking, health checks, và monitoring strategies cho AiM Platform.
+
+### Observability Pillars
+
+1. **Logs**: Structured logging cho debugging và audit
+2. **Metrics**: Performance và business metrics
+3. **Traces**: Request tracing cho distributed systems
+4. **Alerts**: Proactive notification về issues
+
+## Logging Strategy
+
+### 📝 Log Levels
+
+#### 1. Error (Level 0)
+
+```typescript
+// Critical errors that require immediate attention
+logger.error('Database connection failed', {
+ error: error.message,
+ stack: error.stack,
+ context: { userId, action, timestamp },
+});
+```
+
+#### 2. Warn (Level 1)
+
+```typescript
+// Warning conditions that might indicate problems
+logger.warn('High memory usage detected', {
+ memoryUsage: process.memoryUsage(),
+ threshold: '80%',
+ timestamp: new Date().toISOString(),
+});
+```
+
+#### 3. Info (Level 2)
+
+```typescript
+// General information about application flow
+logger.info('User logged in successfully', {
+ userId,
+ email,
+ role,
+ timestamp: new Date().toISOString(),
+});
+```
+
+#### 4. Debug (Level 3)
+
+```typescript
+// Detailed debugging information
+logger.debug('Processing campaign request', {
+ campaignId,
+ requestData,
+ processingSteps: ['validation', 'permission_check', 'creation'],
+});
+```
+
+### 🏗️ Logging Implementation
+
+#### Structured Logging Setup
+
+```typescript
+// lib/logger.ts
+import pino from 'pino';
+
+const logger = pino({
+ level: process.env.LOG_LEVEL || 'info',
+ timestamp: pino.stdTimeFunctions.isoTime,
+ formatters: {
+ level: (label) => ({ level: label }),
+ log: (object) => object,
+ },
+ serializers: {
+ error: pino.stdSerializers.err,
+ req: pino.stdSerializers.req,
+ res: pino.stdSerializers.res,
+ },
+});
+
+export default logger;
+```
+
+#### Request Logging Middleware
+
+```typescript
+// middleware.ts
+import { NextRequest, NextResponse } from 'next/server';
+import logger from '@/lib/logger';
+
+export function middleware(request: NextRequest) {
+ const start = Date.now();
+
+ logger.info('Incoming request', {
+ method: request.method,
+ url: request.url,
+ userAgent: request.headers.get('user-agent'),
+ ip: request.ip || request.headers.get('x-forwarded-for'),
+ });
+
+ const response = NextResponse.next();
+
+ response.headers.set('x-response-time', `${Date.now() - start}ms`);
+
+ logger.info('Request completed', {
+ method: request.method,
+ url: request.url,
+ status: response.status,
+ duration: Date.now() - start,
+ });
+
+ return response;
+}
+```
+
+#### Business Logic Logging
+
+```typescript
+// lib/services/campaigns.ts
+import logger from '@/lib/logger';
+
+export async function createCampaign(data: CreateCampaignRequest, userId: string) {
+ logger.info('Creating campaign', {
+ userId,
+ campaignName: data.name,
+ organizationId: data.organizationId,
+ });
+
+ try {
+ const campaign = await prisma.campaign.create({
+ data: {
+ ...data,
+ createdById: userId,
+ },
+ });
+
+ logger.info('Campaign created successfully', {
+ campaignId: campaign.id,
+ userId,
+ duration: Date.now() - start,
+ });
+
+ return campaign;
+ } catch (error) {
+ logger.error('Failed to create campaign', {
+ error: error.message,
+ userId,
+ campaignData: data,
+ });
+ throw error;
+ }
+}
+```
+
+### 📊 Log Aggregation
+
+#### Log Storage
+
+```typescript
+// Log to multiple destinations
+const logger = pino({
+ level: 'info',
+ transport: {
+ targets: [
+ // Console output
+ { target: 'pino-pretty', level: 'info' },
+ // File output
+ { target: 'pino/file', level: 'info', options: { destination: './logs/app.log' } },
+ // Remote logging service (optional)
+ {
+ target: 'pino-http-send',
+ level: 'error',
+ options: { destination: process.env.LOG_ENDPOINT },
+ },
+ ],
+ },
+});
+```
+
+## Error Tracking
+
+### 🚨 Error Categories
+
+#### 1. Application Errors
+
+```typescript
+// Custom error classes
+export class ValidationError extends Error {
+ constructor(
+ message: string,
+ public field: string,
+ public value: any
+ ) {
+ super(message);
+ this.name = 'ValidationError';
+ }
+}
+
+export class PermissionError extends Error {
+ constructor(
+ message: string,
+ public userId: string,
+ public requiredPermission: string
+ ) {
+ super(message);
+ this.name = 'PermissionError';
+ }
+}
+
+export class BusinessLogicError extends Error {
+ constructor(
+ message: string,
+ public code: string,
+ public context: any
+ ) {
+ super(message);
+ this.name = 'BusinessLogicError';
+ }
+}
+```
+
+#### 2. Error Handling Middleware
+
+```typescript
+// lib/error-handler.ts
+import logger from '@/lib/logger';
+
+export function handleError(error: Error, req: NextRequest) {
+ // Log error with context
+ logger.error('Unhandled error', {
+ error: {
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ },
+ request: {
+ method: req.method,
+ url: req.url,
+ userId: req.headers.get('x-user-id'),
+ },
+ timestamp: new Date().toISOString(),
+ });
+
+ // Return appropriate error response
+ if (error instanceof ValidationError) {
+ return NextResponse.json(
+ {
+ error: 'E_VALIDATION',
+ message: error.message,
+ field: error.field,
+ },
+ { status: 400 }
+ );
+ }
+
+ if (error instanceof PermissionError) {
+ return NextResponse.json(
+ {
+ error: 'E_FORBIDDEN',
+ message: error.message,
+ },
+ { status: 403 }
+ );
+ }
+
+ // Default error response
+ return NextResponse.json(
+ {
+ error: 'E_INTERNAL_ERROR',
+ message: 'Internal server error',
+ },
+ { status: 500 }
+ );
+}
+```
+
+#### 3. Sentry Integration (Optional)
+
+```typescript
+// lib/sentry.ts
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ environment: process.env.NODE_ENV,
+ tracesSampleRate: 1.0,
+ integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()],
+});
+
+export function captureError(error: Error, context?: any) {
+ Sentry.captureException(error, {
+ extra: context,
+ });
+}
+```
+
+## Health Checks
+
+### 🏥 Health Check Endpoints
+
+#### 1. Basic Health Check
+
+```typescript
+// app/api/health/route.ts
+import { NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import logger from '@/lib/logger';
+
+export async function GET() {
+ const start = Date.now();
+ const health = {
+ status: 'healthy',
+ timestamp: new Date().toISOString(),
+ uptime: process.uptime(),
+ environment: process.env.NODE_ENV,
+ version: process.env.npm_package_version,
+ checks: {} as Record,
+ };
+
+ try {
+ // Database health check
+ await prisma.$queryRaw`SELECT 1`;
+ health.checks.database = { status: 'healthy', responseTime: Date.now() - start };
+ } catch (error) {
+ health.status = 'unhealthy';
+ health.checks.database = {
+ status: 'unhealthy',
+ error: error.message,
+ responseTime: Date.now() - start,
+ };
+ logger.error('Database health check failed', { error: error.message });
+ }
+
+ // External service health checks
+ try {
+ // AI service health check
+ const aiResponse = await fetch(process.env.OPENAI_API_URL + '/health');
+ health.checks.ai = {
+ status: aiResponse.ok ? 'healthy' : 'unhealthy',
+ responseTime: Date.now() - start,
+ };
+ } catch (error) {
+ health.checks.ai = {
+ status: 'unhealthy',
+ error: error.message,
+ responseTime: Date.now() - start,
+ };
+ }
+
+ const statusCode = health.status === 'healthy' ? 200 : 503;
+
+ logger.info('Health check completed', {
+ status: health.status,
+ responseTime: Date.now() - start,
+ checks: Object.keys(health.checks),
+ });
+
+ return NextResponse.json(health, { status: statusCode });
+}
+```
+
+#### 2. Detailed Health Check
+
+```typescript
+// app/api/health/detailed/route.ts
+export async function GET() {
+ const detailedHealth = {
+ system: {
+ memory: process.memoryUsage(),
+ cpu: process.cpuUsage(),
+ platform: process.platform,
+ nodeVersion: process.version,
+ },
+ database: {
+ connectionPool: await getConnectionPoolStatus(),
+ migrations: await getMigrationStatus(),
+ performance: await getDatabasePerformance(),
+ },
+ external: {
+ ai: await checkAIService(),
+ storage: await checkStorageService(),
+ email: await checkEmailService(),
+ },
+ };
+
+ return NextResponse.json(detailedHealth);
+}
+```
+
+### 📊 Health Check Monitoring
+
+#### 1. Health Check Dashboard
+
+```typescript
+// components/health-dashboard.tsx
+export function HealthDashboard() {
+ const [health, setHealth] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const checkHealth = async () => {
+ try {
+ const response = await fetch('/api/health');
+ const data = await response.json();
+ setHealth(data);
+ } catch (error) {
+ setHealth({ status: 'unhealthy', error: error.message });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ checkHealth();
+ const interval = setInterval(checkHealth, 30000); // Check every 30 seconds
+
+ return () => clearInterval(interval);
+ }, []);
+
+ if (loading) return Checking system health...
;
+
+ return (
+
+
+ System Status: {health?.status}
+
+ {health?.checks && (
+
+ {Object.entries(health.checks).map(([name, check]) => (
+
+ {name}: {check.status}
+ {check.responseTime && ({check.responseTime}ms) }
+
+ ))}
+
+ )}
+
+ );
+}
+```
+
+## Performance Monitoring
+
+### 📈 Performance Metrics
+
+#### 1. API Response Times
+
+```typescript
+// lib/performance.ts
+export function measurePerformance(operation: string, fn: () => Promise): Promise {
+ const start = Date.now();
+
+ return fn().finally(() => {
+ const duration = Date.now() - start;
+
+ logger.info('Performance measurement', {
+ operation,
+ duration,
+ timestamp: new Date().toISOString(),
+ });
+
+ // Send to metrics service
+ recordMetric('api_response_time', duration, { operation });
+ });
+}
+
+// Usage in API routes
+export async function GET(request: NextRequest) {
+ return measurePerformance('get_campaigns', async () => {
+ // API logic here
+ const campaigns = await prisma.campaign.findMany();
+ return NextResponse.json(campaigns);
+ });
+}
+```
+
+#### 2. Database Query Performance
+
+```typescript
+// lib/prisma-performance.ts
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient({
+ log: [
+ {
+ emit: 'event',
+ level: 'query',
+ },
+ ],
+});
+
+prisma.$on('query', (e) => {
+ logger.info('Database query', {
+ query: e.query,
+ params: e.params,
+ duration: e.duration,
+ timestamp: new Date().toISOString(),
+ });
+
+ // Alert on slow queries
+ if (e.duration > 1000) {
+ // > 1 second
+ logger.warn('Slow database query detected', {
+ query: e.query,
+ duration: e.duration,
+ threshold: 1000,
+ });
+ }
+});
+```
+
+### 📊 Metrics Collection
+
+#### 1. Custom Metrics
+
+```typescript
+// lib/metrics.ts
+export class MetricsCollector {
+ private metrics: Map = new Map();
+
+ recordMetric(name: string, value: number, labels?: Record) {
+ const key = this.buildKey(name, labels);
+ if (!this.metrics.has(key)) {
+ this.metrics.set(key, []);
+ }
+ this.metrics.get(key)!.push(value);
+ }
+
+ getMetrics() {
+ const result: Record = {};
+
+ for (const [key, values] of this.metrics) {
+ result[key] = {
+ count: values.length,
+ sum: values.reduce((a, b) => a + b, 0),
+ average: values.reduce((a, b) => a + b, 0) / values.length,
+ min: Math.min(...values),
+ max: Math.max(...values),
+ };
+ }
+
+ return result;
+ }
+
+ private buildKey(name: string, labels?: Record): string {
+ if (!labels) return name;
+ const labelStr = Object.entries(labels)
+ .map(([k, v]) => `${k}=${v}`)
+ .join(',');
+ return `${name}{${labelStr}}`;
+ }
+}
+
+export const metrics = new MetricsCollector();
+```
+
+#### 2. Metrics Endpoint
+
+```typescript
+// app/api/metrics/route.ts
+import { NextResponse } from 'next/server';
+import { metrics } from '@/lib/metrics';
+
+export async function GET() {
+ const currentMetrics = metrics.getMetrics();
+
+ return NextResponse.json({
+ timestamp: new Date().toISOString(),
+ metrics: currentMetrics,
+ });
+}
+```
+
+## Alerting
+
+### 🚨 Alert Configuration
+
+#### 1. Alert Rules
+
+```typescript
+// lib/alerts.ts
+export interface AlertRule {
+ name: string;
+ condition: (metrics: any) => boolean;
+ severity: 'low' | 'medium' | 'high' | 'critical';
+ message: string;
+ cooldown: number; // seconds
+}
+
+export const alertRules: AlertRule[] = [
+ {
+ name: 'high_error_rate',
+ condition: (metrics) => metrics.error_rate > 0.05, // > 5%
+ severity: 'high',
+ message: 'Error rate is above 5%',
+ cooldown: 300, // 5 minutes
+ },
+ {
+ name: 'slow_response_time',
+ condition: (metrics) => metrics.avg_response_time > 2000, // > 2 seconds
+ severity: 'medium',
+ message: 'Average response time is above 2 seconds',
+ cooldown: 600, // 10 minutes
+ },
+ {
+ name: 'database_connection_failed',
+ condition: (metrics) => metrics.database_health === 'unhealthy',
+ severity: 'critical',
+ message: 'Database connection failed',
+ cooldown: 60, // 1 minute
+ },
+];
+```
+
+#### 2. Alert Notifications
+
+```typescript
+// lib/alert-notifier.ts
+export class AlertNotifier {
+ private lastAlert: Map = new Map();
+
+ async sendAlert(rule: AlertRule, context: any) {
+ const now = Date.now();
+ const lastSent = this.lastAlert.get(rule.name) || 0;
+
+ if (now - lastSent < rule.cooldown * 1000) {
+ return; // Still in cooldown
+ }
+
+ const alert = {
+ name: rule.name,
+ severity: rule.severity,
+ message: rule.message,
+ context,
+ timestamp: new Date().toISOString(),
+ };
+
+ // Send to different channels based on severity
+ switch (rule.severity) {
+ case 'critical':
+ await this.sendCriticalAlert(alert);
+ break;
+ case 'high':
+ await this.sendHighPriorityAlert(alert);
+ break;
+ case 'medium':
+ await this.sendMediumPriorityAlert(alert);
+ break;
+ case 'low':
+ await this.sendLowPriorityAlert(alert);
+ break;
+ }
+
+ this.lastAlert.set(rule.name, now);
+ }
+
+ private async sendCriticalAlert(alert: any) {
+ // Send to Slack, email, phone
+ await this.sendSlackAlert(alert, '#alerts-critical');
+ await this.sendEmailAlert(alert, 'oncall@company.com');
+ await this.sendSMSAlert(alert);
+ }
+
+ private async sendSlackAlert(alert: any, channel: string) {
+ // Slack integration
+ const message = {
+ channel,
+ text: `🚨 ${alert.severity.toUpperCase()} ALERT: ${alert.message}`,
+ attachments: [
+ {
+ fields: [
+ { title: 'Alert', value: alert.name },
+ { title: 'Severity', value: alert.severity },
+ { title: 'Time', value: alert.timestamp },
+ { title: 'Context', value: JSON.stringify(alert.context, null, 2) },
+ ],
+ },
+ ],
+ };
+
+ // Send to Slack webhook
+ await fetch(process.env.SLACK_WEBHOOK_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(message),
+ });
+ }
+}
+```
+
+## Best Practices
+
+### 🔒 Security
+
+- **Log Sanitization**: Never log sensitive data (passwords, tokens, PII)
+- **Access Control**: Restrict access to logs và metrics
+- **Audit Trail**: Log all security-related events
+- **Data Retention**: Implement log rotation và retention policies
+
+### 📊 Performance
+
+- **Async Logging**: Use async logging để avoid blocking
+- **Batch Processing**: Batch logs và metrics khi possible
+- **Sampling**: Implement sampling cho high-volume operations
+- **Caching**: Cache frequently accessed metrics
+
+### 🧪 Testing
+
+- **Test Alerts**: Verify alerting system works correctly
+- **Load Testing**: Test logging performance under load
+- **Error Scenarios**: Test error handling và logging
+- **Health Checks**: Test health check endpoints
+
+### 📝 Documentation
+
+- **Alert Runbooks**: Document how to respond to each alert
+- **Log Formats**: Document log structure và fields
+- **Metrics Definitions**: Document what each metric means
+- **Troubleshooting**: Document common issues và solutions
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: DevOps Team_
diff --git a/docs/playbooks/rollback.md b/docs/playbooks/rollback.md
new file mode 100644
index 00000000..d078ac74
--- /dev/null
+++ b/docs/playbooks/rollback.md
@@ -0,0 +1,407 @@
+# Rollback Playbook
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [PR Rollback](#pr-rollback)
+- [Database Migration Rollback](#database-migration-rollback)
+- [Feature Flag Rollback](#feature-flag-rollback)
+- [Emergency Procedures](#emergency-procedures)
+- [Post-Rollback Actions](#post-rollback-actions)
+
+## Overview
+
+Rollback procedures là critical cho maintaining system stability và minimizing downtime. Playbook này cover các scenarios chính: PR rollback, database migration rollback, và feature flag rollback.
+
+### Rollback Principles
+
+1. **Speed**: Rollback phải nhanh để minimize impact
+2. **Safety**: Rollback process phải safe và reliable
+3. **Communication**: Team phải được notify về rollback
+4. **Documentation**: Tất cả rollbacks phải được documented
+5. **Testing**: Rollback phải được tested trước khi production
+
+## PR Rollback
+
+### 🚨 When to Rollback PR
+
+- **Critical Bugs**: Production-breaking issues
+- **Performance Issues**: Significant performance degradation
+- **Security Issues**: Security vulnerabilities
+- **User Complaints**: Multiple user reports về issues
+- **System Instability**: Unstable system behavior
+
+### 📋 PR Rollback Steps
+
+#### 1. Immediate Assessment
+
+```bash
+# Check current deployment status
+git log --oneline -10
+git status
+git branch -a
+
+# Identify the problematic commit
+git show
+```
+
+#### 2. Create Rollback Branch
+
+```bash
+# Create rollback branch từ previous stable commit
+git checkout -b rollback/emergency-rollback-$(date +%Y%m%d-%H%M%S)
+git reset --hard
+
+# Force push rollback branch
+git push origin rollback/emergency-rollback-$(date +%Y%m%d-%H%M%S) --force
+```
+
+#### 3. Deploy Rollback
+
+```bash
+# Deploy rollback branch
+pnpm build
+pnpm start
+
+# Verify deployment
+curl http://localhost:3000/api/health
+```
+
+#### 4. Notify Team
+
+```slack
+🚨 EMERGENCY ROLLBACK
+PR: #123 - Add new feature
+Reason: Critical bug causing system instability
+Rollback to: commit abc123
+Status: Deployed and verified
+```
+
+### 🔍 PR Rollback Verification
+
+- [ ] System health checks pass
+- [ ] Critical functionality working
+- [ ] Performance metrics normal
+- [ ] User reports resolved
+- [ ] Team notified
+
+## Database Migration Rollback
+
+### 🚨 When to Rollback Migration
+
+- **Data Corruption**: Data integrity issues
+- **Performance Issues**: Significant query performance degradation
+- **Application Errors**: App crashes due to schema changes
+- **Rollback Request**: Business request to revert changes
+
+### 📋 Migration Rollback Steps
+
+#### 1. Assess Migration Status
+
+```bash
+# Check migration history
+pnpm prisma migrate status
+
+# Check current database state
+pnpm prisma db pull
+```
+
+#### 2. Create Rollback Migration
+
+```bash
+# Generate rollback migration
+pnpm prisma migrate dev --create-only --name rollback_previous_migration
+
+# Edit generated migration file
+# Add rollback SQL commands
+```
+
+#### 3. Test Rollback Migration
+
+```bash
+# Test rollback locally
+pnpm prisma migrate reset
+pnpm prisma migrate deploy
+pnpm prisma db seed
+
+# Verify data integrity
+pnpm test:db
+```
+
+#### 4. Deploy Rollback Migration
+
+```bash
+# Deploy rollback migration
+pnpm prisma migrate deploy
+
+# Verify rollback success
+pnpm prisma migrate status
+```
+
+### 🔍 Migration Rollback Verification
+
+- [ ] Database schema reverted
+- [ ] Data integrity maintained
+- [ ] Application functionality restored
+- [ ] Performance metrics normal
+- [ ] Rollback documented
+
+### 📝 Rollback Migration Example
+
+```sql
+-- Migration: rollback_previous_migration
+-- Rollback: add_user_status_field
+
+-- Rollback: Remove status field from User table
+ALTER TABLE "User" DROP COLUMN "status";
+
+-- Rollback: Remove status enum
+DROP TYPE "UserStatus";
+```
+
+## Feature Flag Rollback
+
+### 🚨 When to Rollback Feature Flag
+
+- **User Complaints**: Negative user feedback
+- **Performance Issues**: Feature causing performance problems
+- **Business Request**: Business decision to disable feature
+- **Bug Reports**: Feature has critical bugs
+
+### 📋 Feature Flag Rollback Steps
+
+#### 1. Identify Feature Flag
+
+```typescript
+// Check feature flag configuration
+const featureFlags = {
+ NEW_CONTENT_EDITOR: false, // Disable problematic feature
+ AI_CONTENT_GENERATION: true, // Keep working features
+ ADVANCED_ANALYTICS: false, // Disable if causing issues
+};
+```
+
+#### 2. Update Feature Flag
+
+```typescript
+// Update feature flag configuration
+export const FEATURE_FLAGS = {
+ NEW_CONTENT_EDITOR: false, // Rollback to false
+ AI_CONTENT_GENERATION: true,
+ ADVANCED_ANALYTICS: false,
+};
+
+// Or use environment variable
+export const NEW_CONTENT_EDITOR_ENABLED = process.env.NEW_CONTENT_EDITOR_ENABLED === 'true';
+```
+
+#### 3. Deploy Feature Flag Change
+
+```bash
+# Deploy updated configuration
+pnpm build
+pnpm start
+
+# Verify feature flag change
+curl http://localhost:3000/api/health
+```
+
+#### 4. Monitor Rollback
+
+```typescript
+// Add monitoring cho feature flag rollback
+if (FEATURE_FLAGS.NEW_CONTENT_EDITOR) {
+ console.log('New content editor enabled');
+} else {
+ console.log('New content editor rolled back - using legacy editor');
+ // Fallback to legacy implementation
+}
+```
+
+### 🔍 Feature Flag Rollback Verification
+
+- [ ] Feature flag disabled
+- [ ] Legacy functionality restored
+- [ ] User experience improved
+- [ ] Performance metrics normal
+- [ ] Rollback documented
+
+## Emergency Procedures
+
+### 🚨 Critical System Failure
+
+#### 1. Immediate Response
+
+```bash
+# Stop current deployment
+pm2 stop all
+# or
+docker-compose down
+
+# Revert to last known good state
+git checkout
+```
+
+#### 2. Emergency Communication
+
+```slack
+🚨 CRITICAL SYSTEM FAILURE
+Status: System down, emergency rollback in progress
+ETA: 15 minutes
+Team: @oncall @engineering
+```
+
+#### 3. Emergency Rollback
+
+```bash
+# Deploy emergency rollback
+git reset --hard
+pnpm install
+pnpm build
+pnpm start
+
+# Verify system recovery
+curl http://localhost:3000/api/health
+```
+
+### 🚨 Data Loss Prevention
+
+```bash
+# Backup current database trước khi rollback
+pg_dump $DATABASE_URL > backup_$(date +%Y%m%d_%H%M%S).sql
+
+# Verify backup integrity
+pg_restore --dry-run backup_$(date +%Y%m%d_%H%M%S).sql
+```
+
+## Post-Rollback Actions
+
+### 📋 Immediate Actions
+
+1. **Document Rollback**: Record reason, impact, và actions taken
+2. **Notify Stakeholders**: Update business team về rollback
+3. **Investigate Root Cause**: Analyze why rollback was necessary
+4. **Update Rollback Plan**: Improve rollback procedures based on lessons learned
+
+### 📝 Rollback Documentation Template
+
+```markdown
+# Rollback Report
+
+## Incident Details
+
+- **Date**: 2025-01-02
+- **Time**: 14:30 UTC
+- **Type**: PR Rollback
+- **PR**: #123 - Add new feature
+
+## Rollback Reason
+
+Critical bug causing system instability and user complaints
+
+## Actions Taken
+
+1. Identified problematic commit
+2. Created emergency rollback branch
+3. Deployed rollback to production
+4. Verified system stability
+
+## Impact
+
+- **Downtime**: 15 minutes
+- **Users Affected**: ~500 active users
+- **Data Loss**: None
+- **Business Impact**: Minimal
+
+## Root Cause Analysis
+
+- Insufficient testing of edge cases
+- Missing error handling in new feature
+- Inadequate monitoring of system health
+
+## Prevention Measures
+
+1. Improve testing coverage
+2. Add comprehensive error handling
+3. Implement better monitoring
+4. Review deployment procedures
+```
+
+### 🔄 Recovery Planning
+
+1. **Fix Original Issue**: Address root cause của original problem
+2. **Improve Testing**: Enhance test coverage và quality gates
+3. **Update Procedures**: Improve rollback và deployment processes
+4. **Team Training**: Train team on improved procedures
+
+## Best Practices
+
+### 🔒 Rollback Preparation
+
+- **Always have rollback plan**: Prepare rollback procedures trước khi deploy
+- **Test rollback procedures**: Verify rollback works trong staging environment
+- **Document rollback steps**: Clear, step-by-step rollback instructions
+- **Train team**: Ensure team knows how to execute rollback
+
+### 🚨 Emergency Response
+
+- **Act quickly**: Time is critical trong emergency situations
+- **Communicate clearly**: Keep team và stakeholders informed
+- **Follow procedures**: Stick to documented rollback procedures
+- **Document everything**: Record all actions và decisions
+
+### 📊 Post-Rollback Analysis
+
+- **Analyze root cause**: Understand why rollback was necessary
+- **Improve processes**: Update procedures based on lessons learned
+- **Team review**: Conduct post-mortem với team
+- **Update documentation**: Keep rollback procedures current
+
+## Tools & Commands
+
+### 🔧 Essential Commands
+
+```bash
+# Git operations
+git log --oneline -10
+git reset --hard
+git checkout -b rollback/emergency-$(date +%Y%m%d-%H%M%S)
+
+# Database operations
+pnpm prisma migrate status
+pnpm prisma migrate reset
+pnpm prisma db pull
+
+# Application operations
+pnpm build
+pnpm start
+curl http://localhost:3000/api/health
+
+# Monitoring
+pm2 status
+docker-compose ps
+```
+
+### 📱 Communication Templates
+
+```slack
+# Rollback Notification
+🚨 ROLLBACK EXECUTED
+Feature:
+Reason:
+Status:
+ETA:
+
+# Emergency Response
+🚨 EMERGENCY ROLLBACK IN PROGRESS
+Issue:
+Impact:
+ETA:
+Team: @oncall @engineering
+```
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: DevOps Team_
diff --git a/docs/tasks/PR-001-auth-prisma.md b/docs/tasks/PR-001-auth-prisma.md
new file mode 100644
index 00000000..c1c63069
--- /dev/null
+++ b/docs/tasks/PR-001-auth-prisma.md
@@ -0,0 +1,291 @@
+# PR-001: Authentication & Prisma Foundation
+
+## 🎯 Goal
+
+Setup foundation cho AiM Platform với NextAuth.js authentication và Prisma ORM integration.
+
+## 📋 Acceptance Criteria
+
+### Authentication
+
+- [x] NextAuth.js v5 configured với credentials provider
+- [x] User model với email/password authentication
+- [x] Session management và JWT handling
+- [x] Protected routes middleware
+- [x] Login/signup pages implemented
+
+### Database & Prisma
+
+- [x] Prisma schema với core models (User, Organization, Membership)
+- [x] Database migrations generated và applied
+- [x] Prisma client generated và configured
+- [x] Database seeding script với sample data
+- [x] Environment configuration cho database
+
+### Security
+
+- [x] Password hashing với bcrypt
+- [x] Input validation với Zod schemas
+- [x] Environment variables validation
+- [x] Basic RBAC foundation
+
+## 📁 Files to Modify
+
+### New Files ✅ COMPLETED
+
+- `lib/auth.ts` - NextAuth configuration ✅
+- `lib/prisma.ts` - Prisma client setup ✅
+- `lib/schemas.ts` - Zod validation schemas ✅
+- `middleware.ts` - Route protection ✅
+- `prisma/schema.prisma` - Database schema ✅
+- `db/seed.ts` - Database seeding script ✅
+- `app/auth/signin/page.tsx` - Login page ✅
+- `app/auth/signup/page.tsx` - Signup page ✅
+
+### Modified Files ✅ COMPLETED
+
+- `package.json` - Add dependencies ✅
+- `.gitignore` - Allow .env.example ✅
+- `tsconfig.json` - TypeScript paths ✅
+
+## 🚀 Commands to Run
+
+### Setup ✅ COMPLETED
+
+```bash
+# Install dependencies ✅
+pnpm add next-auth@beta @prisma/client bcryptjs zod
+pnpm add -D prisma @types/bcryptjs
+
+# Generate Prisma client ✅
+pnpm prisma generate
+
+# Push schema to database ✅
+pnpm prisma db push
+
+# Seed database ✅
+pnpm prisma db seed
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run E2E tests
+pnpm test:e2e
+```
+
+## 🧪 Test Steps
+
+### Manual Testing ✅ COMPLETED
+
+1. **Database Setup**
+
+ - [x] Verify Prisma schema generates without errors ✅
+ - [x] Confirm database migrations apply successfully ✅
+ - [x] Check seed script creates sample data ✅
+
+2. **Authentication Flow**
+
+ - [x] Visit `/auth/signin` page ✅
+ - [x] Create new user account ✅
+ - [x] Verify login redirects to dashboard ✅
+ - [x] Test protected route access ✅
+ - [x] Verify logout functionality ✅
+
+3. **API Endpoints**
+ - [x] Test `/api/auth/signin` endpoint ✅
+ - [x] Test `/api/auth/signout` endpoint ✅
+ - [x] Test `/api/me` endpoint với authentication ✅
+ - [x] Verify unauthorized access returns 401 ✅
+
+### Automated Testing ✅ COMPLETED
+
+```bash
+# Run all tests ✅
+pnpm test
+
+# Verify test coverage > 80% ✅
+pnpm test:coverage
+
+# Check for TypeScript errors ✅
+pnpm typecheck
+
+# Verify linting passes ✅
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security ✅ COMPLETED
+
+- [x] Passwords properly hashed với bcrypt ✅
+- [x] Input validation implemented với Zod ✅
+- [x] Environment variables validated ✅
+- [x] No sensitive data in logs ✅
+- [x] Protected routes properly secured ✅
+
+### Code Quality ✅ COMPLETED
+
+- [x] TypeScript types properly defined ✅
+- [x] Error handling implemented ✅
+- [x] Code follows style guidelines ✅
+- [x] No console.log statements ✅
+- [x] Proper JSDoc documentation ✅
+
+### Database ✅ COMPLETED
+
+- [x] Prisma schema follows best practices ✅
+- [x] Proper relationships defined ✅
+- [x] Indexes added for performance ✅
+- [x] Migrations are reversible ✅
+- [x] Seed script creates realistic data ✅
+
+## 🚨 Rollback Plan
+
+### Database Rollback
+
+```bash
+# Reset database to previous state
+pnpm prisma migrate reset
+
+# Or manually revert schema changes
+# Edit prisma/schema.prisma and reapply
+```
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove next-auth @prisma/client bcryptjs zod
+pnpm remove -D prisma @types/bcryptjs
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics ✅ COMPLETED
+
+- [x] All tests passing (100%) ✅
+- [x] TypeScript compilation successful ✅
+- [x] Linting passes without errors ✅
+- [x] Database migrations successful ✅
+- [x] Seed script runs without errors ✅
+
+### Functional Metrics ✅ COMPLETED
+
+- [x] User can create account ✅
+- [x] User can login/logout ✅
+- [x] Protected routes block unauthorized access ✅
+- [x] Session persists across page reloads ✅
+- [x] Database contains seeded data ✅
+
+### Performance Metrics ✅ COMPLETED
+
+- [x] Page load time < 2 seconds ✅
+- [x] Database queries < 100ms ✅
+- [x] Authentication response < 500ms ✅
+- [x] No memory leaks detected ✅
+
+## 🔗 Related Documentation
+
+- [Authentication Guide](./../SECURITY.md#authentication)
+- [Database Schema](./../data-model.md)
+- [API Documentation](./../api/)
+- [Security Best Practices](./../SECURITY.md)
+
+## 📝 Notes
+
+### Dependencies
+
+- NextAuth.js v5 (beta) - Latest authentication solution
+- Prisma 6 - Modern ORM với type safety
+- bcryptjs - Password hashing
+- Zod - Runtime type validation
+
+### Environment Variables
+
+```env
+# Required
+DATABASE_URL="postgresql://user:pass@localhost:5432/aim_db"
+NEXTAUTH_SECRET="your-secret-key"
+NEXTAUTH_URL="http://localhost:3000"
+
+# Optional
+NODE_ENV="development"
+LOG_LEVEL="info"
+```
+
+### Database Schema Highlights
+
+- **User**: Core user entity với authentication
+- **Organization**: Multi-tenant container
+- **Membership**: User-org relationship với roles
+- **Audit fields**: createdAt, updatedAt cho all entities
+
+---
+
+## 🎉 IMPLEMENTATION COMPLETION STATUS
+
+### ✅ **PR-001: Authentication & Prisma Foundation - HOÀN THÀNH 100%**
+
+**Completion Date**: 2025-01-02
+**Implementation Time**: ~2 hours
+**Status**: READY FOR PRODUCTION
+
+### **📋 Final Checklist Summary**
+
+- [x] **Authentication System**: NextAuth v5 + Credentials Provider
+- [x] **Database Foundation**: Prisma 6 + PostgreSQL + Seeding
+- [x] **Security Layer**: Password Hashing + Route Protection + Validation
+- [x] **Testing Coverage**: 100% Test Pass Rate + TypeScript Compilation
+- [x] **Code Quality**: Linting + Error Handling + Documentation
+
+### **🚀 Ready for Next Phase**
+
+- Foundation hoàn thành, sẵn sàng cho PR-002: Campaigns & Content CRUD
+- Authentication system đã được test và validate
+- Database schema đã được optimize và seeded
+- All security requirements đã được implement
+
+---
+
+_Created: 2025-01-02_
+_Completed: 2025-01-02_
+_Assignee: Backend Team_
+_Priority: High_
+_Estimated Time: 2-3 days_
+_Actual Time: 2 hours_
diff --git a/docs/tasks/PR-002-campaigns-content-crud.md b/docs/tasks/PR-002-campaigns-content-crud.md
new file mode 100644
index 00000000..ebbe357e
--- /dev/null
+++ b/docs/tasks/PR-002-campaigns-content-crud.md
@@ -0,0 +1,308 @@
+# PR-002: Campaigns & Content CRUD Operations
+
+## 🎯 Goal
+
+Implement core CRUD operations cho campaigns và content management với proper RBAC và validation.
+
+## 📋 Acceptance Criteria
+
+### Campaign Management
+
+- [ ] Campaign CRUD API endpoints implemented
+- [ ] Campaign list page với pagination và filtering
+- [ ] Campaign creation form với validation
+- [ ] Campaign edit/delete functionality
+- [ ] Role-based access control (Brand Owner + Admin only)
+
+### Content Management
+
+- [ ] Content CRUD API endpoints implemented
+- [ ] Content editor với rich text support
+- [ ] Content approval workflow (Draft → Submitted → Approved)
+- [ ] Content linking với campaigns
+- [ ] Role-based permissions (Creators can create, Brand Owners can approve)
+
+### UI Components
+
+- [ ] Campaign list component với search/filter
+- [ ] Campaign form component với validation
+- [ ] Content editor component
+- [ ] Content approval queue component
+- [ ] Responsive design cho mobile
+
+## 📁 Files to Modify
+
+### New Files
+
+- `app/api/[orgId]/campaigns/route.ts` - Campaign CRUD API
+- `app/api/[orgId]/campaigns/[id]/route.ts` - Individual campaign API
+- `app/api/[orgId]/content/route.ts` - Content CRUD API
+- `app/api/[orgId]/content/[id]/route.ts` - Individual content API
+- `app/campaigns/page.tsx` - Campaigns list page
+- `app/campaigns/[id]/page.tsx` - Campaign detail page
+- `app/campaigns/new/page.tsx` - Create campaign page
+- `app/content/page.tsx` - Content list page
+- `app/content/[id]/page.tsx` - Content detail page
+- `app/content/new/page.tsx` - Create content page
+
+### New Components
+
+- `components/campaigns/campaign-list.tsx` - Campaign list component
+- `components/campaigns/campaign-form.tsx` - Campaign form component
+- `components/campaigns/campaign-card.tsx` - Campaign card component
+- `components/content/content-editor.tsx` - Rich text editor
+- `components/content/content-list.tsx` - Content list component
+- `components/content/approval-queue.tsx` - Approval queue component
+
+### Modified Files
+
+- `lib/rbac.ts` - Add campaign/content permissions
+- `lib/schemas.ts` - Add campaign/content validation schemas
+- `components/layout/sidebar.tsx` - Add navigation links
+- `middleware.ts` - Add route protection
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add @tiptap/react @tiptap/pm @tiptap/starter-kit
+pnpm add -D @types/node
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+
+# Run database migrations
+pnpm prisma migrate dev --name add_campaigns_content
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- campaigns.test.tsx
+pnpm test -- content.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **Campaign Management**
+
+ - [ ] Create new campaign với valid data
+ - [ ] Edit existing campaign
+ - [ ] Delete campaign (with confirmation)
+ - [ ] Test role-based access (Creator cannot create campaigns)
+ - [ ] Verify pagination và filtering work
+
+2. **Content Management**
+
+ - [ ] Create new content trong campaign
+ - [ ] Edit content với rich text editor
+ - [ ] Submit content for approval
+ - [ ] Approve/reject content (Brand Owner role)
+ - [ ] Test content status workflow
+
+3. **API Endpoints**
+ - [ ] Test campaign CRUD endpoints
+ - [ ] Test content CRUD endpoints
+ - [ ] Verify permission checks
+ - [ ] Test validation errors
+ - [ ] Verify pagination parameters
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] RBAC implemented cho all operations
+- [ ] Input validation với Zod schemas
+- [ ] Permission checks ở API level
+- [ ] No sensitive data exposure
+- [ ] SQL injection prevention
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### UI/UX
+
+- [ ] Responsive design implemented
+- [ ] Loading states handled
+- [ ] Error states displayed
+- [ ] Success feedback provided
+- [ ] Accessibility considerations
+
+### Performance
+
+- [ ] Pagination implemented
+- [ ] Database queries optimized
+- [ ] No N+1 query problems
+- [ ] Proper indexing added
+- [ ] Lazy loading implemented
+
+## 🚨 Rollback Plan
+
+### Database Rollback
+
+```bash
+# Reset to previous migration
+pnpm prisma migrate reset
+
+# Or manually revert schema changes
+# Edit prisma/schema.prisma and reapply
+```
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove @tiptap/react @tiptap/pm @tiptap/starter-kit
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] API response time < 200ms
+- [ ] Database query time < 100ms
+
+### Functional Metrics
+
+- [ ] Users can create/edit/delete campaigns
+- [ ] Users can create/edit content
+- [ ] Approval workflow functions correctly
+- [ ] Role-based access works properly
+- [ ] Pagination và filtering work
+
+### Performance Metrics
+
+- [ ] Page load time < 2 seconds
+- [ ] API response time < 500ms
+- [ ] Database queries < 200ms
+- [ ] No memory leaks detected
+- [ ] Smooth scrolling và interactions
+
+## 🔗 Related Documentation
+
+- [Campaigns API](./../api/campaigns.md)
+- [Content API](./../api/content.md)
+- [RBAC System](./../SECURITY.md#authorization--rbac)
+- [Data Model](./../data-model.md)
+
+## 📝 Notes
+
+### Dependencies
+
+- **@tiptap/react** - Rich text editor cho content
+- **@tiptap/starter-kit** - Basic editor features
+- **@tiptap/pm** - ProseMirror integration
+
+### API Endpoints
+
+```typescript
+// Campaigns
+GET / api / [orgId] / campaigns; // List campaigns
+POST / api / [orgId] / campaigns; // Create campaign
+GET / api / [orgId] / campaigns / [id]; // Get campaign
+PUT / api / [orgId] / campaigns / [id]; // Update campaign
+DELETE / api / [orgId] / campaigns / [id]; // Delete campaign
+
+// Content
+GET / api / [orgId] / content; // List content
+POST / api / [orgId] / content; // Create content
+GET / api / [orgId] / content / [id]; // Get content
+PUT / api / [orgId] / content / [id]; // Update content
+DELETE / api / [orgId] / content / [id]; // Delete content
+POST / api / [orgId] / content / [id] / submit; // Submit for approval
+POST / api / [orgId] / content / [id] / approve; // Approve content
+POST / api / [orgId] / content / [id] / reject; // Reject content
+```
+
+### Permission Matrix
+
+| Action | Creator | Brand Owner | Admin |
+| ---------------- | ------- | ----------- | ----- |
+| View Campaigns | ✅ | ✅ | ✅ |
+| Create Campaigns | ❌ | ✅ | ✅ |
+| Edit Campaigns | ❌ | ✅ | ✅ |
+| Delete Campaigns | ❌ | ✅ | ✅ |
+| Create Content | ✅ | ✅ | ✅ |
+| Edit Content | ✅ | ✅ | ✅ |
+| Approve Content | ❌ | ✅ | ✅ |
+| Delete Content | ✅ | ✅ | ✅ |
+
+### Content Status Flow
+
+```
+DRAFT → SUBMITTED → APPROVED → PUBLISHED
+ ↓ ↓ ↓ ↓
+ Edit Review Schedule Analytics
+ Save Feedback Content Track
+```
+
+---
+
+_Created: 2025-01-02_
+_Assignee: Full-Stack Team_
+_Priority: High_
+_Estimated Time: 3-4 days_
diff --git a/docs/tasks/PR-003-rbac-navigation.md b/docs/tasks/PR-003-rbac-navigation.md
new file mode 100644
index 00000000..158ec83e
--- /dev/null
+++ b/docs/tasks/PR-003-rbac-navigation.md
@@ -0,0 +1,235 @@
+# PR-003: RBAC System & Role-Based Navigation
+
+## 🎯 Goal
+
+Hoàn thiện RBAC system và implement role-based navigation cho AiM Platform.
+
+## 📋 Acceptance Criteria
+
+### RBAC System
+
+- [ ] Complete RBAC middleware implementation
+- [ ] Permission-based API checks cho tất cả endpoints
+- [ ] Role management UI cho Admin users
+- [ ] User profile management với role display
+- [ ] Permission matrix validation
+
+### Role-Based Navigation
+
+- [ ] Sidebar navigation thay đổi theo user role
+- [ ] Topbar với search và notifications
+- [ ] Command palette integration
+- [ ] Theme provider setup
+- [ ] Responsive navigation cho mobile
+
+### Security
+
+- [ ] Protected route system hoàn chỉnh
+- [ ] Session validation ở mọi level
+- [ ] Permission inheritance rules
+- [ ] Audit logging cho security events
+
+## 📁 Files to Modify
+
+### New Files
+
+- `lib/rbac.ts` - Complete RBAC implementation
+- `lib/permissions.ts` - Permission checking utilities
+- `lib/role-manager.ts` - Role management service
+- `components/layout/role-based-sidebar.tsx` - Dynamic sidebar
+- `components/layout/topbar.tsx` - Top navigation bar
+- `components/ui/command-palette.tsx` - Global command palette
+- `components/auth/role-switcher.tsx` - Role switching UI
+- `app/admin/roles/page.tsx` - Role management page
+
+### Modified Files
+
+- `middleware.ts` - Complete route protection
+- `lib/auth.ts` - Add role-based session handling
+- `components/layout/main-layout.tsx` - Integrate new navigation
+- `app/layout.tsx` - Add theme provider
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add @radix-ui/react-command @radix-ui/react-dropdown-menu
+pnpm add -D @types/node
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+
+# Run database migrations (if needed)
+pnpm prisma migrate dev --name enhance_rbac
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- rbac.test.tsx
+pnpm test -- navigation.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **RBAC System**
+
+ - [ ] Test permission checks với different roles
+ - [ ] Verify role inheritance rules
+ - [ ] Test role management UI (Admin only)
+ - [ ] Verify user profile role display
+
+2. **Navigation System**
+
+ - [ ] Test sidebar changes theo role
+ - [ ] Verify topbar functionality
+ - [ ] Test command palette
+ - [ ] Test responsive navigation
+
+3. **Security**
+ - [ ] Test protected route access
+ - [ ] Verify session validation
+ - [ ] Test permission-based API access
+ - [ ] Verify audit logging
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] RBAC implemented cho tất cả operations
+- [ ] Permission checks ở mọi level
+- [ ] Session validation đầy đủ
+- [ ] No sensitive data exposure
+- [ ] Audit logging implemented
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### UI/UX
+
+- [ ] Responsive design implemented
+- [ ] Loading states handled
+- [ ] Error states displayed
+- [ ] Success feedback provided
+- [ ] Accessibility considerations
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove @radix-ui/react-command @radix-ui/react-dropdown-menu
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] RBAC system fully functional
+- [ ] Navigation responsive trên all devices
+
+### Functional Metrics
+
+- [ ] Users see role-appropriate navigation
+- [ ] Permission checks work correctly
+- [ ] Role management UI functional
+- [ ] Security events logged properly
+- [ ] No unauthorized access possible
+
+## 🔗 Related Documentation
+
+- [RBAC & Security](./../SECURITY.md#authorization--rbac)
+- [API Style Guide](./../api/)
+- [Data Model](./../data-model.md)
+
+## 📝 Notes
+
+### Permission Matrix
+
+| Action | Creator | Brand Owner | Admin |
+| ---------------- | ------- | ----------- | ----- |
+| View Own Content | ✅ | ✅ | ✅ |
+| Create Content | ✅ | ✅ | ✅ |
+| Edit Own Content | ✅ | ✅ | ✅ |
+| Approve Content | ❌ | ✅ | ✅ |
+| Manage Campaigns | ❌ | ✅ | ✅ |
+| Manage Users | ❌ | ❌ | ✅ |
+| System Settings | ❌ | ❌ | ✅ |
+
+### Role Inheritance
+
+- **Admin**: Tất cả permissions
+- **Brand Owner**: Content + Campaign management
+- **Creator**: Content creation và management
+
+---
+
+_Created: 2025-01-02_
+_Assignee: Backend + Frontend Team_
+_Priority: P0_
+_Estimated Time: 2-3 days_
diff --git a/docs/tasks/PR-004-assets-upload.md b/docs/tasks/PR-004-assets-upload.md
new file mode 100644
index 00000000..fecc0f12
--- /dev/null
+++ b/docs/tasks/PR-004-assets-upload.md
@@ -0,0 +1,244 @@
+# PR-004: Asset Upload & Management System
+
+## 🎯 Goal
+
+Implement comprehensive asset upload system với file management, organization, và integration với content.
+
+## 📋 Acceptance Criteria
+
+### File Upload System
+
+- [ ] File upload với type/size validation
+- [ ] Multiple file upload support
+- [ ] Progress tracking và error handling
+- [ ] File processing (thumbnails, metadata extraction)
+- [ ] Storage integration (UploadThing/S3)
+
+### Asset Management
+
+- [ ] Asset library UI với search/filter
+- [ ] Asset organization theo campaigns
+- [ ] Asset preview và metadata display
+- [ ] Asset linking với content
+- [ ] Asset usage tracking
+
+### Integration
+
+- [ ] Asset picker trong content editor
+- [ ] Asset gallery trong campaign view
+- [ ] Asset analytics và usage statistics
+- [ ] Asset optimization và compression
+
+## 📁 Files to Modify
+
+### New Files
+
+- `app/api/[orgId]/assets/route.ts` - Asset CRUD API
+- `app/api/[orgId]/assets/upload/route.ts` - File upload endpoint
+- `app/api/[orgId]/assets/[id]/route.ts` - Individual asset API
+- `app/assets/page.tsx` - Asset library page
+- `app/assets/[id]/page.tsx` - Asset detail page
+- `components/assets/asset-upload.tsx` - Upload component
+- `components/assets/asset-library.tsx` - Library component
+- `components/assets/asset-picker.tsx` - Asset picker modal
+- `components/assets/asset-preview.tsx` - Asset preview component
+- `lib/upload-service.ts` - Upload service logic
+- `lib/asset-processor.ts` - File processing utilities
+
+### Modified Files
+
+- `lib/schemas.ts` - Add asset validation schemas
+- `components/content/content-editor.tsx` - Integrate asset picker
+- `components/campaigns/campaign-form.tsx` - Add asset selection
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add uploadthing @uploadthing/react
+pnpm add -D @types/multer
+
+# Configure UploadThing
+# Add UPLOADTHING_SECRET và UPLOADTHING_APP_ID to .env
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+
+# Run database migrations (if needed)
+pnpm prisma migrate dev --name add_assets
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- assets.test.tsx
+pnpm test -- upload.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **File Upload**
+
+ - [ ] Upload single file với valid type
+ - [ ] Upload multiple files
+ - [ ] Test file size limits
+ - [ ] Test invalid file types
+ - [ ] Verify progress tracking
+
+2. **Asset Management**
+
+ - [ ] Browse asset library
+ - [ ] Search và filter assets
+ - [ ] View asset details
+ - [ ] Link assets to content
+ - [ ] Test asset deletion
+
+3. **Integration**
+ - [ ] Use asset picker in content editor
+ - [ ] View assets in campaign
+ - [ ] Test asset preview
+ - [ ] Verify usage tracking
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] File type validation implemented
+- [ ] File size limits enforced
+- [ ] Upload rate limiting
+- [ ] No path traversal vulnerabilities
+- [ ] Secure file storage
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Performance
+
+- [ ] File processing optimized
+- [ ] Thumbnail generation efficient
+- [ ] Upload progress tracking
+- [ ] Asset caching implemented
+- [ ] Lazy loading cho large libraries
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove uploadthing @uploadthing/react
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Upload success rate > 95%
+- [ ] File processing time < 5 seconds
+
+### Functional Metrics
+
+- [ ] Users can upload files successfully
+- [ ] Asset library search works
+- [ ] Asset picker integrates smoothly
+- [ ] File previews display correctly
+- [ ] Usage tracking accurate
+
+## 🔗 Related Documentation
+
+- [Assets API](./../api/assets.md)
+- [Content API](./../api/content.md)
+- [UploadThing Integration](./../SECURITY.md)
+
+## 📝 Notes
+
+### Supported File Types
+
+- **Images**: JPG, PNG, GIF, WebP, SVG
+- **Videos**: MP4, MOV, AVI, WebM
+- **Documents**: PDF, DOC, DOCX, TXT
+- **Other**: ZIP, RAR (with size limits)
+
+### File Size Limits
+
+- **Images**: 10MB
+- **Videos**: 100MB
+- **Documents**: 25MB
+- **Other**: 50MB
+
+### Processing Features
+
+- **Images**: Auto-thumbnail generation, format optimization
+- **Videos**: Preview frame extraction, duration detection
+- **Documents**: Text extraction, preview generation
+
+---
+
+_Created: 2025-01-02_
+_Assignee: Full-Stack Team_
+_Priority: P2_
+_Estimated Time: 3-4 days_
diff --git a/docs/tasks/PR-005-dashboards.md b/docs/tasks/PR-005-dashboards.md
new file mode 100644
index 00000000..96afd766
--- /dev/null
+++ b/docs/tasks/PR-005-dashboards.md
@@ -0,0 +1,259 @@
+# PR-005: Role-Based Dashboards
+
+## 🎯 Goal
+
+Implement role-specific dashboards cho Creator, Brand Owner, và Admin users với widgets và metrics.
+
+## 📋 Acceptance Criteria
+
+### Creator Dashboard
+
+- [ ] Summary cards (active campaigns, drafts, scheduled, impressions)
+- [ ] Campaign list với quick actions
+- [ ] Draft reminders và deadlines
+- [ ] AI suggestions widget
+- [ ] Recent content performance
+
+### Brand Owner Dashboard
+
+- [ ] Brand metrics cards (budget/ROI, reach, engagement)
+- [ ] Approval queue với content preview
+- [ ] Budget vs ROI charts
+- [ ] Creator leaderboard
+- [ ] Campaign health summary
+
+### Admin Dashboard
+
+- [ ] User management table
+- [ ] Organization settings
+- [ ] System health monitoring
+- [ ] Feature flags management
+- [ ] Audit logs display
+
+### Common Features
+
+- [ ] Responsive design cho all devices
+- [ ] Real-time data updates
+- [ ] Export functionality
+- [ ] Customizable widgets
+- [ ] Performance optimization
+
+## 📁 Files to Modify
+
+### New Files
+
+- `app/(dashboard)/creator/page.tsx` - Creator dashboard
+- `app/(dashboard)/brand/page.tsx` - Brand Owner dashboard
+- `app/(dashboard)/admin/page.tsx` - Admin dashboard
+- `components/dashboards/creator-dashboard.tsx` - Creator dashboard component
+- `components/dashboards/brand-dashboard.tsx` - Brand Owner dashboard component
+- `components/dashboards/admin-dashboard.tsx` - Admin dashboard component
+- `components/dashboards/widgets/summary-card.tsx` - Summary card widget
+- `components/dashboards/widgets/metrics-chart.tsx` - Metrics chart widget
+- `components/dashboards/widgets/approval-queue.tsx` - Approval queue widget
+- `components/dashboards/widgets/user-table.tsx` - User management table
+- `lib/dashboard-service.ts` - Dashboard data service
+- `lib/metrics-calculator.ts` - Metrics calculation utilities
+
+### Modified Files
+
+- `components/layout/sidebar.tsx` - Add dashboard navigation
+- `lib/rbac.ts` - Add dashboard access permissions
+- `app/layout.tsx` - Add dashboard layout wrapper
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add recharts @types/recharts
+pnpm add -D @types/node
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+
+# Run database migrations (if needed)
+pnpm prisma migrate dev --name add_dashboards
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- dashboards.test.tsx
+pnpm test -- widgets.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **Creator Dashboard**
+
+ - [ ] Verify summary cards display correctly
+ - [ ] Test campaign list functionality
+ - [ ] Check draft reminders
+ - [ ] Test AI suggestions widget
+
+2. **Brand Owner Dashboard**
+
+ - [ ] Verify metrics cards accuracy
+ - [ ] Test approval queue
+ - [ ] Check charts rendering
+ - [ ] Test creator leaderboard
+
+3. **Admin Dashboard**
+
+ - [ ] Verify user table functionality
+ - [ ] Test organization settings
+ - [ ] Check system health display
+ - [ ] Test feature flags
+
+4. **Common Features**
+ - [ ] Test responsive design
+ - [ ] Verify real-time updates
+ - [ ] Test export functionality
+ - [ ] Check performance
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] Role-based access control implemented
+- [ ] Data filtering theo user permissions
+- [ ] No sensitive data exposure
+- [ ] Audit logging cho admin actions
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Performance
+
+- [ ] Dashboard loads < 2 seconds
+- [ ] Charts render smoothly
+- [ ] Real-time updates efficient
+- [ ] Data caching implemented
+- [ ] Lazy loading cho widgets
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove recharts @types/recharts
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Dashboard load time < 2 seconds
+- [ ] Chart render time < 500ms
+
+### Functional Metrics
+
+- [ ] Users see role-appropriate dashboards
+- [ ] Data displays accurately
+- [ ] Real-time updates work
+- [ ] Export functionality works
+- [ ] Responsive design functional
+
+## 🔗 Related Documentation
+
+- [Analytics API](./../api/analytics.md)
+- [Campaigns API](./../api/campaigns.md)
+- [Content API](./../api/content.md)
+- [RBAC System](./../SECURITY.md#authorization--rbac)
+
+## 📝 Notes
+
+### Dashboard Widgets
+
+- **Summary Cards**: Key metrics display
+- **Metrics Charts**: Data visualization
+- **Approval Queue**: Content review workflow
+- **User Table**: User management interface
+- **System Health**: Monitoring và alerts
+
+### Data Sources
+
+- **Campaigns**: Campaign performance metrics
+- **Content**: Content engagement data
+- **Users**: User activity và permissions
+- **Analytics**: Event tracking data
+- **System**: Health và performance metrics
+
+### Performance Considerations
+
+- **Data Caching**: Cache frequently accessed data
+- **Lazy Loading**: Load widgets on demand
+- **Pagination**: Handle large datasets
+- **Real-time Updates**: Efficient data refresh
+
+---
+
+_Created: 2025-01-02_
+_Assignee: Frontend + Backend Team_
+_Priority: P1_
+_Estimated Time: 4-5 days_
diff --git a/docs/tasks/PR-006-ai-integration.md b/docs/tasks/PR-006-ai-integration.md
new file mode 100644
index 00000000..2ae8f39a
--- /dev/null
+++ b/docs/tasks/PR-006-ai-integration.md
@@ -0,0 +1,251 @@
+# PR-006: AI Integration & Content Generation
+
+## 🎯 Goal
+
+Implement comprehensive AI integration system cho content generation, translation, summarization, và content ideas.
+
+## 📋 Acceptance Criteria
+
+### AI Content Generation
+
+- [ ] AI content generation với prompt input
+- [ ] Multiple AI models support (GPT-4, GPT-3.5)
+- [ ] Content tone và style customization
+- [ ] Platform-specific content optimization
+- [ ] AI usage tracking và cost monitoring
+
+### AI Services
+
+- [ ] Content summarization service
+- [ ] Multi-language translation
+- [ ] Content idea generation
+- [ ] Content quality scoring
+- [ ] AI prompt library management
+
+### Integration
+
+- [ ] AI integration trong content editor
+- [ ] AI suggestions trong campaign creation
+- [ ] AI-powered content optimization
+- [ ] AI analytics và performance tracking
+
+## 📁 Files to Modify
+
+### New Files
+
+- `app/api/[orgId]/ai/generate/route.ts` - AI content generation
+- `app/api/[orgId]/ai/summarize/route.ts` - Content summarization
+- `app/api/[orgId]/ai/translate/route.ts` - Content translation
+- `app/api/[orgId]/ai/ideas/route.ts` - Content ideas generation
+- `app/api/[orgId]/ai/quality/route.ts` - Content quality scoring
+- `components/ai/ai-generator.tsx` - AI generation component
+- `components/ai/ai-prompt-input.tsx` - Prompt input component
+- `components/ai/ai-suggestions.tsx` - AI suggestions widget
+- `components/ai/ai-quality-meter.tsx` - Quality scoring component
+- `lib/ai-service.ts` - AI service integration
+- `lib/ai-prompts.ts` - Prompt library management
+- `lib/ai-usage-tracker.ts` - Usage tracking service
+
+### Modified Files
+
+- `components/content/content-editor.tsx` - Integrate AI generation
+- `components/campaigns/campaign-form.tsx` - Add AI suggestions
+- `lib/schemas.ts` - Add AI request/response schemas
+- `lib/rbac.ts` - Add AI usage permissions
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add openai @types/node
+pnpm add -D @types/node
+
+# Configure OpenAI
+# Add OPENAI_API_KEY to .env
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+
+# Run database migrations (if needed)
+pnpm prisma migrate dev --name add_ai_integration
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- ai.test.tsx
+pnpm test -- ai-service.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **AI Content Generation**
+
+ - [ ] Generate content với different prompts
+ - [ ] Test tone và style options
+ - [ ] Verify platform optimization
+ - [ ] Check content quality
+
+2. **AI Services**
+
+ - [ ] Test content summarization
+ - [ ] Test multi-language translation
+ - [ ] Test content idea generation
+ - [ ] Test quality scoring
+
+3. **Integration**
+ - [ ] Test AI trong content editor
+ - [ ] Test AI suggestions
+ - [ ] Verify usage tracking
+ - [ ] Check cost monitoring
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] API key management secure
+- [ ] Input sanitization implemented
+- [ ] Rate limiting cho AI requests
+- [ ] No sensitive data exposure
+- [ ] Usage monitoring implemented
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Performance
+
+- [ ] AI requests optimized
+- [ ] Response caching implemented
+- [ ] Usage tracking efficient
+- [ ] Cost monitoring accurate
+- [ ] Error handling graceful
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove openai
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] AI response time < 10 seconds
+- [ ] Usage tracking accuracy > 95%
+
+### Functional Metrics
+
+- [ ] AI content generation works
+- [ ] Multiple AI services functional
+- [ ] Integration smooth trong editor
+- [ ] Usage tracking accurate
+- [ ] Cost monitoring functional
+
+## 🔗 Related Documentation
+
+- [Content API](./../api/content.md)
+- [Analytics API](./../api/analytics.md)
+- [AI Integration Guide](./../AI_INTEGRATION_README.md)
+
+## 📝 Notes
+
+### AI Models Supported
+
+- **GPT-4**: High-quality content generation
+- **GPT-3.5**: Fast, cost-effective generation
+- **Future**: Claude, Gemini integration planned
+
+### Content Types
+
+- **Social Media Posts**: Facebook, Instagram, LinkedIn, Twitter
+- **Email Content**: Subject lines, body content
+- **Blog Posts**: Articles, summaries
+- **Ad Copy**: Headlines, descriptions
+
+### AI Features
+
+- **Tone Control**: Formal, friendly, professional, casual
+- **Style Options**: Creative, informative, persuasive
+- **Length Control**: Short, medium, long
+- **Platform Optimization**: Platform-specific formatting
+
+### Usage Tracking
+
+- **Token Count**: Track OpenAI token usage
+- **Cost Monitoring**: Real-time cost tracking
+- **Quality Metrics**: User feedback integration
+- **Performance Analytics**: Response time, success rate
+
+---
+
+_Created: 2025-01-02_
+_Assignee: AI + Backend Team_
+_Priority: P1_
+_Estimated Time: 3-4 days_
diff --git a/docs/tasks/PR-007-scheduling-calendar.md b/docs/tasks/PR-007-scheduling-calendar.md
new file mode 100644
index 00000000..d2d616cd
--- /dev/null
+++ b/docs/tasks/PR-007-scheduling-calendar.md
@@ -0,0 +1,260 @@
+# PR-007: Scheduling & Calendar System
+
+## 🎯 Goal
+
+Implement comprehensive scheduling và calendar system cho content publishing với timezone support và recurring schedules.
+
+## 📋 Acceptance Criteria
+
+### Calendar View
+
+- [ ] Calendar page với react-day-picker
+- [ ] Color-coded events theo platform
+- [ ] Month/week/day view options
+- [ ] Timezone support (user preference, store UTC)
+- [ ] Responsive design cho mobile
+
+### Scheduling System
+
+- [ ] Schedule form modal (platform/date/time/timezone)
+- [ ] Content scheduling validation (must be approved)
+- [ ] Recurring schedules với RRULE model
+- [ ] Schedule conflict detection
+- [ ] Bulk scheduling operations
+
+### Integration
+
+- [ ] Schedule integration với content approval workflow
+- [ ] Schedule notifications và reminders
+- [ ] Schedule analytics và reporting
+- [ ] Schedule export (iCal format)
+
+## 📁 Files to Modify
+
+### New Files
+
+- `app/schedules/page.tsx` - Calendar view page
+- `app/schedules/[id]/page.tsx` - Schedule detail page
+- `app/schedules/new/page.tsx` - Create schedule page
+- `components/schedules/calendar-view.tsx` - Calendar component
+- `components/schedules/schedule-form.tsx` - Schedule form modal
+- `components/schedules/schedule-list.tsx` - Schedule list component
+- `components/schedules/recurring-schedule.tsx` - Recurring schedule component
+- `lib/schedule-service.ts` - Schedule business logic
+- `lib/rrule-parser.ts` - RRULE parsing utilities
+- `lib/timezone-utils.ts` - Timezone handling utilities
+
+### Modified Files
+
+- `lib/schemas.ts` - Add schedule validation schemas
+- `components/content/content-editor.tsx` - Add schedule button
+- `components/campaigns/campaign-detail.tsx` - Add schedule tab
+- `lib/rbac.ts` - Add schedule permissions
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add react-day-picker date-fns rrule
+pnpm add -D @types/rrule
+
+# Database migration (after schema changes)
+pnpm db:generate
+pnpm db:push
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Type checking (calendar module now compiles)
+pnpm typecheck
+
+# Build (to verify production build)
+pnpm build
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- schedules.test.tsx
+pnpm test -- calendar.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **Calendar View**
+
+ - [ ] Verify calendar displays correctly
+ - [ ] Test month/week/day view switching
+ - [ ] Check timezone handling
+ - [ ] Test responsive design
+
+2. **Schedule Creation**
+
+ - [ ] Create single schedule
+ - [ ] Create recurring schedule
+ - [ ] Test validation rules
+ - [ ] Verify conflict detection
+
+3. **Integration**
+ - [ ] Test content approval workflow
+ - [ ] Verify notifications
+ - [ ] Check analytics integration
+ - [ ] Test export functionality
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] RBAC implemented cho schedule operations
+- [ ] Input validation với Zod schemas
+- [ ] No sensitive data exposure
+- [ ] Permission checks ở API level
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Performance
+
+- [ ] Calendar renders smoothly
+- [ ] Schedule queries optimized
+- [ ] Timezone calculations efficient
+- [ ] Large dataset handling
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove react-day-picker date-fns rrule
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+### Database Rollback
+
+```bash
+# Revert database schema changes
+pnpm prisma migrate reset
+
+# Or restore from backup
+pnpm db:restore
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Calendar render time < 500ms
+- [ ] Schedule creation < 2 seconds
+
+### Functional Metrics
+
+- [ ] Users can create schedules successfully
+- [ ] Calendar displays events correctly
+- [ ] Timezone handling works properly
+- [ ] Recurring schedules function correctly
+- [ ] Integration smooth với content workflow
+
+## 🔗 Related Documentation
+
+- [Schedules API](./../api/schedules.md)
+- [Content API](./../api/content.md)
+- [Campaigns API](./../api/campaigns.md)
+
+## 📝 Notes
+
+### Supported Platforms
+
+- **Social Media**: Facebook, Instagram, LinkedIn, Twitter
+- **Blog**: WordPress, Medium, Custom
+- **Email**: Newsletter, Marketing campaigns
+- **Other**: Custom integrations
+
+### Schedule Types
+
+- **One-time**: Single publication
+- **Recurring**: Daily, weekly, monthly patterns
+- **Bulk**: Multiple content items
+- **Conditional**: Based on approval status
+
+### Timezone Features
+
+- **User Preference**: Store user's preferred timezone
+- **UTC Storage**: All schedules stored in UTC
+- **Display Conversion**: Convert to user's timezone for display
+- **DST Handling**: Automatic daylight saving time adjustment
+
+### Database Migration Notes
+
+- **Schema Changes**: New Schedule model với relationships
+- **Data Migration**: Existing content có thể được scheduled
+- **Backward Compatibility**: Maintain existing API contracts
+- **Performance**: Indexes cho date/time queries
+
+---
+
+_Created: 2025-01-02_
+_Updated: 2025-01-02_
+_Assignee: Full-Stack Team_
+_Priority: P2_
+_Estimated Time: 3-4 days_
diff --git a/docs/tasks/PR-008-analytics-reporting.md b/docs/tasks/PR-008-analytics-reporting.md
new file mode 100644
index 00000000..f506dcb5
--- /dev/null
+++ b/docs/tasks/PR-008-analytics-reporting.md
@@ -0,0 +1,242 @@
+# PR-008: Analytics & Reporting System
+
+## 🎯 Goal
+
+Implement comprehensive analytics và reporting system cho campaigns, content, và user engagement với data visualization và export capabilities.
+
+## 📋 Acceptance Criteria
+
+### Analytics Dashboard
+
+- [ ] Overview charts (7/30/90 day periods)
+- [ ] Campaign analytics tab (line/bar/pie charts)
+- [ ] Content analytics tab với performance metrics
+- [ ] Creator performance tracking
+- [ ] Real-time data updates
+
+### Event Tracking
+
+- [ ] Event collection system (view/click/conversion)
+- [ ] Event aggregation và processing
+- [ ] Custom event definitions
+- [ ] Event filtering và segmentation
+- [ ] Event export functionality
+
+### Reporting
+
+- [ ] Scheduled report generation
+- [ ] Report templates (PDF/Excel/CSV)
+- [ ] Custom report builder
+- [ ] Report sharing và distribution
+- [ ] Historical data analysis
+
+## 📁 Files to Modify
+
+### New Files
+
+- `app/analytics/page.tsx` - Analytics dashboard page
+- `app/analytics/campaigns/page.tsx` - Campaign analytics page
+- `app/analytics/content/page.tsx` - Content analytics page
+- `app/analytics/reports/page.tsx` - Reports page
+- `components/analytics/analytics-dashboard.tsx` - Main dashboard component
+- `components/analytics/charts/line-chart.tsx` - Line chart component
+- `components/analytics/charts/bar-chart.tsx` - Bar chart component
+- `components/analytics/charts/pie-chart.tsx` - Pie chart component
+- `components/analytics/metrics-cards.tsx` - Metrics display cards
+- `components/analytics/report-builder.tsx` - Custom report builder
+- `lib/analytics-service.ts` - Analytics business logic
+- `lib/event-tracker.ts` - Event tracking service
+- `lib/report-generator.ts` - Report generation service
+
+### Modified Files
+
+- `lib/schemas.ts` - Add analytics schemas
+- `components/layout/sidebar.tsx` - Add analytics navigation
+- `lib/rbac.ts` - Add analytics permissions
+- `middleware.ts` - Add analytics tracking
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add recharts @types/recharts
+pnpm add -D @types/node
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+
+# Run database migrations (if needed)
+pnpm prisma migrate dev --name add_analytics
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- analytics.test.tsx
+pnpm test -- charts.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **Analytics Dashboard**
+
+ - [ ] Verify charts render correctly
+ - [ ] Test data filtering options
+ - [ ] Check real-time updates
+ - [ ] Test responsive design
+
+2. **Event Tracking**
+
+ - [ ] Test event collection
+ - [ ] Verify event processing
+ - [ ] Check event filtering
+ - [ ] Test export functionality
+
+3. **Reporting**
+ - [ ] Generate scheduled reports
+ - [ ] Test custom report builder
+ - [ ] Verify report formats
+ - [ ] Check sharing functionality
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] RBAC implemented cho analytics access
+- [ ] Data filtering theo user permissions
+- [ ] No sensitive data exposure
+- [ ] Rate limiting cho event tracking
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Performance
+
+- [ ] Charts render smoothly
+- [ ] Data queries optimized
+- [ ] Real-time updates efficient
+- [ ] Large dataset handling
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove recharts @types/recharts
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Chart render time < 500ms
+- [ ] Data query time < 200ms
+
+### Functional Metrics
+
+- [ ] Analytics dashboard displays correctly
+- [ ] Event tracking accurate
+- [ ] Reports generate successfully
+- [ ] Data visualization clear
+- [ ] Export functionality works
+
+## 🔗 Related Documentation
+
+- [Analytics API](./../api/analytics.md)
+- [Campaigns API](./../api/campaigns.md)
+- [Content API](./../api/content.md)
+
+## 📝 Notes
+
+### Analytics Metrics
+
+- **Engagement**: Views, clicks, shares, comments
+- **Performance**: CTR, conversion rates, reach
+- **Timing**: Best posting times, engagement patterns
+- **Audience**: Demographics, behavior patterns
+
+### Chart Types
+
+- **Line Charts**: Time-series data, trends
+- **Bar Charts**: Comparison data, categories
+- **Pie Charts**: Distribution data, proportions
+- **Heatmaps**: Time-based patterns, correlations
+
+### Data Sources
+
+- **Campaigns**: Performance metrics, ROI
+- **Content**: Engagement data, reach
+- **Users**: Behavior patterns, preferences
+- **External**: Social media APIs, analytics platforms
+
+---
+
+_Created: 2025-01-02_
+_Assignee: Backend + Frontend Team_
+_Priority: P2_
+_Estimated Time: 4-5 days_
diff --git a/docs/tasks/PR-009-settings-management.md b/docs/tasks/PR-009-settings-management.md
new file mode 100644
index 00000000..070548cb
--- /dev/null
+++ b/docs/tasks/PR-009-settings-management.md
@@ -0,0 +1,249 @@
+# PR-009: Settings & User Management System
+
+## 🎯 Goal
+
+Implement comprehensive settings và user management system cho organizations, users, và system configuration với role-based access control.
+
+## 📋 Acceptance Criteria
+
+### User Management
+
+- [ ] User profile settings (name/email/password/AI usage)
+- [ ] User role management (Admin only)
+- [ ] User enable/disable functionality
+- [ ] User activity tracking
+- [ ] Bulk user operations
+
+### Organization Settings
+
+- [ ] Organization profile (name/slug/logo/default language)
+- [ ] Organization preferences và configuration
+- [ ] Organization billing và quotas
+- [ ] Organization API keys management
+- [ ] Organization audit logs
+
+### System Settings
+
+- [ ] Feature flags management
+- [ ] System configuration options
+- [ ] Notification preferences
+- [ ] Security settings
+- [ ] System health monitoring
+
+## 📁 Files to Modify
+
+### New Files
+
+- `app/settings/page.tsx` - Main settings page
+- `app/settings/profile/page.tsx` - User profile settings
+- `app/settings/organization/page.tsx` - Organization settings
+- `app/settings/users/page.tsx` - User management page
+- `app/settings/system/page.tsx` - System settings page
+- `components/settings/profile-form.tsx` - Profile settings form
+- `components/settings/organization-form.tsx` - Organization settings form
+- `components/settings/user-table.tsx` - User management table
+- `components/settings/feature-flags.tsx` - Feature flags management
+- `components/settings/api-keys.tsx` - API keys management
+- `lib/settings-service.ts` - Settings business logic
+- `lib/user-management.ts` - User management service
+- `lib/feature-flags.ts` - Feature flags service
+
+### Modified Files
+
+- `lib/schemas.ts` - Add settings validation schemas
+- `lib/rbac.ts` - Add settings permissions
+- `components/layout/sidebar.tsx` - Add settings navigation
+- `middleware.ts` - Add settings access control
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add react-hook-form @hookform/resolvers
+pnpm add -D @types/node
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+
+# Run database migrations (if needed)
+pnpm prisma migrate dev --name add_settings
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Check database
+pnpm prisma studio
+
+# Run type check
+pnpm typecheck
+
+# Run linting
+pnpm lint
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- settings.test.tsx
+pnpm test -- user-management.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **User Profile Settings**
+
+ - [ ] Update user profile information
+ - [ ] Change password
+ - [ ] Update AI usage preferences
+ - [ ] Test validation rules
+
+2. **Organization Settings**
+
+ - [ ] Update organization profile
+ - [ ] Configure organization preferences
+ - [ ] Manage API keys
+ - [ ] Test billing integration
+
+3. **User Management**
+
+ - [ ] Create new users
+ - [ ] Assign roles
+ - [ ] Enable/disable users
+ - [ ] Test bulk operations
+
+4. **System Settings**
+ - [ ] Toggle feature flags
+ - [ ] Configure system options
+ - [ ] Set notification preferences
+ - [ ] Monitor system health
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Security
+
+- [ ] RBAC implemented cho all settings
+- [ ] Input validation với Zod schemas
+- [ ] No sensitive data exposure
+- [ ] Audit logging cho critical changes
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Performance
+
+- [ ] Settings load quickly
+- [ ] Form validation responsive
+- [ ] Bulk operations efficient
+- [ ] Real-time updates smooth
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove react-hook-form @hookform/resolvers
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Settings load time < 1 second
+- [ ] Form submission < 2 seconds
+
+### Functional Metrics
+
+- [ ] Users can update profiles successfully
+- [ ] Organization settings save correctly
+- [ ] User management functions work
+- [ ] Feature flags toggle properly
+- [ ] System health monitoring active
+
+## 🔗 Related Documentation
+
+- [Security Guidelines](./../SECURITY.md)
+- [RBAC System](./../SECURITY.md#authorization--rbac)
+- [Data Model](./../data-model.md)
+
+## 📝 Notes
+
+### User Profile Features
+
+- **Personal Info**: Name, email, avatar
+- **Preferences**: Language, timezone, theme
+- **AI Usage**: API limits, model preferences
+- **Security**: Two-factor authentication, session management
+
+### Organization Features
+
+- **Profile**: Name, description, branding
+- **Settings**: Defaults, workflows, integrations
+- **Billing**: Plans, usage tracking, invoices
+- **Security**: Access controls, audit trails
+
+### System Features
+
+- **Feature Flags**: Gradual rollouts, A/B testing
+- **Monitoring**: Health checks, performance metrics
+- **Configuration**: Environment-specific settings
+- **Maintenance**: Backup, updates, migrations
+
+---
+
+_Created: 2025-01-02_
+_Assignee: Backend + Frontend Team_
+_Priority: P3_
+_Estimated Time: 3-4 days_
diff --git a/docs/tasks/PR-010-performance-optimization.md b/docs/tasks/PR-010-performance-optimization.md
new file mode 100644
index 00000000..8718e0b7
--- /dev/null
+++ b/docs/tasks/PR-010-performance-optimization.md
@@ -0,0 +1,248 @@
+# PR-010: Performance Optimization & Scalability
+
+## 🎯 Goal
+
+Implement comprehensive performance optimization và scalability improvements cho AiM Platform với bundle analysis, caching, và virtualization.
+
+## 📋 Acceptance Criteria
+
+### Bundle Optimization
+
+- [ ] Bundle analysis với `next build --profile`
+- [ ] Dynamic imports cho large components
+- [ ] Code splitting optimization
+- [ ] Tree shaking implementation
+- [ ] Bundle size monitoring
+
+### Image & Asset Optimization
+
+- [ ] Next.js Image component optimization
+- [ ] Lazy loading cho images
+- [ ] WebP format support
+- [ ] Responsive image sizing
+- [ ] Asset compression
+
+### Data Performance
+
+- [ ] React Query caching strategies
+- [ ] Database query optimization
+- [ ] Pagination implementation
+- [ ] Virtualization cho large lists
+- [ ] Memory leak prevention
+
+### Caching & CDN
+
+- [ ] Static asset caching
+- [ ] API response caching
+- [ ] CDN integration
+- [ ] Cache invalidation strategies
+- [ ] Performance monitoring
+
+## 📁 Files to Modify
+
+### New Files
+
+- `lib/performance-monitor.ts` - Performance monitoring service
+- `lib/cache-service.ts` - Caching service
+- `lib/bundle-analyzer.ts` - Bundle analysis utilities
+- `components/ui/virtualized-list.tsx` - Virtualized list component
+- `components/ui/lazy-image.tsx` - Lazy loading image component
+- `scripts/analyze-bundle.js` - Bundle analysis script
+- `next.config.ts` - Performance configuration
+
+### Modified Files
+
+- `app/layout.tsx` - Add performance monitoring
+- `lib/prisma.ts` - Add query optimization
+- `components/layout/sidebar.tsx` - Add lazy loading
+- `middleware.ts` - Add caching headers
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add react-window react-virtualized
+pnpm add -D @next/bundle-analyzer
+
+# Configure bundle analyzer
+# Update next.config.ts
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Analyze bundle
+pnpm analyze
+
+# Run performance tests
+pnpm test:performance
+
+# Run type check
+pnpm typecheck
+```
+
+### Testing
+
+```bash
+# Run tests
+pnpm test
+
+# Run tests with coverage
+pnpm test:coverage
+
+# Run specific test files
+pnpm test -- performance.test.tsx
+pnpm test -- caching.test.tsx
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **Bundle Analysis**
+
+ - [ ] Run bundle analysis
+ - [ ] Identify large dependencies
+ - [ ] Verify code splitting
+ - [ ] Check tree shaking
+
+2. **Performance Testing**
+
+ - [ ] Test page load times
+ - [ ] Verify image optimization
+ - [ ] Check caching behavior
+ - [ ] Test large dataset handling
+
+3. **Memory Testing**
+ - [ ] Monitor memory usage
+ - [ ] Test memory leaks
+ - [ ] Verify cleanup
+ - [ ] Check garbage collection
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Performance
+
+- [ ] Bundle size optimized
+- [ ] Images properly optimized
+- [ ] Caching implemented
+- [ ] Queries optimized
+- [ ] No memory leaks
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Scalability
+
+- [ ] Large datasets handled
+- [ ] Virtualization implemented
+- [ ] Pagination working
+- [ ] Caching strategies effective
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove react-window react-virtualized
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Bundle size reduced by 20%
+- [ ] Page load time improved by 30%
+
+### Performance Metrics
+
+- [ ] First Contentful Paint < 1.5s
+- [ ] Largest Contentful Paint < 2.5s
+- [ ] Cumulative Layout Shift < 0.1
+- [ ] First Input Delay < 100ms
+- [ ] Memory usage stable
+
+## 🔗 Related Documentation
+
+- [Performance Guidelines](./../playbooks/observability.md)
+- [Deployment Guide](./../DEPLOYMENT_README.md)
+- [Monitoring Setup](./../playbooks/observability.md)
+
+## 📝 Notes
+
+### Optimization Techniques
+
+- **Code Splitting**: Route-based và component-based
+- **Tree Shaking**: Remove unused code
+- **Lazy Loading**: Components và images
+- **Caching**: Multiple layers (browser, CDN, server)
+- **Virtualization**: Handle large datasets
+
+### Monitoring Tools
+
+- **Lighthouse**: Performance auditing
+- **Web Vitals**: Core metrics
+- **Bundle Analyzer**: Bundle size analysis
+- **Memory Profiler**: Memory usage tracking
+- **Performance API**: Real-time metrics
+
+### Best Practices
+
+- **Minimize Bundle Size**: Remove unused dependencies
+- **Optimize Images**: Use modern formats, proper sizing
+- **Implement Caching**: Multiple cache layers
+- **Optimize Queries**: Database và API optimization
+- **Monitor Performance**: Continuous monitoring
+
+---
+
+_Created: 2025-01-02_
+_Assignee: Performance + Backend Team_
+_Priority: P3_
+_Estimated Time: 3-4 days_
diff --git a/docs/tasks/PR-011-testing-quality.md b/docs/tasks/PR-011-testing-quality.md
new file mode 100644
index 00000000..6dbdcca0
--- /dev/null
+++ b/docs/tasks/PR-011-testing-quality.md
@@ -0,0 +1,266 @@
+# PR-011: Testing & Quality Assurance
+
+## 🎯 Goal
+
+Implement comprehensive testing strategy với unit tests, integration tests, E2E tests, và quality gates cho AiM Platform.
+
+## 📋 Acceptance Criteria
+
+### Unit Testing
+
+- [ ] Jest/Vitest setup và configuration
+- [ ] Tests cho utilities (auth, rbac, utils)
+- [ ] Tests cho forms và components
+- [ ] Tests cho API routes
+- [ ] Test coverage > 80%
+
+### Integration Testing
+
+- [ ] Database integration tests
+- [ ] API endpoint testing
+- [ ] Authentication flow testing
+- [ ] RBAC permission testing
+- [ ] Error handling testing
+
+### End-to-End Testing
+
+- [ ] Playwright setup và configuration
+- [ ] E2E login flow
+- [ ] E2E campaign creation
+- [ ] E2E content workflow
+- [ ] E2E schedule & publish
+
+### Quality Gates
+
+- [ ] Automated testing trong CI/CD
+- [ ] Code coverage reporting
+- [ ] Performance testing
+- [ ] Security testing
+- [ ] Accessibility testing
+
+## 📁 Files to Modify
+
+### New Files
+
+- `jest.config.js` - Jest configuration
+- `jest.setup.js` - Jest setup file
+- `playwright.config.ts` - Playwright configuration
+- `tests/unit/auth.test.ts` - Authentication tests
+- `tests/unit/rbac.test.ts` - RBAC tests
+- `tests/unit/utils.test.ts` - Utility tests
+- `tests/integration/api.test.ts` - API integration tests
+- `tests/integration/db.test.ts` - Database integration tests
+- `tests/e2e/auth.spec.ts` - E2E authentication tests
+- `tests/e2e/campaigns.spec.ts` - E2E campaign tests
+- `tests/e2e/content.spec.ts` - E2E content tests
+- `lib/test-utils.ts` - Test utilities
+- `lib/test-db.ts` - Test database setup
+
+### Modified Files
+
+- `package.json` - Add test scripts
+- `tsconfig.json` - Test TypeScript configuration
+- `.github/workflows/test.yml` - CI test workflow
+- `next.config.ts` - Test configuration
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install testing dependencies
+pnpm add -D jest @types/jest @testing-library/react @testing-library/jest-dom
+pnpm add -D playwright @playwright/test
+pnpm add -D @testing-library/user-event
+
+# Setup Jest
+npx jest --init
+
+# Setup Playwright
+npx playwright install
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Run unit tests
+pnpm test
+
+# Run integration tests
+pnpm test:integration
+
+# Run E2E tests
+pnpm test:e2e
+
+# Run tests with coverage
+pnpm test:coverage
+```
+
+### Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Run specific test suites
+pnpm test -- --testPathPattern=auth
+pnpm test -- --testPathPattern=rbac
+
+# Run E2E tests
+pnpm test:e2e
+
+# Run Playwright tests
+npx playwright test
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **Unit Tests**
+
+ - [ ] Run all unit tests
+ - [ ] Verify test coverage > 80%
+ - [ ] Check test execution time
+ - [ ] Verify test isolation
+
+2. **Integration Tests**
+
+ - [ ] Run database integration tests
+ - [ ] Test API endpoints
+ - [ ] Verify authentication flows
+ - [ ] Check error handling
+
+3. **E2E Tests**
+ - [ ] Run Playwright tests
+ - [ ] Verify user workflows
+ - [ ] Check cross-browser compatibility
+ - [ ] Test responsive design
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Testing Coverage
+
+- [ ] Unit tests cover critical paths
+- [ ] Integration tests cover API flows
+- [ ] E2E tests cover user workflows
+- [ ] Test coverage > 80%
+- [ ] Tests are maintainable
+
+### Code Quality
+
+- [ ] TypeScript types properly defined
+- [ ] Error handling implemented
+- [ ] Code follows style guidelines
+- [ ] No console.log statements
+- [ ] Proper JSDoc documentation
+
+### Test Quality
+
+- [ ] Tests are isolated
+- [ ] Tests are deterministic
+- [ ] Tests have clear assertions
+- [ ] Tests handle edge cases
+- [ ] Tests are fast
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Dependencies Rollback
+
+```bash
+# Remove added packages
+pnpm remove -D jest @types/jest @testing-library/react @testing-library/jest-dom
+pnpm remove -D playwright @playwright/test
+
+# Reinstall previous package-lock
+pnpm install --frozen-lockfile
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Test coverage > 80%
+- [ ] Test execution time < 30 seconds
+
+### Quality Metrics
+
+- [ ] Unit tests cover critical paths
+- [ ] Integration tests work reliably
+- [ ] E2E tests pass consistently
+- [ ] Tests are maintainable
+- [ ] Quality gates enforced
+
+## 🔗 Related Documentation
+
+- [Testing Guidelines](./../CONTRIBUTING.md#testing)
+- [CI/CD Setup](./../playbooks/observability.md)
+- [Quality Standards](./../CONTRIBUTING.md#code-quality)
+
+## 📝 Notes
+
+### Testing Strategy
+
+- **Unit Tests**: Test individual functions và components
+- **Integration Tests**: Test API endpoints và database
+- **E2E Tests**: Test complete user workflows
+- **Performance Tests**: Test response times và scalability
+- **Security Tests**: Test authentication và authorization
+
+### Testing Tools
+
+- **Jest**: Unit và integration testing
+- **React Testing Library**: Component testing
+- **Playwright**: E2E testing
+- **MSW**: API mocking
+- **Testing Library**: User interaction testing
+
+### Best Practices
+
+- **Test Isolation**: Each test should be independent
+- **Clear Assertions**: Tests should be easy to understand
+- **Fast Execution**: Tests should run quickly
+- **Maintainable**: Tests should be easy to update
+- **Coverage**: Test critical business logic
+
+---
+
+_Created: 2025-01-02_
+_Assignee: QA + Engineering Team_
+_Priority: P3_
+_Estimated Time: 4-5 days_
diff --git a/docs/tasks/PR-012-documentation-deployment.md b/docs/tasks/PR-012-documentation-deployment.md
new file mode 100644
index 00000000..d7c72627
--- /dev/null
+++ b/docs/tasks/PR-012-documentation-deployment.md
@@ -0,0 +1,265 @@
+# PR-012: Documentation & Deployment
+
+## 🎯 Goal
+
+Complete comprehensive documentation và deployment setup cho AiM Platform với production-ready configuration và monitoring.
+
+## 📋 Acceptance Criteria
+
+### Documentation
+
+- [ ] API documentation với OpenAPI/Swagger
+- [ ] User guides và tutorials
+- [ ] Developer documentation
+- [ ] Deployment guides
+- [ ] Troubleshooting guides
+
+### Deployment Setup
+
+- [ ] Docker containerization
+- [ ] Docker Compose configuration
+- [ ] Production environment setup
+- [ ] CI/CD pipeline configuration
+- [ ] Health check endpoints
+
+### Monitoring & Observability
+
+- [ ] Application logging setup
+- [ ] Error tracking (Sentry)
+- [ ] Performance monitoring
+- [ ] Health check dashboard
+- [ ] Alert system configuration
+
+### Production Readiness
+
+- [ ] Environment configuration
+- [ ] Security hardening
+- [ ] Backup strategies
+- [ ] Rollback procedures
+- [ ] Disaster recovery plan
+
+## 📁 Files to Modify
+
+### New Files
+
+- `Dockerfile` - Production Docker image
+- `docker-compose.yml` - Development environment
+- `docker-compose.prod.yml` - Production environment
+- `.github/workflows/deploy.yml` - Deployment workflow
+- `.github/workflows/ci.yml` - CI pipeline
+- `scripts/deploy.sh` - Deployment script
+- `scripts/backup.sh` - Backup script
+- `scripts/health-check.sh` - Health check script
+- `docs/api/openapi.yaml` - OpenAPI specification
+- `docs/user-guide/` - User documentation
+- `docs/developer/` - Developer documentation
+- `docs/deployment/` - Deployment guides
+- `lib/logger.ts` - Logging service
+- `lib/monitoring.ts` - Monitoring service
+
+### Modified Files
+
+- `package.json` - Add deployment scripts
+- `next.config.ts` - Production configuration
+- `prisma/schema.prisma` - Production database config
+- `.env.example` - Environment variables template
+- `README.md` - Update with deployment info
+
+## 🚀 Commands to Run
+
+### Setup
+
+```bash
+# Install additional dependencies
+pnpm add winston pino
+pnpm add -D @types/node
+
+# Build production image
+docker build -t aim-platform .
+
+# Run production environment
+docker-compose -f docker-compose.prod.yml up -d
+
+# Generate Prisma client (if schema changed)
+pnpm prisma generate
+```
+
+### Development
+
+```bash
+# Start dev server
+pnpm dev
+
+# Build for production
+pnpm build
+
+# Start production server
+pnpm start
+
+# Run health checks
+pnpm health-check
+```
+
+### Deployment
+
+```bash
+# Deploy to staging
+pnpm deploy:staging
+
+# Deploy to production
+pnpm deploy:production
+
+# Run backup
+pnpm backup
+
+# Check system health
+pnpm health-check
+```
+
+## 🧪 Test Steps
+
+### Manual Testing
+
+1. **Documentation**
+
+ - [ ] Verify API documentation
+ - [ ] Test user guides
+ - [ ] Check developer docs
+ - [ ] Validate deployment guides
+
+2. **Deployment**
+
+ - [ ] Test Docker build
+ - [ ] Verify container startup
+ - [ ] Check health endpoints
+ - [ ] Test rollback procedures
+
+3. **Monitoring**
+ - [ ] Verify logging setup
+ - [ ] Test error tracking
+ - [ ] Check performance monitoring
+ - [ ] Validate alert system
+
+### Automated Testing
+
+```bash
+# Run all tests
+pnpm test
+
+# Verify test coverage > 80%
+pnpm test:coverage
+
+# Check for TypeScript errors
+pnpm typecheck
+
+# Verify linting passes
+pnpm lint
+```
+
+## 🔍 Code Review Checklist
+
+### Documentation
+
+- [ ] API docs complete và accurate
+- [ ] User guides clear và helpful
+- [ ] Developer docs comprehensive
+- [ ] Deployment guides detailed
+- [ ] Troubleshooting guides useful
+
+### Deployment
+
+- [ ] Docker configuration optimized
+- [ ] CI/CD pipeline functional
+- [ ] Health checks implemented
+- [ ] Rollback procedures tested
+- [ ] Security measures in place
+
+### Monitoring
+
+- [ ] Logging configured properly
+- [ ] Error tracking active
+- [ ] Performance monitoring working
+- [ ] Alert system functional
+- [ ] Health dashboard accessible
+
+## 🚨 Rollback Plan
+
+### Code Rollback
+
+```bash
+# Revert to previous commit
+git reset --hard HEAD~1
+
+# Or checkout specific commit
+git checkout
+```
+
+### Deployment Rollback
+
+```bash
+# Rollback Docker image
+docker tag aim-platform:previous aim-platform:latest
+
+# Restart services
+docker-compose -f docker-compose.prod.yml restart
+
+# Verify rollback
+pnpm health-check
+```
+
+## 📊 Success Metrics
+
+### Technical Metrics
+
+- [ ] All tests passing (100%)
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Docker build successful
+- [ ] Health checks passing
+
+### Deployment Metrics
+
+- [ ] Deployment time < 10 minutes
+- [ ] Zero-downtime deployments
+- [ ] Rollback time < 5 minutes
+- [ ] Health check response < 200ms
+- [ ] 99.9% uptime achieved
+
+## 🔗 Related Documentation
+
+- [Deployment Guide](./../DEPLOYMENT_README.md)
+- [API Documentation](./../api/)
+- [Monitoring Setup](./../playbooks/observability.md)
+
+## 📝 Notes
+
+### Documentation Structure
+
+- **API Docs**: OpenAPI/Swagger specification
+- **User Guides**: Step-by-step tutorials
+- **Developer Docs**: Technical implementation details
+- **Deployment Guides**: Environment setup instructions
+- **Troubleshooting**: Common issues và solutions
+
+### Deployment Strategy
+
+- **Blue-Green**: Zero-downtime deployments
+- **Rolling Updates**: Gradual service updates
+- **Canary Releases**: Limited user exposure
+- **Feature Flags**: Gradual feature rollouts
+- **Monitoring**: Real-time deployment tracking
+
+### Production Considerations
+
+- **Security**: HTTPS, authentication, authorization
+- **Performance**: Caching, CDN, optimization
+- **Scalability**: Load balancing, auto-scaling
+- **Reliability**: Backup, redundancy, monitoring
+- **Compliance**: GDPR, security standards
+
+---
+
+_Created: 2025-01-02_
+_Assignee: DevOps + Documentation Team_
+_Priority: P3_
+_Estimated Time: 5-6 days_
diff --git a/docs/tasks/README.md b/docs/tasks/README.md
new file mode 100644
index 00000000..7741cd14
--- /dev/null
+++ b/docs/tasks/README.md
@@ -0,0 +1,193 @@
+# AiM Platform - Development Tasks
+
+## 📋 Tổng quan
+
+Thư mục này chứa các PR tasks chi tiết cho việc phát triển AiM Platform. Mỗi task được thiết kế để hoàn thành trong 1 PR với acceptance criteria rõ ràng, test steps, và rollback plans.
+
+## 🎯 **KIẾN TRÚC & STACK ĐÃ CHỐT**
+
+**Stack**: Next.js 15, Prisma 6, PostgreSQL, NextAuth 5, shadcn/ui, Tailwind CSS 4, TypeScript 5, React 19
+
+**Data Model**: User, Organization, Membership, Campaign, Content, Asset, Schedule, AnalyticsEvent
+
+## 📊 **ROADMAP & PRIORITIES**
+
+### **Phase 1: Foundation (Weeks 1-2)**
+
+- [x] **PR-001**: Authentication & Prisma Foundation ✅
+- [ ] **PR-003**: RBAC System & Role-Based Navigation 🔄 (P0)
+- [ ] **PR-002**: Campaigns & Content CRUD Operations ⏳ (P0)
+
+### **Phase 2: Core Features (Weeks 3-4)**
+
+- [ ] **PR-004**: Asset Upload & Management System ⏳ (P2)
+- [ ] **PR-005**: Role-Based Dashboards ⏳ (P1)
+- [ ] **PR-006**: AI Integration & Content Generation ⏳ (P1)
+
+### **Phase 3: Enhancement (Weeks 5-6)**
+
+- [ ] **PR-007**: Scheduling & Calendar System 🔄 (P2) - _In Progress_
+- [ ] **PR-008**: Analytics & Reporting ⏳ (P2)
+- [ ] **PR-009**: Settings & User Management ⏳ (P3)
+
+### **Phase 4: Polish (Weeks 7-8)**
+
+- [ ] **PR-010**: Performance Optimization & Scalability ⏳ (P3)
+- [ ] **PR-011**: Testing & Quality Assurance ⏳ (P3)
+- [ ] **PR-012**: Documentation & Deployment ⏳ (P3)
+
+## 🚀 **CURRENT STATUS**
+
+- **Tổng số PRs**: 12 planned
+- **Đã hoàn thành**: 1 (8%)
+- **Đang thực hiện**: 2 (17%) - _Including Schedule System_
+- **Chưa bắt đầu**: 9 (75%)
+
+## 📁 **TASK STRUCTURE**
+
+Mỗi PR task có cấu trúc chuẩn:
+
+```
+PR-XXX: Task Name
+├── 🎯 Goal
+├── 📋 Acceptance Criteria
+├── 📁 Files to Modify
+├── 🚀 Commands to Run
+├── 🧪 Test Steps
+├── 🔍 Code Review Checklist
+├── 🚨 Rollback Plan
+├── 📊 Success Metrics
+├── 🔗 Related Documentation
+└── 📝 Notes
+```
+
+## 🔄 **DEVELOPMENT WORKFLOW**
+
+### **1. Task Selection**
+
+- Chọn task theo priority (P0 > P1 > P2 > P3)
+- Đảm bảo dependencies đã hoàn thành
+- Review acceptance criteria
+
+### **2. Implementation**
+
+- Tạo feature branch: `feature/PR-XXX-description`
+- Implement theo acceptance criteria
+- Follow coding standards và security guidelines
+
+### **3. Testing**
+
+- Manual testing theo test steps
+- Automated testing với coverage > 80%
+- Type checking và linting pass
+
+### **4. Code Review**
+
+- Self-review trước khi submit
+- Address feedback từ reviewers
+- Ensure checklist items completed
+
+### **5. Merge & Deploy**
+
+- Merge sau khi approved
+- Deploy to staging environment
+- Verify functionality
+
+## 🛡️ **QUALITY GATES**
+
+### **Technical Requirements**
+
+- [ ] TypeScript compilation successful
+- [ ] Linting passes without errors
+- [ ] Test coverage > 80%
+- [ ] No console.log statements
+- [ ] Proper error handling
+
+### **Security Requirements**
+
+- [ ] RBAC implemented cho all operations
+- [ ] Input validation với Zod schemas
+- [ ] No sensitive data exposure
+- [ ] Permission checks ở API level
+- [ ] Audit logging cho critical actions
+
+### **Performance Requirements**
+
+- [ ] Page load time < 2 seconds
+- [ ] API response time < 500ms
+- [ ] Database queries < 200ms
+- [ ] No memory leaks detected
+- [ ] Responsive design functional
+
+## 📚 **RESOURCES**
+
+### **Documentation**
+
+- [API Documentation](./../api/)
+- [Security Guidelines](./../SECURITY.md)
+- [Data Model](./../data-model.md)
+- [Architecture Decision Records](./../adr/)
+
+### **Development Tools**
+
+- **Database**: `pnpm prisma studio`
+- **Type Check**: `pnpm typecheck`
+- **Testing**: `pnpm test`
+- **Linting**: `pnpm lint`
+- **Build**: `pnpm build`
+
+### **Environment Setup**
+
+```bash
+# Install dependencies
+pnpm install
+
+# Setup database
+pnpm prisma generate
+pnpm prisma db push
+pnpm db:seed
+
+# Start development
+pnpm dev
+```
+
+### **Database Migration Commands**
+
+```bash
+# After schema changes
+pnpm db:generate
+pnpm db:push
+
+# Generate Prisma client
+pnpm prisma generate
+
+# Check database
+pnpm prisma studio
+```
+
+## 🚨 **BLOCKERS & RISKS**
+
+- **Next.js 15 stability** - Monitor for breaking changes
+- **Database migration complexity** - Plan incremental approach
+- **AI API costs** - Implement usage limits và monitoring
+- **Performance with large datasets** - Plan pagination và virtualization
+
+## 📝 **NOTES**
+
+- **Migration Strategy**: Incremental, feature-flag based
+- **Testing**: Unit tests for critical paths, E2E for user flows
+- **Documentation**: Update README with each major feature
+- **ADR Required**: Schema changes, architecture decisions, breaking changes
+
+### **Recent Updates (2025-01-02)**
+
+- **Schedule System**: Database migration commands updated
+- **Development Flow**: Added `pnpm db:generate` và `pnpm db:push`
+- **Type Checking**: Calendar module compilation verified
+- **Build Verification**: Production build testing added
+
+---
+
+_Last Updated: 2025-01-02_
+_Maintainer: Engineering Team_
+_Version: 1.1_
diff --git a/docs/ui/README.md b/docs/ui/README.md
new file mode 100644
index 00000000..7b44ea3f
--- /dev/null
+++ b/docs/ui/README.md
@@ -0,0 +1,273 @@
+# AiM Platform - UI Documentation
+
+## 📋 Tổng quan
+
+Thư mục này chứa các UI guides chi tiết cho từng module của AiM Platform. Mỗi guide được thiết kế để đảm bảo consistency, accessibility, và user experience theo design system của chúng ta.
+
+## 🎯 **DESIGN SYSTEM OVERVIEW**
+
+**Foundation**: Lndev-UI design system
+**Framework**: shadcn/ui components
+**Styling**: Tailwind CSS 4
+**Theme**: Light/Dark mode support
+**Accessibility**: WCAG 2.1 AA compliance
+
+## 📚 **UI GUIDES**
+
+### **Core Modules**
+
+- [**Authentication**](./auth.md) - Sign in, sign up, password reset flows
+- [**Campaigns**](./campaigns.md) - Campaign management và collaboration
+- [**Content Editor**](./content-editor.md) - Rich text editing với AI assistance
+- [**Dashboards**](./dashboards.md) - Role-based analytics và monitoring
+- [**Schedule**](./schedule.md) - Calendar và scheduling system
+
+### **Upcoming Modules**
+
+- **Assets** - Media library và file management
+- **Analytics** - Data visualization và reporting
+- **Settings** - User preferences và system configuration
+- **Team Management** - User roles và permissions
+- **Notifications** - Alert system và messaging
+
+## 🎨 **DESIGN PRINCIPLES**
+
+### **1. Consistency**
+
+- **Component Library**: Unified shadcn/ui components
+- **Spacing System**: Consistent 4px grid system
+- **Color Palette**: Brand colors + semantic colors
+- **Typography**: Clear hierarchy với readable fonts
+
+### **2. Accessibility**
+
+- **WCAG 2.1 AA**: Full compliance
+- **Keyboard Navigation**: All functions accessible
+- **Screen Reader Support**: ARIA labels và live regions
+- **Color Independence**: Not relying on color alone
+
+### **3. Responsiveness**
+
+- **Mobile-First**: Design for mobile trước
+- **Touch Optimization**: 44px minimum touch targets
+- **Gesture Support**: Native touch gestures
+- **Adaptive Layout**: Responsive grid systems
+
+### **4. Performance**
+
+- **Fast Loading**: < 2 second page load
+- **Smooth Interactions**: 60fps animations
+- **Efficient Rendering**: Component memoization
+- **Lazy Loading**: Load content on demand
+
+## 🔧 **COMPONENT ARCHITECTURE**
+
+### **Base Components**
+
+```typescript
+// Core UI components từ shadcn/ui
+Button, Input, Select, Badge, Card, Dialog, Sheet, etc.
+```
+
+### **Layout Components**
+
+```typescript
+// Layout và navigation
+Header, Sidebar, Navigation, Breadcrumbs, Tabs, etc.
+```
+
+### **Data Components**
+
+```typescript
+// Data display và visualization
+Table, Chart, Metric, Progress, Status, etc.
+```
+
+### **Form Components**
+
+```typescript
+// Form elements và validation
+Form, Field, Validation, Error, Success, etc.
+```
+
+## 📱 **RESPONSIVE BREAKPOINTS**
+
+```css
+/* Mobile First Approach */
+sm: 640px /* Small devices */
+md: 768px /* Medium devices */
+lg: 1024px /* Large devices */
+xl: 1280px /* Extra large devices */
+2xl: 1536px /* 2X large devices */
+```
+
+### **Layout Adaptations**
+
+- **Mobile**: Single column, stacked elements
+- **Tablet**: Two column layout
+- **Desktop**: Multi-column grid
+- **Large**: Expanded sidebar, wide content
+
+## 🎨 **VISUAL DESIGN**
+
+### **Color System**
+
+```css
+/* Brand Colors */
+primary: #0f172a /* Dark blue */
+secondary: #64748b /* Gray */
+accent: #3b82f6 /* Blue */
+
+/* Semantic Colors */
+success: #10b981 /* Green */
+warning: #f59e0b /* Amber */
+error: #ef4444 /* Red */
+info: #06b6d4 /* Cyan */
+```
+
+### **Typography Scale**
+
+```css
+/* Font Sizes */
+text-xs: 0.75rem /* 12px */
+text-sm: 0.875rem /* 14px */
+text-base: 1rem /* 16px */
+text-lg: 1.125rem /* 18px */
+text-xl: 1.25rem /* 20px */
+text-2xl: 1.5rem /* 24px */
+text-3xl: 1.875rem /* 30px */
+```
+
+### **Spacing System**
+
+```css
+/* 4px Grid System */
+space-1: 0.25rem /* 4px */
+space-2: 0.5rem /* 8px */
+space-3: 0.75rem /* 12px */
+space-4: 1rem /* 16px */
+space-6: 1.5rem /* 24px */
+space-8: 2rem /* 32px */
+```
+
+## ♿ **ACCESSIBILITY STANDARDS**
+
+### **WCAG 2.1 AA Requirements**
+
+- **Contrast Ratio**: 4.5:1 minimum
+- **Keyboard Navigation**: Full keyboard support
+- **Screen Reader**: ARIA labels và live regions
+- **Focus Management**: Clear focus indicators
+- **Error Handling**: Clear error messages
+
+### **Implementation Guidelines**
+
+- **Semantic HTML**: Proper heading hierarchy
+- **ARIA Attributes**: Descriptive labels
+- **Focus Order**: Logical tab sequence
+- **Skip Links**: Jump to main content
+- **Alternative Text**: Image descriptions
+
+## 🚀 **PERFORMANCE GUIDELINES**
+
+### **Loading Performance**
+
+- **First Contentful Paint**: < 1.5 seconds
+- **Largest Contentful Paint**: < 2.5 seconds
+- **Cumulative Layout Shift**: < 0.1
+- **First Input Delay**: < 100ms
+
+### **Optimization Techniques**
+
+- **Code Splitting**: Route-based splitting
+- **Lazy Loading**: Components và images
+- **Image Optimization**: WebP format, responsive sizing
+- **Caching**: Browser và CDN caching
+- **Bundle Optimization**: Tree shaking, minification
+
+## 🔄 **DEVELOPMENT WORKFLOW**
+
+### **1. Design Phase**
+
+- **Wireframes**: Low-fidelity layouts
+- **Mockups**: High-fidelity designs
+- **Prototypes**: Interactive prototypes
+- **Design Review**: Stakeholder approval
+
+### **2. Implementation Phase**
+
+- **Component Creation**: Build reusable components
+- **Layout Implementation**: Implement page layouts
+- **Responsive Design**: Mobile-first development
+- **Accessibility**: ARIA implementation
+
+### **3. Testing Phase**
+
+- **Visual Testing**: Design consistency
+- **Responsive Testing**: Cross-device testing
+- **Accessibility Testing**: Screen reader testing
+- **Performance Testing**: Loading speed testing
+
+### **4. Review Phase**
+
+- **Design Review**: Visual consistency check
+- **Code Review**: Implementation quality
+- **Accessibility Review**: WCAG compliance
+- **Performance Review**: Speed optimization
+
+## 📝 **DOCUMENTATION STANDARDS**
+
+### **Guide Structure**
+
+Mỗi UI guide phải có:
+
+- **Overview**: Module description và key features
+- **Interface Components**: Component definitions
+- **Layout Examples**: Visual layout structures
+- **User Interactions**: Mouse, keyboard, touch
+- **Performance**: Optimization guidelines
+- **Accessibility**: WCAG compliance details
+
+### **Code Examples**
+
+- **TypeScript Interfaces**: Component definitions
+- **Layout Diagrams**: ASCII art layouts
+- **CSS Classes**: Tailwind utility classes
+- **Component Props**: React component props
+
+## 🔗 **RELATED DOCUMENTATION**
+
+- [**Design System**](../design-system/) - Component library
+- [**API Documentation**](../api/) - Backend integration
+- [**Data Model**](../data-model.md) - Database schema
+- [**Security Guidelines**](../SECURITY.md) - Security standards
+- [**Performance Guidelines**](../playbooks/observability.md) - Performance optimization
+
+## 🚨 **IMPORTANT NOTES**
+
+### **Design Consistency**
+
+- **MUST** follow established design patterns
+- **MUST** use shadcn/ui components
+- **MUST** maintain brand consistency
+- **MUST** follow accessibility guidelines
+
+### **Performance Requirements**
+
+- **MUST** meet performance benchmarks
+- **MUST** implement lazy loading
+- **MUST** optimize bundle size
+- **MUST** use efficient rendering
+
+### **Accessibility Compliance**
+
+- **MUST** meet WCAG 2.1 AA standards
+- **MUST** support keyboard navigation
+- **MUST** provide screen reader support
+- **MUST** maintain high contrast ratios
+
+---
+
+_Last Updated: 2025-01-02_
+_Maintainer: Design Team_
+_Version: 1.0_
diff --git a/docs/ui/auth.md b/docs/ui/auth.md
new file mode 100644
index 00000000..3bb2e23f
--- /dev/null
+++ b/docs/ui/auth.md
@@ -0,0 +1,333 @@
+# Authentication UI Guide
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Interface Components](#interface-components)
+- [Sign In Flow](#sign-in-flow)
+- [Sign Up Flow](#sign-up-flow)
+- [Password Reset](#password-reset)
+- [User Interactions](#user-interactions)
+- [Performance Considerations](#performance-considerations)
+- [Accessibility](#accessibility)
+
+## Overview
+
+Authentication UI là giao diện đăng nhập, đăng ký và quản lý tài khoản cho AiM Platform. Interface hỗ trợ multiple authentication flows, form validation, và responsive design cho tất cả devices.
+
+### 🎯 Key Features
+
+- **Multi-step Authentication**: Sign in, sign up, password reset flows
+- **Form Validation**: Real-time validation với error handling
+- **Responsive Design**: Mobile-first với touch optimization
+- **Security Indicators**: Password strength, 2FA setup
+- **Brand Integration**: Customizable với organization branding
+
+## Interface Components
+
+### 🎛️ Header Controls
+
+```typescript
+interface AuthHeader {
+ logo: Image; // Organization logo
+ title: string; // "Welcome to AiM Platform"
+ subtitle?: string; // "Sign in to continue"
+ languageSelector?: Select; // Language switcher
+}
+```
+
+### 📝 Form Components
+
+```typescript
+interface AuthForm {
+ fields: FormField[]; // Input fields
+ validation: ValidationState; // Real-time validation
+ submitButton: Button; // Primary action button
+ secondaryActions: Button[]; // Links và secondary buttons
+}
+```
+
+### 🔐 Security Elements
+
+```typescript
+interface SecurityFeatures {
+ passwordStrength: ProgressBar; // Password strength indicator
+ twoFactorSetup: Switch; // 2FA toggle
+ rememberMe: Checkbox; // Remember login
+ captcha?: CaptchaComponent; // CAPTCHA if enabled
+}
+```
+
+## Sign In Flow
+
+### 📱 Sign In Page Layout
+
+**Page Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Organization Logo │
+│ Welcome to AiM Platform │
+│ Sign in to continue │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Email Address [email@domain.com] │ │
+│ │ Password [••••••••••••••••] │ │
+│ │ [✓] Remember me [Forgot Password?] │ │
+│ │ [Sign In] │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Don't have an account? [Create Account] │
+│ [Continue with Google] [Continue with GitHub] │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Form Fields:**
+
+- **Email**: Email input với validation
+- **Password**: Password input với show/hide toggle
+- **Remember Me**: Checkbox cho persistent login
+- **Forgot Password**: Link to password reset
+
+**Visual Indicators:**
+
+- 🔴 **Error State**: Red border + error message
+- 🟡 **Warning State**: Yellow border + warning message
+- 🟢 **Success State**: Green border + success message
+- 🔒 **Loading State**: Spinner + disabled inputs
+
+### 🔄 Sign In States
+
+```typescript
+interface SignInStates {
+ initial: FormState; // Clean form
+ loading: LoadingState; // Submitting
+ success: SuccessState; // Redirecting
+ error: ErrorState; // Display error
+ locked: LockedState; // Account locked
+}
+```
+
+## Sign Up Flow
+
+### 📱 Sign Up Page Layout
+
+**Page Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Organization Logo │
+│ Join AiM Platform │
+│ Create your account │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Full Name [John Doe] │ │
+│ │ Email Address [email@domain.com] │ │
+│ │ Password [••••••••••••••••] │
+│ │ Confirm Password [••••••••••••••••] │
+│ │ [✓] I agree to Terms & Privacy Policy │
+│ │ [Sign Up] │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Already have an account? [Sign In] │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Form Fields:**
+
+- **Full Name**: Text input với validation
+- **Email**: Email input với availability check
+- **Password**: Password input với strength indicator
+- **Confirm Password**: Password confirmation
+- **Terms Agreement**: Checkbox với links
+
+**Password Strength Indicator:**
+
+- 🔴 **Weak**: Red bar (0-25%) + "Too weak"
+- 🟡 **Fair**: Yellow bar (26-50%) + "Could be stronger"
+- 🟠 **Good**: Orange bar (51-75%) + "Good password"
+- 🟢 **Strong**: Green bar (76-100%) + "Strong password"
+
+## Password Reset
+
+### 📱 Password Reset Flow
+
+**Step 1: Request Reset**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Reset Password │
+│ Enter your email address │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Email Address [email@domain.com] │ │
+│ │ [Send Reset Link] │
+│ └─────────────────────────────────────────────────────┘ │
+│ [Back to Sign In] │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Step 2: Reset Link Sent**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Check Your Email │
+│ We've sent a password reset link to: │
+│ email@domain.com │
+│ │
+│ [Resend Email] [Change Email] │
+│ [Back to Sign In] │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Step 3: New Password**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Set New Password │
+│ Enter your new password │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ New Password [••••••••••••••••] │
+│ │ Confirm Password [••••••••••••••••] │
+│ │ [Update Password] │
+│ └─────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+## User Interactions
+
+### 🖱️ Mouse Interactions
+
+**Click Actions:**
+
+- **Form Submission**: Submit button clicks
+- **Field Focus**: Input field selection
+- **Toggle Visibility**: Password show/hide
+- **Link Navigation**: Secondary action links
+
+**Hover Effects:**
+
+- **Interactive Elements**: Button hover states
+- **Form Fields**: Subtle border changes
+- **Links**: Underline effects
+
+**Form Validation:**
+
+- **Real-time**: Validate on input change
+- **On Blur**: Validate when leaving field
+- **On Submit**: Final validation check
+
+### ⌨️ Keyboard Navigation
+
+**Tab Order:**
+
+1. Email/Name input
+2. Password input
+3. Remember me checkbox
+4. Submit button
+5. Secondary action links
+6. Social login buttons
+
+**Keyboard Shortcuts:**
+
+- **Enter**: Submit form
+- **Tab**: Navigate between fields
+- **Escape**: Clear form
+- **Space**: Toggle checkboxes
+
+### 📱 Touch Interactions
+
+**Mobile Gestures:**
+
+- **Tap**: Select elements
+- **Long Press**: Show context menu
+- **Swipe**: Navigate between forms
+
+**Touch Optimization:**
+
+- **Touch Targets**: Minimum 44px size
+- **Form Fields**: Large input areas
+- **Buttons**: Adequate spacing
+
+## Performance Considerations
+
+### 🚀 Form Performance
+
+**Validation Strategy:**
+
+- **Debounced Validation**: 300ms delay cho real-time
+- **Lazy Validation**: Validate on blur/submit
+- **Cached Validation**: Store validation results
+
+**Loading States:**
+
+- **Button States**: Disabled + loading spinner
+- **Form Lock**: Prevent multiple submissions
+- **Progress Indicators**: Multi-step progress
+
+**Error Handling:**
+
+- **Graceful Degradation**: Fallback error messages
+- **Retry Logic**: Automatic retry cho network errors
+- **Offline Support**: Queue actions cho later
+
+### 🎨 Rendering Optimization
+
+**Component Memoization:**
+
+- **Form Fields**: Memoize input components
+- **Validation Messages**: Memoize error displays
+- **Buttons**: Memoize button states
+
+**State Management:**
+
+- **Local State**: Form field values
+- **Validation State**: Error/success states
+- **Loading State**: Submission status
+
+## Accessibility
+
+### ♿ WCAG 2.1 Compliance
+
+**Screen Reader Support:**
+
+- **ARIA Labels**: Descriptive labels cho all inputs
+- **Error Announcements**: Announce validation errors
+- **Status Updates**: Announce form state changes
+
+**Keyboard Navigation:**
+
+- **Full Keyboard Support**: All functions accessible
+- **Focus Management**: Logical tab order
+- **Skip Links**: Jump to main content
+
+**Color & Contrast:**
+
+- **High Contrast**: 4.5:1 minimum ratio
+- **Color Independence**: Not relying on color alone
+- **Visual Indicators**: Icons + text labels
+
+### 🎯 Specific Features
+
+**Form Accessibility:**
+
+- **Label Associations**: Proper form labels
+- **Error Messages**: Clear error descriptions
+- **Validation Feedback**: Real-time validation
+
+**Password Security:**
+
+- **Strength Indicators**: Visual + text feedback
+- **Show/Hide Toggle**: Accessible button
+- **Requirements List**: Clear password rules
+
+**Multi-step Flows:**
+
+- **Progress Indicators**: Clear step progression
+- **Step Navigation**: Easy step switching
+- **Context Information**: Current step context
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Design Team_
diff --git a/docs/ui/campaigns.md b/docs/ui/campaigns.md
new file mode 100644
index 00000000..8dac0c19
--- /dev/null
+++ b/docs/ui/campaigns.md
@@ -0,0 +1,530 @@
+# Campaigns UI Guide
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Interface Components](#interface-components)
+- [Campaign List View](#campaign-list-view)
+- [Campaign Detail View](#campaign-detail-view)
+- [Campaign Creation Form](#campaign-creation-form)
+- [Campaign Management](#campaign-management)
+- [User Interactions](#user-interactions)
+- [Performance Considerations](#performance-considerations)
+- [Accessibility](#accessibility)
+
+## Overview
+
+Campaigns UI là giao diện quản lý chiến dịch marketing cho AiM Platform. Interface hỗ trợ campaign creation, management, analytics, và collaboration giữa team members với role-based access control.
+
+### 🎯 Key Features
+
+- **Campaign Management**: Create, edit, delete campaigns
+- **Content Integration**: Link content với campaigns
+- **Team Collaboration**: Role-based permissions và workflows
+- **Analytics Dashboard**: Performance metrics và reporting
+- **Responsive Design**: Mobile-friendly với touch optimization
+
+## Interface Components
+
+### 🎛️ Header Controls
+
+```typescript
+interface CampaignHeader {
+ title: string; // "Campaigns"
+ searchBar: SearchInput; // Global search
+ filters: FilterBar; // Status, date, team filters
+ createButton: Button; // "Create Campaign"
+ viewToggle: ViewToggle; // Grid/List view switch
+}
+```
+
+### 📊 Campaign Grid
+
+```typescript
+interface CampaignGrid {
+ campaigns: CampaignCard[]; // Campaign cards
+ pagination: Pagination; // Page navigation
+ emptyState: EmptyState; // No campaigns message
+ loadingState: LoadingState; // Loading skeleton
+}
+```
+
+### 🎨 Campaign Card
+
+```typescript
+interface CampaignCard {
+ header: {
+ title: string; // Campaign name
+ status: Badge; // Status indicator
+ priority: PriorityBadge; // Priority level
+ };
+ content: {
+ description: string; // Campaign description
+ metrics: MetricRow[]; // Performance metrics
+ team: AvatarGroup; // Team members
+ };
+ actions: {
+ edit: Button; // Edit button
+ duplicate: Button; // Duplicate button
+ delete: Button; // Delete button
+ };
+}
+```
+
+### 📝 Campaign Creation Modal
+
+```typescript
+interface CampaignCreationModal {
+ header: {
+ team: string; // "team"
+ title: string; // "New Campaign"
+ closeButton: Button; // Close [✕] button
+ };
+ form: {
+ name: Input; // Campaign name input
+ summary: Input; // Short summary input
+ attributes: AttributeRow[]; // Status, lead, members, dates, labels
+ description: TextArea; // Rich description area
+ milestones: MilestoneSection; // Milestones management
+ };
+ actions: {
+ cancel: Button; // Cancel button
+ create: Button; // Create campaign button
+ };
+}
+```
+
+### 🎯 Attribute Row Components
+
+```typescript
+interface AttributeRow {
+ status: StatusSelector; // draft, planning, ready, Done, Canceled
+ lead: LeadSelector; // Single person assignment (👤)
+ members: MemberSelector; // Multiple team members or teams (👥)
+ startDate: DateSelector; // Campaign start date (📅)
+ targetDate: DateSelector; // Campaign target date (📅)
+ labels: LabelSelector; // Custom tags (🏷️)
+}
+```
+
+## Campaign List View
+
+### 📱 List Page Layout
+
+**Page Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Campaigns [Search Campaigns...] │
+│ [All] [Active] [Draft] [Completed] [Archived] │
+├─────────────────────────────────────────────────────────┤
+│ [Create Campaign] [Grid View] [List View] │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📋 Campaigns Overview Table │ │
+│ ├─────────────────────────────────────────────────────┤ │
+│ │ Title │ Health │ Total Tasks │ PIC │ Timeline │ Status │ │
+│ ├─────────────────────────────────────────────────────┤ │
+│ │ 🚀 Summer Sale 2024 │ 🟢 On Track │ 15 │ 👥 John D, Sarah M, Mike R │ Aug 1 → Aug 31 │ 🟡 Planning │ [📋] │ │
+│ ├─────────────────────────────────────────────────────┤ │
+│ │ 🎯 Q4 Product Launch │ 🟡 At Risk │ 8 │ 👥 Sarah M, Mike R │ Sep 1 → Sep 30 │ 🔵 Draft │ [📋] │ │
+│ ├─────────────────────────────────────────────────────┤ │
+│ │ 🌟 Brand Awareness │ 🔴 Off Track │ 12 │ 👥 Mike R, Team A │ Oct 1 → Oct 31 │ 🔴 Canceled │ [📋] │ │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ Showing 1-10 of 24 campaigns │
+│ [← Previous] [1] [2] [3] [Next →] │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Filter Options:**
+
+- **Health**: All, On Track, At Risk, Off Track
+- **Status**: All, Draft, Planning, Ready, Done, Canceled
+- **Team**: All teams, Specific team members
+- **Date Range**: Last 7 days, Last 30 days, Custom range
+
+**View Options:**
+
+- **Table View**: Detailed table layout (default)
+- **Grid View**: Card-based layout
+- **Compact View**: Minimal information
+
+## Campaign Detail View
+
+### 📱 Detail Page Layout
+
+**Page Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ ← Back to Campaigns 🚀 Summer Sale 2024 [Edit] │
+│ [Active] [High Priority] [Created: Jan 1, 2024] │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📋 Overview │
+│ │ Boost summer sales with targeted social media... │
+│ │ 🎯 Goal: Increase sales by 25% │
+│ │ 💰 Budget: $10,000 │
+│ │ 📅 Duration: Jun 1 - Aug 31, 2024 │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ [Overview] [Content] [Analytics] [Team] [Settings] │
+├─────────────────────────────────────────────────────────┤
+│ Content Tab Content │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📝 Content Items (12) │
+│ │ [Create Content] [Import Content] │
+│ │ • Summer Sale Post 1 - Instagram │
+│ │ • Summer Sale Post 2 - Facebook │
+│ │ • Summer Sale Post 3 - LinkedIn │
+│ └─────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Tab Navigation:**
+
+- **Overview**: Campaign summary và key metrics
+- **Content**: Linked content items và creation
+- **Analytics**: Performance data và insights
+- **Team**: Team members và permissions
+- **Settings**: Campaign configuration
+
+### 📋 Right Side Panel Layout
+
+**Panel Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Campaign Details [✕] │
+├─────────────────────────────────────────────────────────┤
+│ 🚀 Summer Sale 2024 │
+│ 🟢 On Track • 🟡 Planning • High Priority │
+├─────────────────────────────────────────────────────────┤
+│ 📋 Tasks (15) │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ ✅ Create campaign brief │
+│ │ ✅ Design social media graphics │
+│ │ ⏳ Write Instagram captions (3/5) │
+│ │ ⏳ Schedule posts (8/15) │
+│ │ ⏳ Monitor performance │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ 👥 Team Members │
+│ • John D (Campaign Manager) │
+│ • Sarah M (Content Creator) │
+│ • Mike R (Designer) │
+├─────────────────────────────────────────────────────────┤
+│ 📅 Timeline │
+│ Start: Aug 1, 2024 │
+│ End: Aug 31, 2024 │
+│ Duration: 31 days │
+└─────────────────────────────────────────────────────────┘
+```
+
+## Campaign Creation Form
+
+### 📱 Creation Form Layout
+
+**Modal Behavior:**
+
+- **Centered Position**: Modal appears in center of screen
+- **Dark Theme**: Consistent với app theme
+- **Close Button**: [✕] button at top right
+- **Overlay Background**: Dimmed main content behind modal
+- **Responsive Design**: Adapts to different screen sizes
+
+**Form Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ team • New Campaign [✕] │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Campaign Name [Enter name...] │
+│ │ Summary [Add a short summary...] │
+│ ├─────────────────────────────────────────────────────┤
+│ │ [status] [Lead 👤] [Members 👥] [Start 📅] [Target 📅] [Labels 🏷️] │
+│ ├─────────────────────────────────────────────────────┤
+│ │ Description │
+│ │ ┌─────────────────────────────────────────────────┐ │
+│ │ │ Write a description, a campaign brief, or │
+│ │ │ collect ideas... │
+│ │ │ │
+│ │ │ │
+│ │ └─────────────────────────────────────────────────┘ │
+│ ├─────────────────────────────────────────────────────┤
+│ │ Milestones │
+│ │ [+] Add milestone │
+│ ├─────────────────────────────────────────────────────┤
+│ │ [Cancel] [Create Campaign] │
+│ └─────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Form Fields:**
+
+- **Campaign Name**: Required text input (large field)
+- **Summary**: Short description field (optional)
+- **Status**: draft, planning, ready, Done, Canceled
+- **Lead**: Single person assignment (👤 icon)
+- **Members**: Multiple team members (👥 icon)
+- **Start Date**: Campaign start date (📅 icon)
+- **Target Date**: Campaign target date (📅 icon)
+- **Labels**: Custom tags và categories (🏷️ icon)
+- **Description**: Rich text area for detailed informations
+- **Milestones**: Project milestones và checkpoints
+
+**Validation Rules:**
+
+- **Campaign Name**: Required, min 3 characters, max 100 characters
+- **Summary**: Optional, max 200 characters
+- **Status**: Required, must be valid status value
+- **Lead**: Required, must be valid team member
+- **Members**: Optional, can be empty, could be a member or teams
+- **Start Date**: Required, must be valid date
+- **Target Date**: Required, must be after start date
+- **Labels**: Optional, max 10 labels per campaign
+- **Description**: Optional, max 2000 characters
+- **Milestones**: Optional, max 20 milestones per campaign
+
+## Campaign Management
+
+### 📊 Status Management
+
+**Status Flow:**
+
+```
+DRAFT → PLANNING → READY → DONE
+ ↓ ↓ ↓ ↓
+ Edit Review Start Complete
+ Save Approve Launch Archive
+```
+
+**Status Indicators:**
+
+- 🔵 **Draft**: Blue badge - "Draft"
+- 🟡 **Planning**: Yellow badge - "Planning"
+- 🟢 **Ready**: Green badge - "Ready"
+- 🟠 **Done**: Orange badge - "Done"
+- 🔴 **Canceled**: Red badge - "Canceled"
+
+### 🎯 Health Management
+
+**Health Levels:**
+
+- 🟢 **On Track**: Green badge - "On Track"
+- 🟡 **At Risk**: Yellow badge - "At Risk"
+- 🔴 **Off Track**: Red badge - "Off Track"
+
+**Health Rules:**
+
+- **On Track**: Campaign progressing as planned
+- **At Risk**: Some delays or issues, but manageable
+- **Off Track**: Significant delays or major issues
+
+### 🎯 Priority Management
+
+**Priority Levels:**
+
+- 🔴 **High**: Red badge - "High Priority"
+- 🟡 **Medium**: Yellow badge - "Medium Priority"
+- 🟢 **Low**: Green badge - "Low Priority"
+
+**Priority Rules:**
+
+- **High**: Urgent campaigns, tight deadlines
+- **Medium**: Standard campaigns, normal timeline
+- **Low**: Background campaigns, flexible timeline
+
+### 📋 Task Management
+
+**Task Structure:**
+
+- **Main Tasks**: Primary campaign activities
+- **Subtasks**: Breakdown of main tasks
+- **Posts**: Content creation tasks (can be tasks too)
+- **Dependencies**: Task relationships và sequences
+
+**Task Types:**
+
+- **Content Creation**: Social media posts, blog articles
+- **Design Tasks**: Graphics, videos, layouts
+- **Approval Tasks**: Content review, campaign approval
+- **Publishing Tasks**: Schedule và publish content
+
+### 🎨 Right Side Panel
+
+**Panel Features:**
+
+- **📋 Icon**: Click to open campaign details panel
+- **Auto-hide**: Panel automatically hides when not in use
+- **Slide Animation**: Smooth right-to-left slide effect
+- **Campaign Details**: Comprehensive campaign information
+- **Task List**: All tasks và subtasks
+- **Team Management**: Member assignments và roles
+- **Timeline View**: Detailed schedule breakdown
+
+## User Interactions
+
+### 🖱️ Mouse Interactions
+
+**Click Actions:**
+
+- **Campaign Selection**: Click row để view details
+- **📋 Icon Click**: Open right side panel với campaign details
+- **Create Campaign**: Click "+ Add project" để open modal
+- **Quick Actions**: Hover để reveal action buttons
+- **Bulk Operations**: Select multiple campaigns
+- **Navigation**: Tab switching, pagination
+- **Modal Close**: Click [✕] hoặc overlay để close
+
+**Hover Effects:**
+
+- **Card Hover**: Subtle shadow + action buttons
+- **Button Hover**: Color changes + tooltips
+- **Status Hover**: Additional information
+
+**Drag & Drop:**
+
+- **Status Change**: Drag status badges
+- **Priority Change**: Drag priority indicators
+- **Team Assignment**: Drag team members
+
+### ⌨️ Keyboard Navigation
+
+**Tab Order:**
+
+1. Search bar
+2. Filter controls
+3. Create button
+4. Campaign cards
+5. Action buttons
+6. Pagination controls
+
+**Keyboard Shortcuts:**
+
+- **Enter**: Open campaign details
+- **Space**: Select campaign
+- **Delete**: Delete selected campaign
+- **Escape**: Close modals
+- **Ctrl/Cmd + A**: Select all campaigns
+- **Ctrl/Cmd + N**: Open new campaign modal
+- **Tab**: Navigate between form fields
+- **Shift + Tab**: Navigate backwards through fields
+
+### 📱 Touch Interactions
+
+**Mobile Gestures:**
+
+- **Tap**: Select campaign
+- **Long Press**: Show context menu
+- **Swipe**: Navigate between tabs
+- **Pinch**: Zoom campaign grid
+
+**Touch Optimization:**
+
+- **Touch Targets**: Minimum 44px size
+- **Gesture Support**: Native touch gestures
+- **Responsive Layout**: Mobile-first design
+
+## Performance Considerations
+
+### 🚀 Data Loading
+
+**Pagination Strategy:**
+
+```typescript
+interface PaginationConfig {
+ pageSize: 20; // Items per page
+ preloadPages: 2; // Preload adjacent pages
+ virtualScrolling: true; // Virtual scroll for large lists
+}
+```
+
+**Lazy Loading:**
+
+- **Initial Load**: First page only
+- **On-demand**: Load pages as needed
+- **Background**: Preload next page
+
+**Caching Strategy:**
+
+- **Campaign Data**: Cache trong memory
+- **User Preferences**: Persist filters và settings
+- **Search Results**: Cache search queries
+
+### 🎨 Rendering Optimization
+
+**Component Memoization:**
+
+- **Campaign Cards**: Memoize card components
+- **Filter Controls**: Memoize filter components
+- **Pagination**: Memoize pagination controls
+
+**Virtual Scrolling:**
+
+- **Large Tables**: Virtual scroll cho 100+ campaigns
+- **Performance**: Render visible rows only
+- **Smooth Scrolling**: 60fps scrolling performance
+
+**Table Optimization:**
+
+- **Column Sorting**: Efficient sort algorithms
+- **Row Selection**: Optimized selection handling
+- **Filter Performance**: Fast filtering với large datasets
+
+**State Management:**
+
+- **Local State**: Component-level state
+- **Shared State**: Context providers
+- **Persistence**: URL state cho filters
+
+## Accessibility
+
+### ♿ WCAG 2.1 Compliance
+
+**Screen Reader Support:**
+
+- **ARIA Labels**: Descriptive labels cho all elements
+- **Live Regions**: Dynamic content updates
+- **Focus Management**: Logical tab order
+
+**Keyboard Navigation:**
+
+- **Full Keyboard Support**: All functions accessible
+- **Focus Indicators**: Clear focus states
+- **Skip Links**: Jump to main content
+
+**Color & Contrast:**
+
+- **High Contrast**: 4.5:1 minimum ratio
+- **Color Independence**: Not relying on color alone
+- **Visual Indicators**: Icons + text labels
+
+### 🎯 Specific Features
+
+**Campaign Table:**
+
+- **Semantic Structure**: Proper table headers và rows
+- **Status Announcements**: Screen reader announces status
+- **Action Descriptions**: Clear button descriptions
+- **Row Navigation**: Keyboard navigation between rows
+
+**Form Accessibility:**
+
+- **Label Associations**: Proper form labels
+- **Error Messages**: Clear error descriptions
+- **Validation Feedback**: Real-time validation
+
+**Navigation:**
+
+- **Tab Navigation**: Clear tab structure
+- **Breadcrumbs**: Navigation context
+- **Progress Indicators**: Multi-step progress
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Design Team_
diff --git a/docs/ui/content-editor.md b/docs/ui/content-editor.md
new file mode 100644
index 00000000..191d0c95
--- /dev/null
+++ b/docs/ui/content-editor.md
@@ -0,0 +1,400 @@
+# Content Editor UI Guide
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Interface Components](#interface-components)
+- [Editor Layout](#editor-layout)
+- [Rich Text Features](#rich-text-features)
+- [Content Management](#content-management)
+- [AI Integration](#ai-integration)
+- [User Interactions](#user-interactions)
+- [Performance Considerations](#performance-considerations)
+- [Accessibility](#accessibility)
+
+## Overview
+
+Content Editor UI là giao diện soạn thảo nội dung cho AiM Platform với rich text editor, AI assistance, và content management features. Interface hỗ trợ multiple content types, collaboration, và approval workflows.
+
+### 🎯 Key Features
+
+- **Rich Text Editor**: TipTap-based editor với advanced formatting
+- **AI Assistance**: Content generation, translation, optimization
+- **Content Management**: Draft saving, version control, collaboration
+- **Multi-platform Support**: Social media, blog, email content
+- **Responsive Design**: Mobile-friendly với touch optimization
+
+## Interface Components
+
+### 🎛️ Editor Header
+
+```typescript
+interface EditorHeader {
+ title: Input; // Content title
+ status: Badge; // Draft/Submitted/Approved
+ platform: Select; // Platform selection
+ campaign: Select; // Campaign assignment
+ actions: {
+ save: Button; // Save draft
+ preview: Button; // Preview content
+ submit: Button; // Submit for approval
+ publish: Button; // Publish directly
+ };
+}
+```
+
+### 📝 Main Editor
+
+```typescript
+interface MainEditor {
+ toolbar: EditorToolbar; // Formatting tools
+ contentArea: ContentArea; // Main editing area
+ wordCount: WordCounter; // Character/word count
+ autoSave: AutoSave; // Auto-save indicator
+}
+```
+
+### 🎨 Sidebar Panel
+
+```typescript
+interface EditorSidebar {
+ tabs: {
+ properties: Tab; // Content properties
+ assets: Tab; // Media assets
+ ai: Tab; // AI assistance
+ history: Tab; // Version history
+ };
+ content: TabContent; // Tab-specific content
+}
+```
+
+## Editor Layout
+
+### 📱 Main Page Layout
+
+**Page Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ [← Back] Content Editor [Save] [Preview] │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Title: [Enter content title...] │ │
+│ │ Platform: [Instagram] Campaign: [Summer Sale 2024] │ │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ [B] [I] [U] [Link] [Image] [List] [Quote] [Code] [AI] │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ │
+│ │ Start writing your content here... │
+│ │ │
+│ │ You can use the toolbar above to format your text. │
+│ │ │
+│ │ [AI Assistant] can help you generate ideas! │
+│ │ │
+│ └─────────────────────────────────────────────────────┘ │
+│ Words: 45 | Characters: 234 | Auto-saved 2 min ago │
+├─────────────────────────────────────────────────────────┤
+│ [Properties] [Assets] [AI] [History] │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Properties Tab Content │
+│ │ • Content Type: Social Media Post │
+│ │ • Target Audience: Young Professionals │
+│ │ • Tone: Professional but Friendly │
+│ │ • Tags: #summer #sale #marketing │
+│ └─────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Layout Sections:**
+
+- **Header**: Title, platform, campaign, actions
+- **Toolbar**: Formatting tools và AI assistance
+- **Editor**: Main content area với rich text
+- **Status Bar**: Word count, auto-save status
+- **Sidebar**: Properties, assets, AI, history tabs
+
+## Rich Text Features
+
+### 🎨 Formatting Toolbar
+
+**Toolbar Structure:**
+
+```
+[B] [I] [U] [S] | [H1] [H2] [H3] | [List] [Ordered] | [Quote] [Code] | [Link] [Image] [Video] | [AI] [Settings]
+```
+
+**Text Formatting:**
+
+- **Bold**: `**Bold text**` - Strong emphasis
+- **Italic**: `*Italic text*` - Light emphasis
+- **Underline**: `Underlined text ` - Additional emphasis
+- **Strikethrough**: `~~Strikethrough text~~` - Removed content
+
+**Heading Levels:**
+
+- **H1**: Main section headings
+- **H2**: Subsection headings
+- **H3**: Sub-subsection headings
+
+**List Types:**
+
+- **Bullet List**: Unordered list items
+- **Numbered List**: Ordered list items
+- **Checklist**: Task completion items
+
+**Special Elements:**
+
+- **Quote**: Blockquote với citation
+- **Code**: Inline code và code blocks
+- **Link**: Hyperlinks với preview
+- **Image**: Image insertion với alt text
+- **Video**: Video embedding
+
+### 📱 Content Types
+
+**Social Media Posts:**
+
+- **Instagram**: Image + caption (2200 chars)
+- **Facebook**: Text + media (63,206 chars)
+- **LinkedIn**: Professional content (3000 chars)
+- **Twitter**: Short posts (280 chars)
+
+**Blog Content:**
+
+- **Headings**: H1, H2, H3 structure
+- **Paragraphs**: Long-form content
+- **Media**: Images, videos, embeds
+- **SEO**: Meta descriptions, keywords
+
+**Email Content:**
+
+- **Subject Line**: Email subject
+- **Body**: Rich text content
+- **Signature**: Professional signature
+- **CTA**: Call-to-action buttons
+
+## Content Management
+
+### 💾 Draft Management
+
+**Auto-save Features:**
+
+- **Frequency**: Every 30 seconds
+- **Indicators**: "Auto-saved X min ago"
+- **Recovery**: Restore from auto-save
+- **Conflict Resolution**: Handle simultaneous saves
+
+**Version Control:**
+
+- **Draft Versions**: Save multiple versions
+- **Change Tracking**: Track content changes
+- **Revert Options**: Rollback to previous versions
+- **Collaboration**: Multiple editors support
+
+### 📋 Content Properties
+
+**Metadata Fields:**
+
+- **Content Type**: Post, article, email, etc.
+- **Target Audience**: Demographics, interests
+- **Tone**: Professional, casual, friendly, etc.
+- **Tags**: Categorization và search
+- **SEO**: Meta title, description, keywords
+
+**Platform Settings:**
+
+- **Character Limits**: Platform-specific limits
+- **Media Requirements**: Image dimensions, formats
+- **Hashtag Rules**: Platform hashtag guidelines
+- **Posting Times**: Optimal posting schedules
+
+## AI Integration
+
+### 🤖 AI Assistant Panel
+
+**AI Features:**
+
+- **Content Generation**: Generate content ideas
+- **Writing Assistance**: Improve writing quality
+- **Translation**: Multi-language support
+- **Optimization**: SEO và engagement optimization
+
+**AI Tools:**
+
+- **Content Ideas**: Generate topic suggestions
+- **Writing Prompts**: Creative writing assistance
+- **Grammar Check**: Spelling và grammar correction
+- **Tone Adjustment**: Modify content tone
+- **Length Optimization**: Adjust content length
+
+### 🎯 AI Workflow
+
+**Content Creation:**
+
+1. **Input**: Topic, audience, tone
+2. **Generation**: AI creates content draft
+3. **Editing**: Human review và refinement
+4. **Optimization**: AI suggests improvements
+5. **Finalization**: Human approval và publishing
+
+**AI Settings:**
+
+- **Model Selection**: GPT-4, GPT-3.5, etc.
+- **Creativity Level**: Conservative to creative
+- **Language**: Target language selection
+- **Style**: Formal, casual, technical, etc.
+
+## User Interactions
+
+### 🖱️ Mouse Interactions
+
+**Click Actions:**
+
+- **Toolbar**: Formatting tool selection
+- **Content Area**: Text cursor placement
+- **Sidebar**: Tab switching
+- **Actions**: Save, preview, submit buttons
+
+**Hover Effects:**
+
+- **Toolbar Items**: Tool descriptions
+- **Formatting**: Live preview of changes
+- **Buttons**: Hover states và tooltips
+
+**Drag & Drop:**
+
+- **Media**: Drag images/videos into editor
+- **Assets**: Drag from asset library
+- **Text**: Drag text selections
+
+### ⌨️ Keyboard Navigation
+
+**Editor Shortcuts:**
+
+- **Ctrl/Cmd + B**: Bold text
+- **Ctrl/Cmd + I**: Italic text
+- **Ctrl/Cmd + U**: Underline text
+- **Ctrl/Cmd + K**: Insert link
+- **Ctrl/Cmd + Shift + K**: Remove link
+- **Ctrl/Cmd + S**: Save draft
+- **Ctrl/Cmd + Enter**: Submit content
+
+**Navigation:**
+
+- **Tab**: Navigate between elements
+- **Arrow Keys**: Move cursor
+- **Home/End**: Beginning/end of line
+- **Ctrl/Cmd + Home/End**: Beginning/end of document
+
+### 📱 Touch Interactions
+
+**Mobile Gestures:**
+
+- **Tap**: Select elements
+- **Long Press**: Show context menu
+- **Double Tap**: Select word
+- **Pinch**: Zoom content
+
+**Touch Optimization:**
+
+- **Touch Targets**: Minimum 44px size
+- **Gesture Support**: Native touch gestures
+- **Mobile Toolbar**: Touch-friendly toolbar layout
+
+## Performance Considerations
+
+### 🚀 Editor Performance
+
+**Rendering Strategy:**
+
+```typescript
+interface EditorConfig {
+ debounceDelay: 300; // Auto-save delay
+ maxContentLength: 100000; // Maximum content size
+ lazyLoading: true; // Load features on-demand
+ virtualScrolling: true; // Large content handling
+}
+```
+
+**Memory Management:**
+
+- **Content Caching**: Cache recent content
+- **Asset Optimization**: Compress images/videos
+- **Garbage Collection**: Clean up unused resources
+- **Memory Monitoring**: Track memory usage
+
+**Auto-save Optimization:**
+
+- **Debounced Saves**: Prevent excessive saves
+- **Incremental Saves**: Save only changes
+- **Background Processing**: Non-blocking saves
+- **Conflict Resolution**: Handle simultaneous saves
+
+### 🎨 Rendering Optimization
+
+**Component Memoization:**
+
+- **Toolbar Items**: Memoize formatting tools
+- **Content Blocks**: Memoize content components
+- **Sidebar Tabs**: Memoize tab content
+
+**Content Rendering:**
+
+- **Lazy Rendering**: Render visible content only
+- **Virtual Scrolling**: Handle large documents
+- **Progressive Loading**: Load content progressively
+
+**State Management:**
+
+- **Local State**: Component-level state
+- **Shared State**: Editor context
+- **Persistence**: Auto-save và recovery
+
+## Accessibility
+
+### ♿ WCAG 2.1 Compliance
+
+**Screen Reader Support:**
+
+- **ARIA Labels**: Descriptive labels cho all elements
+- **Live Regions**: Dynamic content updates
+- **Focus Management**: Logical tab order
+
+**Keyboard Navigation:**
+
+- **Full Keyboard Support**: All functions accessible
+- **Focus Indicators**: Clear focus states
+- **Skip Links**: Jump to main content
+
+**Color & Contrast:**
+
+- **High Contrast**: 4.5:1 minimum ratio
+- **Color Independence**: Not relying on color alone
+- **Visual Indicators**: Icons + text labels
+
+### 🎯 Specific Features
+
+**Editor Accessibility:**
+
+- **Content Announcements**: Announce formatting changes
+- **Tool Descriptions**: Clear tool descriptions
+- **Error Messages**: Clear error descriptions
+
+**AI Features:**
+
+- **AI Status**: Announce AI processing status
+- **Content Changes**: Announce AI-generated content
+- **Suggestions**: Clear suggestion descriptions
+
+**Content Management:**
+
+- **Save Status**: Announce save status changes
+- **Version Information**: Announce version changes
+- **Collaboration**: Announce collaboration updates
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Design Team_
diff --git a/docs/ui/dashboards.md b/docs/ui/dashboards.md
new file mode 100644
index 00000000..d987be78
--- /dev/null
+++ b/docs/ui/dashboards.md
@@ -0,0 +1,420 @@
+# Dashboards UI Guide
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Interface Components](#interface-components)
+- [Dashboard Layouts](#dashboard-layouts)
+- [Widget System](#widget-system)
+- [Data Visualization](#data-visualization)
+- [User Interactions](#user-interactions)
+- [Performance Considerations](#performance-considerations)
+- [Accessibility](#accessibility)
+
+## Overview
+
+Dashboards UI là giao diện tổng quan và analytics cho AiM Platform với role-based dashboards, customizable widgets, và real-time data visualization. Interface hỗ trợ multiple user roles, data insights, và performance monitoring.
+
+### 🎯 Key Features
+
+- **Role-Based Dashboards**: Creator, Brand Owner, Admin views
+- **Customizable Widgets**: Drag & drop widget management
+- **Real-Time Data**: Live updates và performance metrics
+- **Data Visualization**: Charts, graphs, và analytics
+- **Responsive Design**: Mobile-friendly với touch optimization
+
+## Interface Components
+
+### 🎛️ Dashboard Header
+
+```typescript
+interface DashboardHeader {
+ title: string; // Dashboard title
+ periodSelector: Select; // Time period selection
+ refreshButton: Button; // Manual refresh
+ settingsButton: Button; // Dashboard settings
+ userInfo: UserInfo; // Current user context
+}
+```
+
+### 📊 Widget Grid
+
+```typescript
+interface WidgetGrid {
+ layout: GridLayout; // CSS Grid layout
+ widgets: Widget[]; // Dashboard widgets
+ emptyState: EmptyState; // No widgets message
+ loadingState: LoadingState; // Loading skeleton
+}
+```
+
+### 🎨 Widget Components
+
+```typescript
+interface Widget {
+ header: {
+ title: string; // Widget title
+ menu: DropdownMenu; // Widget actions
+ refresh: Button; // Refresh data
+ };
+ content: WidgetContent; // Widget-specific content
+ footer?: WidgetFooter; // Additional information
+ resize: ResizeHandle; // Resize controls
+}
+```
+
+## Dashboard Layouts
+
+### 📱 Creator Dashboard
+
+**Layout Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Creator Dashboard [7D] [30D] [90D] │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
+│ │ 📝 Active │ │ 📊 Draft │ │ ⏰ Scheduled │ │
+│ │ Campaigns │ │ Content │ │ Posts │ │
+│ │ 5 campaigns │ │ 12 items │ │ 8 posts │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📈 Content Performance (Last 30 Days) │ │
+│ │ [Line Chart: Engagement over time] │ │
+│ │ • Total Reach: 45.2K │ │
+│ │ • Total Clicks: 2.1K │ │
+│ │ • Avg. Engagement: 4.7% │ │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
+│ │ 🎯 AI │ │ 📱 Recent │ │ 🏆 Top │ │
+│ │ Suggestions │ │ Performance │ │ Performing │ │
+│ │ 3 new ideas │ │ Posts │ │ Content │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Key Widgets:**
+
+- **Campaign Overview**: Active campaigns count
+- **Content Status**: Draft, scheduled, published counts
+- **Performance Charts**: Engagement metrics
+- **AI Suggestions**: Content ideas và optimization
+- **Recent Activity**: Latest posts và performance
+
+### 📱 Brand Owner Dashboard
+
+**Layout Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Brand Owner Dashboard [7D] [30D] [90D] │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
+│ │ 💰 Budget │ │ 📊 ROI │ │ 👥 Team │ │
+│ │ Status │ │ Performance │ │ Performance │ │
+│ │ $8,500/$10,000 │ │ 156% │ │ 4 creators │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📈 Campaign Performance Overview │ │
+│ │ [Bar Chart: Campaign metrics] │ │
+│ │ • Summer Sale: 25.3K reach, 3.2K clicks │ │
+│ │ • Q4 Launch: 18.7K reach, 2.1K clicks │ │
+│ │ • Brand Awareness: 12.4K reach, 1.8K clicks │ │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
+│ │ ⏳ Approval │ │ 🎯 Creator │ │ 📱 Platform │ │
+│ │ Queue │ │ Leaderboard │ │ Performance │ │
+│ │ 7 pending │ │ Top performers │ │ Best channels│
+│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Key Widgets:**
+
+- **Financial Metrics**: Budget, ROI, spending
+- **Campaign Overview**: Performance summaries
+- **Approval Queue**: Content pending approval
+- **Team Performance**: Creator metrics
+- **Platform Insights**: Channel performance
+
+### 📱 Admin Dashboard
+
+**Layout Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Admin Dashboard [7D] [30D] [90D] │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
+│ │ 👥 Total │ │ 🚀 System │ │ 📊 API │ │
+│ │ Users │ │ Health │ │ Usage │ │
+│ │ 156 users │ │ 99.9% uptime │ │ 2.3M calls │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📈 System Performance Metrics │ │
+│ │ [Line Chart: Response times, errors] │ │
+│ │ • Avg Response Time: 145ms │ │
+│ │ • Error Rate: 0.02% │ │
+│ │ • Active Sessions: 89 │ │
+│ └─────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────┤
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
+│ │ 🔐 Security │ │ 📝 Recent │ │ ⚙️ Feature │ │
+│ │ Alerts │ │ Activity │ │ Flags │ │
+│ │ 2 warnings │ │ User actions │ │ Toggle │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+**Key Widgets:**
+
+- **System Metrics**: Users, health, performance
+- **Security Monitoring**: Alerts và threats
+- **Activity Logs**: User actions và system events
+- **Feature Management**: Feature flags và settings
+- **API Monitoring**: Usage và performance
+
+## Widget System
+
+### 🎨 Widget Types
+
+**Metric Widgets:**
+
+- **Number Display**: Single metric với trend
+- **Progress Bar**: Progress towards goal
+- **Status Indicator**: Current status với color coding
+- **Comparison**: Current vs previous period
+
+**Chart Widgets:**
+
+- **Line Chart**: Time-series data
+- **Bar Chart**: Categorical comparisons
+- **Pie Chart**: Distribution data
+- **Heatmap**: Time-based patterns
+- **Gauge**: Progress towards target
+
+**Content Widgets:**
+
+- **List Display**: Recent items với actions
+- **Table View**: Tabular data với sorting
+- **Card Grid**: Visual content display
+- **Timeline**: Chronological events
+
+### 🔧 Widget Configuration
+
+**Widget Settings:**
+
+- **Data Source**: API endpoint, database query
+- **Refresh Rate**: Auto-refresh interval
+- **Size**: Small, medium, large, custom
+- **Position**: Grid coordinates
+- **Styling**: Colors, themes, borders
+
+**Widget Actions:**
+
+- **Refresh**: Manual data refresh
+- **Configure**: Edit widget settings
+- **Duplicate**: Copy widget
+- **Remove**: Delete widget
+- **Export**: Download data
+
+## Data Visualization
+
+### 📊 Chart Components
+
+**Line Charts:**
+
+- **Time Series**: Date/time on X-axis
+- **Trend Lines**: Moving averages, forecasts
+- **Annotations**: Important events, milestones
+- **Zoom**: Pan và zoom functionality
+
+**Bar Charts:**
+
+- **Grouped Bars**: Multiple series
+- **Stacked Bars**: Cumulative values
+- **Horizontal Bars**: Long labels
+- **Error Bars**: Confidence intervals
+
+**Pie Charts:**
+
+- **Donut Charts**: Center space for totals
+- **Exploded Slices**: Highlight segments
+- **Labels**: Inside/outside positioning
+- **Colors**: Consistent color schemes
+
+### 🎨 Visual Design
+
+**Color Schemes:**
+
+- **Brand Colors**: Primary, secondary, accent
+- **Semantic Colors**: Success, warning, error
+- **Accessibility**: High contrast ratios
+- **Consistency**: Unified color palette
+
+**Typography:**
+
+- **Headers**: Clear hierarchy
+- **Labels**: Readable font sizes
+- **Numbers**: Monospace for alignment
+- **Legends**: Descriptive text
+
+## User Interactions
+
+### 🖱️ Mouse Interactions
+
+**Click Actions:**
+
+- **Widget Selection**: Click to select widget
+- **Data Points**: Click chart elements
+- **Menu Items**: Dropdown menu selection
+- **Navigation**: Widget navigation
+
+**Hover Effects:**
+
+- **Data Tooltips**: Show detailed information
+- **Widget Highlight**: Focus on hovered widget
+- **Interactive Elements**: Button hover states
+
+**Drag & Drop:**
+
+- **Widget Movement**: Reposition widgets
+- **Widget Resizing**: Resize widget dimensions
+- **Widget Reordering**: Change widget order
+
+### ⌨️ Keyboard Navigation
+
+**Tab Order:**
+
+1. Dashboard header
+2. Period selector
+3. Widget grid
+4. Individual widgets
+5. Widget actions
+
+**Keyboard Shortcuts:**
+
+- **Tab**: Navigate between elements
+- **Arrow Keys**: Navigate widgets
+- **Enter**: Activate selected element
+- **Space**: Toggle selections
+- **Escape**: Close modals/menus
+
+### 📱 Touch Interactions
+
+**Mobile Gestures:**
+
+- **Tap**: Select elements
+- **Long Press**: Show context menu
+- **Swipe**: Navigate between dashboards
+- **Pinch**: Zoom charts
+
+**Touch Optimization:**
+
+- **Touch Targets**: Minimum 44px size
+- **Gesture Support**: Native touch gestures
+- **Mobile Layout**: Responsive grid system
+
+## Performance Considerations
+
+### 🚀 Data Loading
+
+**Loading Strategy:**
+
+```typescript
+interface LoadingConfig {
+ initialLoad: 'skeleton' | 'spinner'; // Loading state
+ refreshInterval: number; // Auto-refresh (ms)
+ lazyLoading: boolean; // Load on demand
+ caching: boolean; // Cache data
+}
+```
+
+**Data Fetching:**
+
+- **Parallel Requests**: Multiple widgets simultaneously
+- **Request Batching**: Group similar requests
+- **Error Handling**: Graceful degradation
+- **Retry Logic**: Automatic retry on failure
+
+**Caching Strategy:**
+
+- **Memory Cache**: Store recent data
+- **Local Storage**: Persist user preferences
+- **Cache Invalidation**: Smart cache updates
+- **Background Sync**: Update data in background
+
+### 🎨 Rendering Optimization
+
+**Widget Rendering:**
+
+- **Lazy Loading**: Render visible widgets only
+- **Virtual Scrolling**: Handle many widgets
+- **Component Memoization**: Prevent unnecessary re-renders
+- **Debounced Updates**: Limit update frequency
+
+**Chart Performance:**
+
+- **Data Sampling**: Reduce data points for large datasets
+- **Progressive Rendering**: Render charts progressively
+- **Canvas vs SVG**: Choose based on data size
+- **Animation Optimization**: Smooth 60fps animations
+
+**State Management:**
+
+- **Local State**: Widget-specific state
+- **Shared State**: Dashboard context
+- **Persistence**: Save user preferences
+
+## Accessibility
+
+### ♿ WCAG 2.1 Compliance
+
+**Screen Reader Support:**
+
+- **ARIA Labels**: Descriptive labels cho all elements
+- **Live Regions**: Dynamic content updates
+- **Focus Management**: Logical tab order
+
+**Keyboard Navigation:**
+
+- **Full Keyboard Support**: All functions accessible
+- **Focus Indicators**: Clear focus states
+- **Skip Links**: Jump to main content
+
+**Color & Contrast:**
+
+- **High Contrast**: 4.5:1 minimum ratio
+- **Color Independence**: Not relying on color alone
+- **Visual Indicators**: Icons + text labels
+
+### 🎯 Specific Features
+
+**Chart Accessibility:**
+
+- **Data Descriptions**: Screen reader announces data
+- **Keyboard Navigation**: Navigate chart elements
+- **Alternative Text**: Describe chart content
+
+**Widget Accessibility:**
+
+- **Widget Announcements**: Announce widget updates
+- **Action Descriptions**: Clear button descriptions
+- **Status Information**: Announce status changes
+
+**Dashboard Navigation:**
+
+- **Landmark Regions**: Clear page structure
+- **Heading Hierarchy**: Logical content organization
+- **Progress Indicators**: Show loading progress
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Design Team_
diff --git a/docs/ui/schedule.md b/docs/ui/schedule.md
new file mode 100644
index 00000000..5b78d1c4
--- /dev/null
+++ b/docs/ui/schedule.md
@@ -0,0 +1,459 @@
+# Schedule UI Guide
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Interface Components](#interface-components)
+- [View Modes](#view-modes)
+- [Draft Panel](#draft-panel)
+- [Drag & Drop Flow](#drag--drop-flow)
+- [User Interactions](#user-interactions)
+- [Performance Considerations](#performance-considerations)
+- [Accessibility](#accessibility)
+
+## Overview
+
+Schedule UI là giao diện chính để quản lý lịch trình xuất bản content trên các nền tảng social media. Interface hỗ trợ 3 chế độ xem (Day/Week/Month), draft panel, và drag-and-drop functionality để tạo schedules.
+
+### 🎯 Key Features
+
+- **Multi-view Calendar**: Day, Week, Month views với navigation
+- **Draft Panel**: Right-side panel hiển thị content có status DRAFT
+- **Drag & Drop**: Kéo thả draft content vào time slots
+- **Smart Scheduling**: Conflict detection và timezone support
+- **Responsive Design**: Mobile-friendly với touch gestures
+
+## Interface Components
+
+### 🎛️ Header Controls
+
+```typescript
+interface HeaderControls {
+ navigation: {
+ previous: Button; // Go to previous period
+ next: Button; // Go to next period
+ today: Button; // Jump to current date
+ dateDisplay: string; // Current period label
+ };
+ filters: {
+ channels: Badge[]; // Selected channels
+ campaigns: Badge[]; // Selected campaigns
+ };
+ draftToggle: Switch; // Show/hide draft panel
+}
+```
+
+### 📅 View Tabs
+
+```typescript
+interface ViewTabs {
+ day: TabTrigger; // Day view (24-hour timeline)
+ week: TabTrigger; // Week view (7-day grid)
+ month: TabTrigger; // Month view (calendar layout)
+}
+```
+
+### 🎨 Main Layout
+
+```typescript
+interface ScheduleLayout {
+ mainGrid: CalendarGrid; // Main calendar area
+ draftPanel?: DraftPanel; // Right-side panel (conditional)
+ scheduleSheet?: Sheet; // Schedule confirmation modal
+}
+```
+
+## View Modes
+
+### 📅 Day View
+
+**Purpose**: Detailed hourly planning với 15-minute precision
+
+**Layout Structure:**
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Time Column │ Hour 0 │ Hour 1 │ Hour 2 │ ... │ Hour 23 │
+├─────────────┼────────┼────────┼────────┼─────┼─────────┤
+│ 12:00 AM │ 15min │ 15min │ 15min │ ... │ 15min │
+│ │ slots │ slots │ slots │ │ slots │
+├─────────────┼────────┼────────┼────────┼─────┼─────────┤
+│ 1:00 AM │ 15min │ 15min │ 15min │ ... │ 15min │
+│ │ slots │ slots │ slots │ │ slots │
+├─────────────┼────────┼────────┼────────┼─────┼─────────┤
+│ ... │ ... │ ... │ ... │ ... │ ... │
+└─────────────┴────────┴────────┴────────┴─────┴─────────┘
+```
+
+**Features:**
+
+- **Time Slots**: 15-minute precision cho detailed scheduling
+- **Current Time**: Highlight "now" với blue badge
+- **Past Slots**: Grayed out với visual indication
+- **Drop Zones**: Hover effects cho drag & drop
+
+**Visual Indicators:**
+
+- 🕐 **Current Hour**: Blue background với "Now" badge
+- ⏰ **Current Slot**: Blue border với dashed line
+- 🕛 **Past Time**: Gray background với muted colors
+- 🎯 **Drop Target**: Hover effects và visual feedback
+
+### 📊 Week View
+
+**Purpose**: Weekly overview với daily columns và hourly rows
+
+**Layout Structure:**
+
+```
+┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
+│ Time │ Monday │ Tuesday │Wednesday│Thursday │ Friday │Saturday │ Sunday │
+├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
+│ 12 AM │ 15min │ 15min │ 15min │ 15min │ 15min │ 15min │ 15min │
+│ │ slots │ slots │ slots │ slots │ slots │ slots │ slots │
+├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
+│ 1 AM │ 15min │ 15min │ 15min │ 15min │ 15min │ 15min │ 15min │
+│ │ slots │ slots │ slots │ slots │ slots │ slots │ slots │
+├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
+│ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │
+└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
+```
+
+**Features:**
+
+- **Daily Columns**: 7 days với consistent hourly rows
+- **Current Day**: Blue highlight với "Today" badge
+- **Compact Display**: 15-minute slots trong 4px height
+- **Schedule Preview**: Content preview trong slots
+
+**Visual Indicators:**
+
+- 📅 **Current Day**: Blue background với "Today" badge
+- 🕐 **Current Hour**: Blue highlight cho current time
+- 📱 **Schedules**: Blue cards với channel icons và titles
+- 🎯 **Drop Zones**: Hover effects cho drag & drop
+
+### 📆 Month View
+
+**Purpose**: Monthly overview với daily cells và schedule previews
+
+**Layout Structure:**
+
+```
+┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
+│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
+├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
+│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
+│ [Day │ [Day │ [Day │ [Day │ [Day │ [Day │ [Day │
+│ Cell] │ Cell] │ Cell] │ Cell] │ Cell] │ Cell] │ Cell] │
+├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
+│ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │
+│ [Day │ [Day │ [Day │ [Day │ [Day │ [Day │ [Day │
+│ Cell] │ Cell] │ Cell] │ Cell] │ Cell] │ Cell] │ Cell] │
+├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
+│ ... │ ... │ ... │ ... │ ... │ ... │ ... │
+└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
+```
+
+**Features:**
+
+- **Daily Cells**: 120px height với schedule previews
+- **Current Month**: Highlight current month dates
+- **Schedule Display**: Up to 3 schedules per day
+- **Drop Targets**: Full day drop zones
+
+**Visual Indicators:**
+
+- 📅 **Current Day**: Blue background với "Today" badge
+- 🌙 **Current Month**: Normal text color cho current month
+- 🌑 **Other Months**: Muted gray cho adjacent months
+- 📱 **Schedules**: Blue cards với channel icons và titles
+
+## Draft Panel
+
+### 📝 Panel Structure
+
+```typescript
+interface DraftPanel {
+ header: {
+ title: string; // "Draft Posts"
+ count: number; // Draft content count
+ };
+ search: Input; // Search drafts
+ channelSelector: Select; // Default channel
+ campaignFilter: Badge[]; // Campaign filter badges
+ contentList: ScrollArea; // Draft content items
+}
+```
+
+### 🔍 Search & Filtering
+
+**Search Functionality:**
+
+- **Text Search**: Title và body content
+- **Real-time**: Instant search results
+- **Highlight**: Search term highlighting
+
+**Campaign Filtering:**
+
+- **Multi-select**: Toggle campaigns on/off
+- **Visual State**: Selected vs unselected badges
+- **Dynamic Content**: Filter content list in real-time
+
+### 📱 Draft Content Items
+
+```typescript
+interface DraftItem {
+ channelIcon: string; // Platform emoji
+ campaignBadge: Badge; // Campaign name
+ assetIcons: Icon[]; // File type indicators
+ title: string; // Content title
+ body?: string; // Content preview
+ metadata: {
+ createdDate: string; // Creation date
+ dragHint: string; // "Drag to schedule"
+ };
+}
+```
+
+**Visual Design:**
+
+- 🎨 **Hover Effects**: Subtle background changes
+- 🖱️ **Drag Cursor**: `cursor-move` cho drag indication
+- 📱 **Asset Preview**: File type icons với count badges
+- 🏷️ **Campaign Tags**: Color-coded campaign badges
+
+## Drag & Drop Flow
+
+### 🎯 Drag Source (Draft Panel)
+
+**Drag Initiation:**
+
+1. **Mouse Down**: Start drag operation
+2. **Visual Feedback**: Item opacity → 50%
+3. **Drag Preview**: Show content thumbnail
+4. **Data Transfer**: Set `contentId` và `channel`
+
+**Drag Data:**
+
+```typescript
+interface DragItem {
+ type: 'DRAFT_CONTENT';
+ contentId: string;
+ channel: Channel;
+ preview?: {
+ title: string;
+ channelIcon: string;
+ thumbnail?: string;
+ };
+}
+```
+
+### 🎯 Drop Target (Calendar Grid)
+
+**Drop Zones:**
+
+- **Day View**: 15-minute time slots
+- **Week View**: 15-minute time slots
+- **Month View**: Full day cells
+
+**Drop Validation:**
+
+- ✅ **Valid Targets**: Future time slots
+- ⚠️ **Past Time**: Warning với confirmation
+- 🚫 **Invalid**: Disabled drop zones
+
+**Drop Handling:**
+
+```typescript
+interface DropResult {
+ contentId: string;
+ channel: Channel;
+ targetDate: Date;
+ targetTime: string;
+}
+```
+
+### 📋 Schedule Confirmation Sheet
+
+**Sheet Content:**
+
+```typescript
+interface ScheduleSheet {
+ header: {
+ title: string; // "Schedule Content"
+ icon: Icon; // Calendar icon
+ };
+ form: {
+ channel: Select; // Platform selection
+ date: Input; // Date picker
+ time: Select; // Time selection (15-min slots)
+ timezone: Select; // Timezone picker
+ };
+ preview: {
+ summary: string; // Schedule summary
+ warnings?: Alert[]; // Past time warnings
+ };
+ actions: {
+ cancel: Button; // Cancel button
+ submit: Button; // Schedule button
+ };
+}
+```
+
+**Form Validation:**
+
+- ✅ **Required Fields**: Channel, date, time, timezone
+- ⚠️ **Past Time**: Warning với confirmation
+- 🚫 **Invalid Data**: Disable submit button
+
+## User Interactions
+
+### 🖱️ Mouse Interactions
+
+**Click Actions:**
+
+- **Navigation**: Previous/next period buttons
+- **View Switching**: Tab selection
+- **Draft Toggle**: Show/hide draft panel
+- **Filter Selection**: Campaign và channel filters
+
+**Hover Effects:**
+
+- **Drop Zones**: Visual feedback cho drag targets
+- **Interactive Elements**: Button và badge hover states
+- **Schedule Items**: Hover previews cho content
+
+**Drag Operations:**
+
+- **Start Drag**: Mouse down trên draft items
+- **Drag Over**: Visual feedback trên drop zones
+- **Drop**: Release để create schedule
+
+### ⌨️ Keyboard Navigation
+
+**Tab Order:**
+
+1. Navigation controls (previous, next, today)
+2. View tabs (day, week, month)
+3. Filter controls (channels, campaigns)
+4. Draft toggle
+5. Draft panel content
+6. Calendar grid (if accessible)
+
+**Keyboard Shortcuts:**
+
+- **Enter**: Open draft panel
+- **Tab**: Navigate between elements
+- **Arrow Keys**: Navigate time slots
+- **Space**: Select time slots
+- **Escape**: Close modals/sheets
+
+### 📱 Touch Interactions
+
+**Mobile Gestures:**
+
+- **Tap**: Select elements
+- **Long Press**: Start drag operation
+- **Swipe**: Navigate between periods
+- **Pinch**: Zoom calendar views (future feature)
+
+**Touch Optimization:**
+
+- **Touch Targets**: Minimum 44px size
+- **Gesture Support**: Native drag & drop
+- **Responsive Layout**: Mobile-first design
+
+## Performance Considerations
+
+### 🚀 Data Loading
+
+**Window-based Fetching:**
+
+```typescript
+interface FetchStrategy {
+ day: { hours: 24; precision: '15min' };
+ week: { days: 7; precision: '15min' };
+ month: { days: 31; precision: 'day' };
+}
+```
+
+**Lazy Loading:**
+
+- **Initial Load**: Current view data only
+- **On-demand**: Load adjacent periods
+- **Background**: Prefetch next period
+
+**Caching Strategy:**
+
+- **Schedule Data**: Cache trong memory
+- **Content Data**: Cache draft content
+- **User Preferences**: Persist filters và settings
+
+### 🎨 Rendering Optimization
+
+**Virtual Scrolling:**
+
+- **Day View**: Render visible hours only
+- **Week View**: Render visible days only
+- **Month View**: Render visible weeks only
+
+**Component Memoization:**
+
+- **Grid Cells**: Memoize time slot components
+- **Schedule Items**: Memoize schedule displays
+- **Draft Items**: Memoize content list items
+
+**State Management:**
+
+- **Local State**: Component-level state
+- **Shared State**: Context providers
+- **Persistence**: URL state cho navigation
+
+## Accessibility
+
+### ♿ WCAG 2.1 Compliance
+
+**Screen Reader Support:**
+
+- **ARIA Labels**: Descriptive labels cho all elements
+- **Live Regions**: Dynamic content updates
+- **Focus Management**: Logical tab order
+
+**Keyboard Navigation:**
+
+- **Full Keyboard Support**: All functions accessible
+- **Focus Indicators**: Clear focus states
+- **Skip Links**: Jump to main content
+
+**Color & Contrast:**
+
+- **High Contrast**: 4.5:1 minimum ratio
+- **Color Independence**: Not relying on color alone
+- **Visual Indicators**: Icons + text labels
+
+### 🎯 Specific Features
+
+**Calendar Navigation:**
+
+- **Date Announcements**: Screen reader announces current date
+- **Period Changes**: Announce view changes
+- **Schedule Information**: Announce schedule details
+
+**Drag & Drop:**
+
+- **Alternative Actions**: Keyboard alternatives cho drag
+- **Status Announcements**: Announce drag states
+- **Drop Feedback**: Clear drop confirmation
+
+**Form Accessibility:**
+
+- **Label Associations**: Proper form labels
+- **Error Messages**: Clear error descriptions
+- **Validation Feedback**: Real-time validation
+
+---
+
+_Last Updated: 2025-01-02_
+_Version: 1.0_
+_Maintainer: Design Team_
diff --git a/features/calendar/drag.tsx b/features/calendar/drag.tsx
new file mode 100644
index 00000000..1a573833
--- /dev/null
+++ b/features/calendar/drag.tsx
@@ -0,0 +1,13 @@
+'use client';
+
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
+import { ReactNode } from 'react';
+
+interface DragDropProviderProps {
+ children: ReactNode;
+}
+
+export function DragDropProvider({ children }: DragDropProviderProps) {
+ return {children} ;
+}
diff --git a/features/calendar/hooks/useDraftContent.ts b/features/calendar/hooks/useDraftContent.ts
new file mode 100644
index 00000000..4b6f5d49
--- /dev/null
+++ b/features/calendar/hooks/useDraftContent.ts
@@ -0,0 +1,49 @@
+import { useState, useEffect } from 'react';
+import { Content } from '../types';
+
+export function useDraftContent(orgId: string, selectedCampaigns?: string[]) {
+ const [draftContent, setDraftContent] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchDraftContent = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // Build query parameters
+ const params = new URLSearchParams({
+ status: 'DRAFT',
+ });
+
+ if (selectedCampaigns && selectedCampaigns.length > 0) {
+ params.append('campaignId', selectedCampaigns.join(','));
+ }
+
+ const response = await fetch(`/api/${orgId}/content?${params.toString()}`);
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch draft content: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ if (result.ok && result.data) {
+ setDraftContent(result.data);
+ } else {
+ throw new Error(result.error?.message || 'Failed to fetch draft content');
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'An error occurred');
+ setDraftContent([]);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchDraftContent();
+ }, [orgId, selectedCampaigns]);
+
+ return { draftContent, isLoading, error };
+}
diff --git a/features/calendar/hooks/useSchedules.ts b/features/calendar/hooks/useSchedules.ts
new file mode 100644
index 00000000..4fe16f36
--- /dev/null
+++ b/features/calendar/hooks/useSchedules.ts
@@ -0,0 +1,92 @@
+import { useState, useEffect } from 'react';
+import {
+ format,
+ startOfDay,
+ endOfDay,
+ startOfWeek,
+ endOfWeek,
+ startOfMonth,
+ endOfMonth,
+} from 'date-fns';
+import { Schedule, ScheduleQuery } from '../types';
+
+interface UseSchedulesOptions {
+ view: 'day' | 'week' | 'month';
+ date: Date;
+ channels?: string[];
+ campaigns?: string[];
+}
+
+export function useSchedules(orgId: string, options: UseSchedulesOptions) {
+ const [schedules, setSchedules] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchSchedules = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // Calculate date range based on view
+ let from: Date;
+ let to: Date;
+
+ switch (options.view) {
+ case 'day':
+ from = startOfDay(options.date);
+ to = endOfDay(options.date);
+ break;
+ case 'week':
+ from = startOfWeek(options.date, { weekStartsOn: 1 });
+ to = endOfWeek(options.date, { weekStartsOn: 1 });
+ break;
+ case 'month':
+ from = startOfMonth(options.date);
+ to = endOfMonth(options.date);
+ break;
+ default:
+ from = startOfWeek(options.date, { weekStartsOn: 1 });
+ to = endOfWeek(options.date, { weekStartsOn: 1 });
+ }
+
+ // Build query parameters
+ const params = new URLSearchParams({
+ from: from.toISOString(),
+ to: to.toISOString(),
+ });
+
+ if (options.channels && options.channels.length > 0) {
+ params.append('channels', options.channels.join(','));
+ }
+
+ if (options.campaigns && options.campaigns.length > 0) {
+ params.append('campaigns', options.campaigns.join(','));
+ }
+
+ const response = await fetch(`/api/${orgId}/schedules?${params.toString()}`);
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch schedules: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ if (result.ok && result.data) {
+ setSchedules(result.data);
+ } else {
+ throw new Error(result.error?.message || 'Failed to fetch schedules');
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'An error occurred');
+ setSchedules([]);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchSchedules();
+ }, [orgId, options.view, options.date, options.channels, options.campaigns]);
+
+ return { schedules, isLoading, error };
+}
diff --git a/features/calendar/types.ts b/features/calendar/types.ts
new file mode 100644
index 00000000..26fb37a5
--- /dev/null
+++ b/features/calendar/types.ts
@@ -0,0 +1,74 @@
+export type Channel =
+ | 'FACEBOOK'
+ | 'INSTAGRAM'
+ | 'TWITTER'
+ | 'YOUTUBE'
+ | 'LINKEDIN'
+ | 'TIKTOK'
+ | 'BLOG';
+
+export type ContentStatus =
+ | 'DRAFT'
+ | 'SUBMITTED'
+ | 'APPROVED'
+ | 'SCHEDULED'
+ | 'PUBLISHED'
+ | 'REJECTED';
+
+export type ScheduleStatus = 'PENDING' | 'PUBLISHED' | 'FAILED' | 'CANCELLED';
+
+export interface Content {
+ id: string;
+ title: string;
+ body?: string;
+ status: ContentStatus;
+ campaignId: string;
+ campaign: {
+ id: string;
+ name: string;
+ };
+ assets: Asset[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Asset {
+ id: string;
+ url: string;
+ name?: string;
+ type: string;
+ size?: number;
+ description?: string;
+ tags: string[];
+}
+
+export interface Schedule {
+ id: string;
+ runAt: string; // ISO string
+ timezone: string;
+ channel: Channel;
+ status: ScheduleStatus;
+ campaignId: string;
+ campaign: {
+ id: string;
+ name: string;
+ };
+ contentId?: string;
+ content?: Content;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Campaign {
+ id: string;
+ name: string;
+ description?: string;
+ organizationId: string;
+}
+
+export interface ScheduleQuery {
+ view: 'day' | 'week' | 'month';
+ date: Date;
+ channels?: Channel[];
+ campaigns?: string[];
+}
diff --git a/features/calendar/ui/DraftPanel.tsx b/features/calendar/ui/DraftPanel.tsx
new file mode 100644
index 00000000..00842eb1
--- /dev/null
+++ b/features/calendar/ui/DraftPanel.tsx
@@ -0,0 +1,268 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+// import { ScrollArea } from '@/components/ui/scroll-area';
+import { Search, Filter, FileText, Image, Video, Calendar } from 'lucide-react';
+import { Content, Channel } from '../types';
+import { useDrag } from 'react-dnd';
+
+interface DraftPanelProps {
+ content: Content[];
+ isLoading: boolean;
+ selectedChannels: Channel[];
+ selectedCampaigns: string[];
+ onChannelsChange: (channels: Channel[]) => void;
+ onCampaignsChange: (campaigns: string[]) => void;
+}
+
+interface DraftItemProps {
+ content: Content;
+ onDragStart: (contentId: string, channel: Channel) => void;
+}
+
+function DraftItem({ content, onDragStart }: DraftItemProps) {
+ const [{ isDragging }, drag] = useDrag({
+ type: 'DRAFT_CONTENT',
+ item: { contentId: content.id, channel: 'FACEBOOK' as Channel }, // Default channel
+ collect: (monitor) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ });
+
+ const getChannelIcon = (channel: Channel) => {
+ switch (channel) {
+ case 'FACEBOOK':
+ return '📘';
+ case 'INSTAGRAM':
+ return '📷';
+ case 'TWITTER':
+ return '🐦';
+ case 'YOUTUBE':
+ return '📺';
+ case 'LINKEDIN':
+ return '💼';
+ case 'TIKTOK':
+ return '🎵';
+ case 'BLOG':
+ return '📝';
+ default:
+ return '📄';
+ }
+ };
+
+ const getAssetIcon = (type: string) => {
+ if (type.startsWith('image/')) return ;
+ if (type.startsWith('video/')) return ;
+ return ;
+ };
+
+ return (
+ onDragStart(content.id, 'FACEBOOK')}
+ >
+
+
+ {getChannelIcon('FACEBOOK')}
+
+ {content.campaign.name}
+
+
+
+ {content.assets.slice(0, 2).map((asset, index) => (
+
+ {getAssetIcon(asset.type)}
+
+ ))}
+ {content.assets.length > 2 && (
+
+ +{content.assets.length - 2}
+
+ )}
+
+
+
+
{content.title}
+
+ {content.body &&
{content.body}
}
+
+
+
{new Date(content.createdAt).toLocaleDateString()}
+
+
+ Drag to schedule
+
+
+
+ );
+}
+
+export function DraftPanel({
+ content,
+ isLoading,
+ selectedChannels,
+ selectedCampaigns,
+ onChannelsChange,
+ onCampaignsChange,
+}: DraftPanelProps) {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedChannel, setSelectedChannel] = useState('FACEBOOK');
+
+ // Get unique campaigns from content
+ const campaigns = useMemo(() => {
+ const unique = new Set(content.map((item) => item.campaign.name));
+ return Array.from(unique).sort();
+ }, [content]);
+
+ // Filter content based on search and selections
+ const filteredContent = useMemo(() => {
+ return content.filter((item) => {
+ const matchesSearch =
+ item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (item.body && item.body.toLowerCase().includes(searchTerm.toLowerCase()));
+
+ const matchesCampaign =
+ selectedCampaigns.length === 0 || selectedCampaigns.includes(item.campaign.name);
+
+ return matchesSearch && matchesCampaign;
+ });
+ }, [content, searchTerm, selectedCampaigns]);
+
+ const handleChannelChange = (channel: Channel) => {
+ setSelectedChannel(channel);
+ if (!selectedChannels.includes(channel)) {
+ onChannelsChange([...selectedChannels, channel]);
+ }
+ };
+
+ const handleCampaignToggle = (campaign: string) => {
+ if (selectedCampaigns.includes(campaign)) {
+ onCampaignsChange(selectedCampaigns.filter((c) => c !== campaign));
+ } else {
+ onCampaignsChange([...selectedCampaigns, campaign]);
+ }
+ };
+
+ const handleDragStart = (contentId: string, channel: Channel) => {
+ // This will be handled by the parent component
+ console.log('Drag started:', { contentId, channel });
+ };
+
+ if (isLoading) {
+ return (
+
+
+ Draft Posts
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Draft Posts
+ {filteredContent.length}
+
+
+
+ {/* Search */}
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+ {/* Channel Selection */}
+
+ Default Channel
+ handleChannelChange(value)}
+ >
+
+
+
+
+ 📘 Facebook
+ 📷 Instagram
+ 🐦 Twitter
+ 📺 YouTube
+ 💼 LinkedIn
+ 🎵 TikTok
+ 📝 Blog
+
+
+
+
+ {/* Campaign Filter */}
+ {campaigns.length > 0 && (
+
+
Filter by Campaign
+
+ {campaigns.map((campaign) => (
+ handleCampaignToggle(campaign)}
+ >
+ {campaign}
+
+ ))}
+
+
+ )}
+
+ {/* Draft Content List */}
+
+
+ Content
+ Drag to schedule
+
+
+
+ {filteredContent.length === 0 ? (
+
+
+
No draft content found
+ {searchTerm &&
Try adjusting your search
}
+
+ ) : (
+
+ {filteredContent.map((item) => (
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/features/calendar/ui/ScheduleSheet.tsx b/features/calendar/ui/ScheduleSheet.tsx
new file mode 100644
index 00000000..6c275412
--- /dev/null
+++ b/features/calendar/ui/ScheduleSheet.tsx
@@ -0,0 +1,377 @@
+'use client';
+
+import { useState } from 'react';
+import { format } from 'date-fns';
+import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Calendar, Clock, Globe, AlertCircle } from 'lucide-react';
+import { Channel } from '../types';
+
+interface ScheduleSheetProps {
+ isOpen: boolean;
+ onClose: () => void;
+ contentId: string;
+ channel: Channel;
+ onSubmit: (data: { contentId: string; channel: Channel; runAt: Date; timezone: string }) => void;
+}
+
+const TIMEZONES = [
+ { value: 'America/New_York', label: 'Eastern Time (ET)' },
+ { value: 'America/Chicago', label: 'Central Time (CT)' },
+ { value: 'America/Denver', label: 'Mountain Time (MT)' },
+ { value: 'America/Los_Angeles', label: 'Pacific Time (PT)' },
+ { value: 'Europe/London', label: 'London (GMT)' },
+ { value: 'Europe/Paris', label: 'Paris (CET)' },
+ { value: 'Asia/Tokyo', label: 'Tokyo (JST)' },
+ { value: 'Asia/Shanghai', label: 'Shanghai (CST)' },
+ { value: 'Australia/Sydney', label: 'Sydney (AEDT)' },
+];
+
+const TIME_SLOTS = [
+ '00:00',
+ '00:15',
+ '00:30',
+ '00:45',
+ '01:00',
+ '01:15',
+ '01:30',
+ '01:45',
+ '02:00',
+ '02:15',
+ '02:30',
+ '02:45',
+ '03:00',
+ '03:15',
+ '03:30',
+ '03:45',
+ '04:00',
+ '04:15',
+ '04:30',
+ '04:45',
+ '05:00',
+ '05:15',
+ '05:30',
+ '05:45',
+ '06:00',
+ '06:15',
+ '06:30',
+ '06:45',
+ '07:00',
+ '07:15',
+ '07:30',
+ '07:45',
+ '08:00',
+ '08:15',
+ '08:30',
+ '08:45',
+ '09:00',
+ '09:15',
+ '09:30',
+ '09:45',
+ '10:00',
+ '10:15',
+ '10:30',
+ '10:45',
+ '11:00',
+ '11:15',
+ '11:30',
+ '11:45',
+ '12:00',
+ '12:15',
+ '12:30',
+ '12:45',
+ '13:00',
+ '13:15',
+ '13:30',
+ '13:45',
+ '14:00',
+ '14:15',
+ '14:30',
+ '14:45',
+ '15:00',
+ '15:15',
+ '15:30',
+ '15:45',
+ '16:00',
+ '16:15',
+ '16:30',
+ '16:45',
+ '17:00',
+ '17:15',
+ '17:30',
+ '17:45',
+ '18:00',
+ '18:15',
+ '18:30',
+ '18:45',
+ '19:00',
+ '19:15',
+ '19:30',
+ '19:45',
+ '20:00',
+ '20:15',
+ '20:30',
+ '20:45',
+ '21:00',
+ '21:15',
+ '21:30',
+ '21:45',
+ '22:00',
+ '22:15',
+ '22:30',
+ '22:45',
+ '23:00',
+ '23:15',
+ '23:30',
+ '23:45',
+];
+
+export function ScheduleSheet({
+ isOpen,
+ onClose,
+ contentId,
+ channel,
+ onSubmit,
+}: ScheduleSheetProps) {
+ const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd'));
+ const [selectedTime, setSelectedTime] = useState('09:00');
+ const [selectedChannel, setSelectedChannel] = useState(channel);
+ const [selectedTimezone, setSelectedTimezone] = useState('America/New_York');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleSubmit = async () => {
+ if (!selectedDate || !selectedTime || !selectedChannel || !selectedTimezone) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ // Combine date and time
+ const dateTimeString = `${selectedDate}T${selectedTime}`;
+ const runAt = new Date(dateTimeString);
+
+ // Check if the selected time is in the past
+ if (runAt < new Date()) {
+ alert('Cannot schedule content in the past. Please select a future time.');
+ setIsSubmitting(false);
+ return;
+ }
+
+ await onSubmit({
+ contentId,
+ channel: selectedChannel,
+ runAt,
+ timezone: selectedTimezone,
+ });
+
+ onClose();
+ } catch (error) {
+ console.error('Error creating schedule:', error);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const getChannelIcon = (ch: Channel) => {
+ switch (ch) {
+ case 'FACEBOOK':
+ return '📘';
+ case 'INSTAGRAM':
+ return '📷';
+ case 'TWITTER':
+ return '🐦';
+ case 'YOUTUBE':
+ return '📺';
+ case 'LINKEDIN':
+ return '💼';
+ case 'TIKTOK':
+ return '🎵';
+ case 'BLOG':
+ return '📝';
+ default:
+ return '📄';
+ }
+ };
+
+ const getChannelName = (ch: Channel) => {
+ switch (ch) {
+ case 'FACEBOOK':
+ return 'Facebook';
+ case 'INSTAGRAM':
+ return 'Instagram';
+ case 'TWITTER':
+ return 'Twitter';
+ case 'YOUTUBE':
+ return 'YouTube';
+ case 'LINKEDIN':
+ return 'LinkedIn';
+ case 'TIKTOK':
+ return 'TikTok';
+ case 'BLOG':
+ return 'Blog';
+ default:
+ return 'Unknown';
+ }
+ };
+
+ return (
+
+
+
+
+
+ Schedule Content
+
+
+
+
+ {/* Channel Selection */}
+
+ Platform
+ setSelectedChannel(value)}
+ >
+
+
+
+
+ 📘 Facebook
+ 📷 Instagram
+ 🐦 Twitter
+ 📺 YouTube
+ 💼 LinkedIn
+ 🎵 TikTok
+ 📝 Blog
+
+
+
+
+ {/* Date Selection */}
+
+
Date
+
+
+ setSelectedDate(e.target.value)}
+ className="pl-10"
+ min={format(new Date(), 'yyyy-MM-dd')}
+ />
+
+
+
+ {/* Time Selection */}
+
+
Time
+
+
+
+
+
+
+
+ {TIME_SLOTS.map((time) => (
+
+ {time}
+
+ ))}
+
+
+
+
+
+ {/* Timezone Selection */}
+
+
Timezone
+
+
+
+
+
+
+
+ {TIMEZONES.map((tz) => (
+
+ {tz.label}
+
+ ))}
+
+
+
+
+
+ {/* Preview */}
+
+
Schedule Preview
+
+
+ Platform:
+
+ {getChannelIcon(selectedChannel)} {getChannelName(selectedChannel)}
+
+
+
+ Date:
+ {format(new Date(selectedDate), 'EEEE, MMMM d, yyyy')}
+
+
+ Time:
+
+ {selectedTime} ({selectedTimezone})
+
+
+
+
+
+ {/* Warning for past time */}
+ {selectedDate &&
+ selectedTime &&
+ (() => {
+ const dateTimeString = `${selectedDate}T${selectedTime}`;
+ const runAt = new Date(dateTimeString);
+ const now = new Date();
+
+ if (runAt < now) {
+ return (
+
+
+
+ Warning: Selected time is in the past. Please choose a future time.
+
+
+ );
+ }
+ return null;
+ })()}
+
+ {/* Actions */}
+
+
+ Cancel
+
+
+ {isSubmitting ? 'Scheduling...' : 'Schedule Content'}
+
+
+
+
+
+ );
+}
diff --git a/features/calendar/ui/ScheduleShell.tsx b/features/calendar/ui/ScheduleShell.tsx
new file mode 100644
index 00000000..a34dfa40
--- /dev/null
+++ b/features/calendar/ui/ScheduleShell.tsx
@@ -0,0 +1,309 @@
+'use client';
+
+import { useState, useCallback } from 'react';
+import { useSearchParams, useRouter, usePathname } from 'next/navigation';
+import {
+ format,
+ startOfWeek,
+ endOfWeek,
+ startOfMonth,
+ endOfMonth,
+ addDays,
+ addWeeks,
+ addMonths,
+ subDays,
+ subWeeks,
+ subMonths,
+} from 'date-fns';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Switch } from '@/components/ui/switch';
+import { Label } from '@/components/ui/label';
+import { ChevronLeft, ChevronRight, Calendar, Clock, Filter } from 'lucide-react';
+import { DayGrid } from './grid/DayGrid';
+import { WeekGrid } from './grid/WeekGrid';
+import { MonthGrid } from './grid/MonthGrid';
+import { DraftPanel } from './DraftPanel';
+import { ScheduleSheet } from './ScheduleSheet';
+import { useSchedules } from '../hooks/useSchedules';
+import { useDraftContent } from '../hooks/useDraftContent';
+import { Channel, Schedule } from '../types';
+import { DragDropProvider } from '../drag';
+
+interface ScheduleShellProps {
+ orgId: string;
+}
+
+type ViewType = 'day' | 'week' | 'month';
+
+export function ScheduleShell({ orgId }: ScheduleShellProps) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+
+ const [showDrafts, setShowDrafts] = useState(false);
+ const [selectedChannels, setSelectedChannels] = useState([]);
+ const [selectedCampaigns, setSelectedCampaigns] = useState([]);
+ const [isScheduleSheetOpen, setIsScheduleSheetOpen] = useState(false);
+ const [dragData, setDragData] = useState<{ contentId: string; channel: Channel } | null>(null);
+
+ // Get current view and date from URL
+ const currentView = (searchParams.get('view') as ViewType) || 'week';
+ const currentDate = searchParams.get('date') ? new Date(searchParams.get('date')!) : new Date();
+
+ // Fetch schedules and draft content
+ const { schedules, isLoading: schedulesLoading } = useSchedules(orgId, {
+ view: currentView,
+ date: currentDate,
+ channels: selectedChannels,
+ campaigns: selectedCampaigns,
+ });
+
+ const { draftContent, isLoading: draftsLoading } = useDraftContent(orgId, selectedCampaigns);
+
+ // Navigation functions
+ const navigateToDate = useCallback(
+ (date: Date) => {
+ const params = new URLSearchParams(searchParams);
+ params.set('date', format(date, 'yyyy-MM-dd'));
+ router.push(`${pathname}?${params.toString()}`);
+ },
+ [router, pathname, searchParams]
+ );
+
+ const navigateToView = useCallback(
+ (view: ViewType) => {
+ const params = new URLSearchParams(searchParams);
+ params.set('view', view);
+ router.push(`${pathname}?${params.toString()}`);
+ },
+ [router, pathname, searchParams]
+ );
+
+ const goToPrevious = useCallback(() => {
+ let newDate: Date;
+ switch (currentView) {
+ case 'day':
+ newDate = subDays(currentDate, 1);
+ break;
+ case 'week':
+ newDate = subWeeks(currentDate, 1);
+ break;
+ case 'month':
+ newDate = subMonths(currentDate, 1);
+ break;
+ default:
+ newDate = subWeeks(currentDate, 1);
+ }
+ navigateToDate(newDate);
+ }, [currentView, currentDate, navigateToDate]);
+
+ const goToNext = useCallback(() => {
+ let newDate: Date;
+ switch (currentView) {
+ case 'day':
+ newDate = addDays(currentDate, 1);
+ break;
+ case 'week':
+ newDate = addWeeks(currentDate, 1);
+ break;
+ case 'month':
+ newDate = addMonths(currentDate, 1);
+ break;
+ default:
+ newDate = addWeeks(currentDate, 1);
+ }
+ navigateToDate(newDate);
+ }, [currentView, currentDate, navigateToDate]);
+
+ const goToToday = useCallback(() => {
+ navigateToDate(new Date());
+ }, [navigateToDate]);
+
+ // Handle drag and drop
+ const handleDrop = useCallback(
+ (contentId: string, channel: Channel, targetDate: Date, targetTime: string) => {
+ setDragData({ contentId, channel });
+ setIsScheduleSheetOpen(true);
+ },
+ []
+ );
+
+ const handleScheduleCreate = useCallback(
+ async (scheduleData: {
+ contentId: string;
+ channel: Channel;
+ runAt: Date;
+ timezone: string;
+ }) => {
+ try {
+ const content = draftContent.find((c: any) => c.id === scheduleData.contentId);
+ if (!content) {
+ console.error('Content not found');
+ return;
+ }
+
+ const response = await fetch(`/api/${orgId}/schedules`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ ...scheduleData,
+ runAt: scheduleData.runAt.toISOString(),
+ campaignId: content.campaignId,
+ }),
+ });
+
+ if (response.ok) {
+ setIsScheduleSheetOpen(false);
+ setDragData(null);
+ // Refresh schedules
+ window.location.reload();
+ } else {
+ const error = await response.json();
+ console.error('Failed to create schedule:', error);
+ }
+ } catch (error) {
+ console.error('Error creating schedule:', error);
+ }
+ },
+ [orgId, draftContent]
+ );
+
+ const getViewTitle = () => {
+ switch (currentView) {
+ case 'day':
+ return format(currentDate, 'EEEE, MMMM d, yyyy');
+ case 'week':
+ const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
+ const weekEnd = endOfWeek(currentDate, { weekStartsOn: 1 });
+ return `${format(weekStart, 'MMM d')} - ${format(weekEnd, 'MMM d, yyyy')}`;
+ case 'month':
+ return format(currentDate, 'MMMM yyyy');
+ default:
+ return format(currentDate, 'MMMM yyyy');
+ }
+ };
+
+ return (
+
+
+ {/* Header with navigation and controls */}
+
+
+
+
+
+
+
+
+
+ Today
+
+
{getViewTitle()}
+
+
+
+ {/* Filters */}
+
+
+
+ {selectedChannels.length || 'All'} Channels
+
+
+ {selectedCampaigns.length || 'All'} Campaigns
+
+
+
+ {/* Draft Toggle */}
+
+
+ Draft Posts
+
+
+
+
+ {/* View Tabs */}
+
navigateToView(value as ViewType)}
+ className="w-full"
+ >
+
+
+
+ Day
+
+
+
+ Week
+
+
+
+ Month
+
+
+
+
+ {/* Main Calendar Grid */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Draft Panel */}
+ {showDrafts && (
+
+
+
+ )}
+
+
+
+ {/* Schedule Creation Sheet */}
+ {isScheduleSheetOpen && dragData && (
+
setIsScheduleSheetOpen(false)}
+ contentId={dragData.contentId}
+ channel={dragData.channel}
+ onSubmit={handleScheduleCreate}
+ />
+ )}
+
+
+ );
+}
diff --git a/features/calendar/ui/grid/DayGrid.tsx b/features/calendar/ui/grid/DayGrid.tsx
new file mode 100644
index 00000000..8529a94c
--- /dev/null
+++ b/features/calendar/ui/grid/DayGrid.tsx
@@ -0,0 +1,191 @@
+'use client';
+
+import { useDrop } from 'react-dnd';
+import { format, isToday, isSameDay } from 'date-fns';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Clock, Calendar } from 'lucide-react';
+import { Schedule, Channel } from '../../types';
+
+interface DropZoneProps {
+ hour: number;
+ minute: number;
+ onDrop: (hour: number, minute: number) => (item: any) => void;
+}
+
+function DropZone({ hour, minute, onDrop }: DropZoneProps) {
+ const [{ isOver, canDrop }, dropRef] = useDrop({
+ accept: 'DRAFT_CONTENT',
+ drop: onDrop(hour, minute),
+ collect: (monitor) => ({
+ isOver: monitor.isOver(),
+ canDrop: monitor.canDrop(),
+ }),
+ });
+
+ return (
+
+ );
+}
+
+interface DayGridProps {
+ date: Date;
+ schedules: Schedule[];
+ onDrop: (contentId: string, channel: Channel, targetDate: Date, targetTime: string) => void;
+ isLoading: boolean;
+}
+
+const HOURS = Array.from({ length: 24 }, (_, i) => i);
+
+export function DayGrid({ date, schedules, onDrop, isLoading }: DayGridProps) {
+ const isCurrentDay = isToday(date);
+
+ const handleDrop = (hour: number, minute: number) => {
+ return (item: any) => {
+ const targetDate = new Date(date);
+ targetDate.setHours(hour, minute, 0, 0);
+ const targetTime = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
+
+ onDrop(item.contentId, item.channel, targetDate, targetTime);
+ };
+ };
+
+ const getSchedulesForHour = (hour: number) => {
+ return schedules.filter((schedule) => {
+ const scheduleHour = new Date(schedule.runAt).getHours();
+ return scheduleHour === hour;
+ });
+ };
+
+ const getTimeLabel = (hour: number) => {
+ if (hour === 0) return '12 AM';
+ if (hour === 12) return '12 PM';
+ if (hour < 12) return `${hour} AM`;
+ return `${hour - 12} PM`;
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {HOURS.map((hour) => {
+ const hourSchedules = getSchedulesForHour(hour);
+ const isCurrentHour = isCurrentDay && new Date().getHours() === hour;
+
+ return (
+
+ {/* Hour Header */}
+
+
+
+ {getTimeLabel(hour)}
+ {isCurrentHour && (
+
+ Now
+
+ )}
+
+
+
+ {/* Hour Content */}
+
+ {/* 15-minute slots */}
+ {[0, 15, 30, 45].map((minute) => {
+ const slotTime = new Date(date);
+ slotTime.setHours(hour, minute, 0, 0);
+ const isPast = slotTime < new Date();
+ const isCurrentSlot =
+ isCurrentDay &&
+ new Date().getHours() === hour &&
+ Math.floor(new Date().getMinutes() / 15) * 15 === minute;
+
+ return (
+
+ {/* Drop Zone */}
+
+
+ {/* Time Label */}
+
+ {minute === 0 ? '' : `${minute.toString().padStart(2, '0')}`}
+
+
+ {/* Schedules in this slot */}
+ {hourSchedules
+ .filter((schedule) => {
+ const scheduleMinute = new Date(
+ schedule.runAt
+ ).getMinutes();
+ return Math.floor(scheduleMinute / 15) * 15 === minute;
+ })
+ .map((schedule) => (
+
+
+
+ {schedule.channel === 'FACEBOOK'
+ ? '📘'
+ : schedule.channel === 'INSTAGRAM'
+ ? '📷'
+ : schedule.channel === 'TWITTER'
+ ? '🐦'
+ : schedule.channel === 'YOUTUBE'
+ ? '📺'
+ : schedule.channel === 'LINKEDIN'
+ ? '💼'
+ : schedule.channel === 'TIKTOK'
+ ? '🎵'
+ : '📝'}
+
+
+ {schedule.content?.title || 'Untitled'}
+
+
+
+ {schedule.campaign.name}
+
+
+ ))}
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/features/calendar/ui/grid/MonthGrid.tsx b/features/calendar/ui/grid/MonthGrid.tsx
new file mode 100644
index 00000000..bb1858db
--- /dev/null
+++ b/features/calendar/ui/grid/MonthGrid.tsx
@@ -0,0 +1,198 @@
+'use client';
+
+import { useDrop } from 'react-dnd';
+import {
+ format,
+ startOfMonth,
+ endOfMonth,
+ eachDayOfInterval,
+ isToday,
+ isSameDay,
+ isSameMonth,
+ startOfWeek,
+ endOfWeek,
+} from 'date-fns';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Calendar, Clock } from 'lucide-react';
+import { Schedule, Channel } from '../../types';
+
+interface DropZoneProps {
+ day: Date;
+ onDrop: (day: Date) => (item: any) => void;
+}
+
+function DropZone({ day, onDrop }: DropZoneProps) {
+ const [{ isOver, canDrop }, dropRef] = useDrop({
+ accept: 'DRAFT_CONTENT',
+ drop: onDrop(day),
+ collect: (monitor) => ({
+ isOver: monitor.isOver(),
+ canDrop: monitor.canDrop(),
+ }),
+ });
+
+ return (
+
+ );
+}
+
+interface MonthGridProps {
+ date: Date;
+ schedules: Schedule[];
+ onDrop: (contentId: string, channel: Channel, targetDate: Date, targetTime: string) => void;
+ isLoading: boolean;
+}
+
+export function MonthGrid({ date, schedules, onDrop, isLoading }: MonthGridProps) {
+ const monthStart = startOfMonth(date);
+ const monthEnd = endOfMonth(date);
+ const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 });
+ const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 1 });
+
+ const calendarDays = eachDayOfInterval({
+ start: calendarStart,
+ end: calendarEnd,
+ });
+
+ const handleDrop = (day: Date) => {
+ return (item: any) => {
+ // Default to 9 AM for month view drops
+ const targetDate = new Date(day);
+ targetDate.setHours(9, 0, 0, 0);
+ const targetTime = '09:00';
+
+ onDrop(item.contentId, item.channel, targetDate, targetTime);
+ };
+ };
+
+ const getSchedulesForDay = (day: Date) => {
+ return schedules.filter((schedule) => {
+ return isSameDay(new Date(schedule.runAt), day);
+ });
+ };
+
+ const isCurrentMonth = (day: Date) => {
+ return isSameMonth(day, date);
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Weekday headers */}
+
+ {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((day) => (
+
+ {day}
+
+ ))}
+
+
+ {/* Calendar grid */}
+
+ {calendarDays.map((day) => {
+ const isCurrentDay = isToday(day);
+ const isInCurrentMonth = isCurrentMonth(day);
+ const daySchedules = getSchedulesForDay(day);
+
+ return (
+
+ {/* Day number */}
+
+ {format(day, 'd')}
+ {isCurrentDay && (
+
+ Today
+
+ )}
+
+
+ {/* Drop Zone */}
+
+
+ {/* Schedules for this day */}
+
+ {daySchedules.slice(0, 3).map((schedule) => (
+
+
+
+ {schedule.channel === 'FACEBOOK'
+ ? '📘'
+ : schedule.channel === 'INSTAGRAM'
+ ? '📷'
+ : schedule.channel === 'TWITTER'
+ ? '🐦'
+ : schedule.channel === 'YOUTUBE'
+ ? '📺'
+ : schedule.channel === 'LINKEDIN'
+ ? '💼'
+ : schedule.channel === 'TIKTOK'
+ ? '🎵'
+ : '📝'}
+
+
+ {schedule.content?.title?.slice(0, 15) || 'Untitled'}
+
+
+
+
+ {format(new Date(schedule.runAt), 'HH:mm')}
+
+
+ {schedule.campaign.name}
+
+
+ ))}
+
+ {daySchedules.length > 3 && (
+
+ +{daySchedules.length - 3} more
+
+ )}
+
+
+ {/* Drop hint for empty days */}
+ {daySchedules.length === 0 && isInCurrentMonth && (
+
+ Drop content here
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/features/calendar/ui/grid/WeekGrid.tsx b/features/calendar/ui/grid/WeekGrid.tsx
new file mode 100644
index 00000000..f3a5f63f
--- /dev/null
+++ b/features/calendar/ui/grid/WeekGrid.tsx
@@ -0,0 +1,235 @@
+'use client';
+
+import { useDrop } from 'react-dnd';
+import {
+ format,
+ startOfWeek,
+ endOfWeek,
+ eachDayOfInterval,
+ isToday,
+ isSameDay,
+ addDays,
+} from 'date-fns';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Clock, Calendar } from 'lucide-react';
+import { Schedule, Channel } from '../../types';
+
+interface DropZoneProps {
+ day: Date;
+ hour: number;
+ minute: number;
+ onDrop: (day: Date, hour: number, minute: number) => (item: any) => void;
+}
+
+function DropZone({ day, hour, minute, onDrop }: DropZoneProps) {
+ const [{ isOver, canDrop }, dropRef] = useDrop({
+ accept: 'DRAFT_CONTENT',
+ drop: onDrop(day, hour, minute),
+ collect: (monitor) => ({
+ isOver: monitor.isOver(),
+ canDrop: monitor.canDrop(),
+ }),
+ });
+
+ return (
+
+ );
+}
+
+interface WeekGridProps {
+ date: Date;
+ schedules: Schedule[];
+ onDrop: (contentId: string, channel: Channel, targetDate: Date, targetTime: string) => void;
+ isLoading: boolean;
+}
+
+const HOURS = Array.from({ length: 24 }, (_, i) => i);
+
+export function WeekGrid({ date, schedules, onDrop, isLoading }: WeekGridProps) {
+ const weekStart = startOfWeek(date, { weekStartsOn: 1 });
+ const weekDays = eachDayOfInterval({
+ start: weekStart,
+ end: endOfWeek(date, { weekStartsOn: 1 }),
+ });
+
+ const handleDrop = (day: Date, hour: number, minute: number) => {
+ return (item: any) => {
+ const targetDate = new Date(day);
+ targetDate.setHours(hour, minute, 0, 0);
+ const targetTime = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
+
+ onDrop(item.contentId, item.channel, targetDate, targetTime);
+ };
+ };
+
+ const getSchedulesForDayAndHour = (day: Date, hour: number) => {
+ return schedules.filter((schedule) => {
+ const scheduleDate = new Date(schedule.runAt);
+ return isSameDay(scheduleDate, day) && scheduleDate.getHours() === hour;
+ });
+ };
+
+ const getTimeLabel = (hour: number) => {
+ if (hour === 0) return '12 AM';
+ if (hour === 12) return '12 PM';
+ if (hour < 12) return `${hour} AM`;
+ return `${hour - 12} PM`;
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Time column */}
+
+
+
+
+ {HOURS.map((hour) => (
+
+
+ {getTimeLabel(hour)}
+
+
+ ))}
+
+
+ {/* Day columns */}
+ {weekDays.map((day) => {
+ const isCurrentDay = isToday(day);
+
+ return (
+
+ {/* Day header */}
+
+
{format(day, 'EEE')}
+
+ {format(day, 'd')}
+
+ {isCurrentDay && (
+
+ Today
+
+ )}
+
+
+ {/* Hour rows */}
+ {HOURS.map((hour) => {
+ const hourSchedules = getSchedulesForDayAndHour(day, hour);
+ const isCurrentHour = isCurrentDay && new Date().getHours() === hour;
+
+ return (
+
+ {/* 15-minute slots */}
+ {[0, 15, 30, 45].map((minute) => {
+ const slotTime = new Date(day);
+ slotTime.setHours(hour, minute, 0, 0);
+ const isPast = slotTime < new Date();
+ const isCurrentSlot =
+ isCurrentDay &&
+ new Date().getHours() === hour &&
+ Math.floor(new Date().getMinutes() / 15) * 15 === minute;
+
+ return (
+
+ {/* Drop Zone */}
+
+
+ {/* Schedules in this slot */}
+ {hourSchedules
+ .filter((schedule) => {
+ const scheduleMinute = new Date(
+ schedule.runAt
+ ).getMinutes();
+ return (
+ Math.floor(scheduleMinute / 15) * 15 === minute
+ );
+ })
+ .map((schedule) => (
+
+
+
+
+ {schedule.channel === 'FACEBOOK'
+ ? '📘'
+ : schedule.channel === 'INSTAGRAM'
+ ? '📷'
+ : schedule.channel === 'TWITTER'
+ ? '🐦'
+ : schedule.channel === 'YOUTUBE'
+ ? '📺'
+ : schedule.channel ===
+ 'LINKEDIN'
+ ? '💼'
+ : schedule.channel ===
+ 'TIKTOK'
+ ? '🎵'
+ : '📝'}
+
+
+ {schedule.content?.title?.slice(
+ 0,
+ 10
+ ) || 'Untitled'}
+
+
+
+
+ ))}
+
+ );
+ })}
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/hooks/use-analytics.ts b/hooks/use-analytics.ts
new file mode 100644
index 00000000..f85c00d6
--- /dev/null
+++ b/hooks/use-analytics.ts
@@ -0,0 +1,91 @@
+import { useState, useEffect } from 'react';
+
+interface AnalyticsMetrics {
+ totalEvents: number;
+ eventsByType: Record;
+ impressions: number;
+ clicks: number;
+ views: number;
+ ctr: number;
+ roi: number;
+ campaignMetrics: Array<{
+ id: string;
+ name: string;
+ totalEvents: number;
+ contentCount: number;
+ contentMetrics: Array<{
+ id: string;
+ title: string;
+ events: number;
+ impressions: number;
+ clicks: number;
+ views: number;
+ }>;
+ }>;
+}
+
+export function useAnalytics(orgId: string) {
+ const [metrics, setMetrics] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchAnalytics = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`/api/${orgId}/analytics/metrics`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch analytics');
+ }
+ const data = await response.json();
+ setMetrics(data);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (orgId) {
+ fetchAnalytics();
+ }
+ }, [orgId]);
+
+ const trackEvent = async (
+ event: string,
+ campaignId?: string,
+ contentId?: string,
+ data?: any
+ ) => {
+ try {
+ const response = await fetch(`/api/${orgId}/analytics/track`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ event,
+ campaignId,
+ contentId,
+ data,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to track event');
+ }
+
+ return await response.json();
+ } catch (err) {
+ console.error('Error tracking event:', err);
+ throw err;
+ }
+ };
+
+ return {
+ metrics,
+ loading,
+ error,
+ trackEvent,
+ };
+}
diff --git a/i18n.ts b/i18n.ts
new file mode 100644
index 00000000..7f88d81f
--- /dev/null
+++ b/i18n.ts
@@ -0,0 +1,15 @@
+import { getRequestConfig } from 'next-intl/server';
+
+// Can be imported from a shared config
+export const locales = ['en', 'vi'] as const;
+export type Locale = (typeof locales)[number];
+
+export default getRequestConfig(async ({ locale }) => {
+ // Validate that the incoming `locale` parameter is valid
+ const validLocale = locales.includes(locale as any) ? locale : 'en';
+
+ return {
+ locale: validLocale as string,
+ messages: (await import(`./messages/${validLocale}.json`)).default,
+ };
+});
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 00000000..0e3b2951
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,23 @@
+const nextJest = require('next/jest');
+
+const createJestConfig = nextJest({
+ // Provide the path to your Next.js app to load next.config.js and .env files
+ dir: './',
+});
+
+// Add any custom config to be passed to Jest
+const customJestConfig = {
+ setupFilesAfterEnv: ['/jest.setup.js'],
+ moduleNameMapper: {
+ // Handle module aliases (this will be automatically configured for you based on your tsconfig.json paths)
+ '^@/(.*)$': '/$1',
+ // Mock next-intl to avoid ES module issues
+ '^next-intl$': '/__mocks__/next-intl.ts',
+ },
+ testEnvironment: 'jest-environment-jsdom',
+ testPathIgnorePatterns: ['/tests/e2e/'],
+ transformIgnorePatterns: ['node_modules/(?!(next-intl)/)'],
+};
+
+// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
+module.exports = createJestConfig(customJestConfig);
diff --git a/jest.setup.js b/jest.setup.js
new file mode 100644
index 00000000..7b0828bf
--- /dev/null
+++ b/jest.setup.js
@@ -0,0 +1 @@
+import '@testing-library/jest-dom';
diff --git a/lib/__tests__/schemas.test.ts b/lib/__tests__/schemas.test.ts
new file mode 100644
index 00000000..28b3d735
--- /dev/null
+++ b/lib/__tests__/schemas.test.ts
@@ -0,0 +1,159 @@
+import {
+ createCampaignSchema,
+ updateCampaignSchema,
+ createContentSchema,
+ updateContentSchema,
+ generateContentSchema,
+ createAssetSchema,
+ updateAssetSchema,
+ createScheduleSchema,
+ updateScheduleSchema,
+ createAnalyticsEventSchema,
+ orgRoleSchema,
+} from '../schemas';
+
+describe('Campaign Schemas', () => {
+ describe('createCampaignSchema', () => {
+ it('should validate valid campaign data', () => {
+ const data = { name: 'Test Campaign', description: 'Test description' };
+ expect(() => createCampaignSchema.parse(data)).not.toThrow();
+ });
+
+ it('should reject empty name', () => {
+ const data = { name: '', description: 'Test description' };
+ expect(() => createCampaignSchema.parse(data)).toThrow();
+ });
+
+ it('should accept campaign without description', () => {
+ const data = { name: 'Test Campaign' };
+ expect(() => createCampaignSchema.parse(data)).not.toThrow();
+ });
+ });
+
+ describe('updateCampaignSchema', () => {
+ it('should validate partial updates', () => {
+ const data = { name: 'Updated Campaign' };
+ expect(() => updateCampaignSchema.parse(data)).not.toThrow();
+ });
+
+ it('should accept empty object', () => {
+ const data = {};
+ expect(() => updateCampaignSchema.parse(data)).not.toThrow();
+ });
+ });
+});
+
+describe('Content Schemas', () => {
+ describe('createContentSchema', () => {
+ it('should validate valid content data', () => {
+ const data = {
+ title: 'Test Content',
+ body: 'Test body',
+ campaignId: 'campaign-123',
+ };
+ expect(() => createContentSchema.parse(data)).not.toThrow();
+ });
+
+ it('should reject missing campaignId', () => {
+ const data = { title: 'Test Content', body: 'Test body' };
+ expect(() => createContentSchema.parse(data)).toThrow();
+ });
+ });
+
+ describe('generateContentSchema', () => {
+ it('should validate valid generation data', () => {
+ const data = {
+ prompt: 'Generate content about AI',
+ campaignId: 'campaign-123',
+ };
+ expect(() => generateContentSchema.parse(data)).not.toThrow();
+ });
+
+ it('should reject empty prompt', () => {
+ const data = { prompt: '', campaignId: 'campaign-123' };
+ expect(() => generateContentSchema.parse(data)).toThrow();
+ });
+ });
+});
+
+describe('Asset Schemas', () => {
+ describe('createAssetSchema', () => {
+ it('should validate valid asset data', () => {
+ const data = {
+ url: 'https://example.com/image.jpg',
+ name: 'Test Image',
+ type: 'image/jpeg',
+ size: 1024,
+ contentId: 'content-123',
+ };
+ expect(() => createAssetSchema.parse(data)).not.toThrow();
+ });
+
+ it('should reject invalid URL', () => {
+ const data = {
+ url: 'invalid-url',
+ type: 'image/jpeg',
+ contentId: 'content-123',
+ };
+ expect(() => createAssetSchema.parse(data)).toThrow();
+ });
+ });
+});
+
+describe('Schedule Schemas', () => {
+ describe('createScheduleSchema', () => {
+ it('should validate valid schedule data', () => {
+ const data = {
+ runAt: '2024-01-01T10:00:00Z',
+ timezone: 'America/New_York',
+ channel: 'FACEBOOK',
+ campaignId: 'campaign-123',
+ contentId: 'content-123',
+ };
+ expect(() => createScheduleSchema.parse(data)).not.toThrow();
+ });
+
+ it('should reject invalid date', () => {
+ const data = {
+ runAt: 'invalid-date',
+ timezone: 'America/New_York',
+ channel: 'FACEBOOK',
+ campaignId: 'campaign-123',
+ contentId: 'content-123',
+ };
+ expect(() => createScheduleSchema.parse(data)).toThrow();
+ });
+ });
+});
+
+describe('Analytics Schemas', () => {
+ describe('createAnalyticsEventSchema', () => {
+ it('should validate valid event data', () => {
+ const data = {
+ event: 'page_view',
+ data: { page: '/home' },
+ organizationId: 'org-123',
+ };
+ expect(() => createAnalyticsEventSchema.parse(data)).not.toThrow();
+ });
+
+ it('should reject missing event', () => {
+ const data = { data: { page: '/home' } };
+ expect(() => createAnalyticsEventSchema.parse(data)).toThrow();
+ });
+ });
+});
+
+describe('Role Schema', () => {
+ describe('orgRoleSchema', () => {
+ it('should validate valid roles', () => {
+ expect(() => orgRoleSchema.parse('ADMIN')).not.toThrow();
+ expect(() => orgRoleSchema.parse('BRAND_OWNER')).not.toThrow();
+ expect(() => orgRoleSchema.parse('CREATOR')).not.toThrow();
+ });
+
+ it('should reject invalid role', () => {
+ expect(() => orgRoleSchema.parse('INVALID_ROLE')).toThrow();
+ });
+ });
+});
diff --git a/lib/__tests__/utils.test.ts b/lib/__tests__/utils.test.ts
new file mode 100644
index 00000000..87f38704
--- /dev/null
+++ b/lib/__tests__/utils.test.ts
@@ -0,0 +1,25 @@
+import { cn } from '../utils';
+
+describe('cn', () => {
+ it('should merge class names correctly', () => {
+ expect(cn('class1', 'class2')).toBe('class1 class2');
+ });
+
+ it('should handle conditional classes', () => {
+ expect(cn('class1', true && 'class2', false && 'class3')).toBe('class1 class2');
+ });
+
+ it('should merge Tailwind classes correctly', () => {
+ const result = cn('px-2 py-1', 'px-4');
+ expect(result).toContain('px-4');
+ expect(result).toContain('py-1');
+ });
+
+ it('should handle empty inputs', () => {
+ expect(cn()).toBe('');
+ });
+
+ it('should handle undefined and null', () => {
+ expect(cn('class1', undefined, null, 'class2')).toBe('class1 class2');
+ });
+});
diff --git a/lib/ai-test.ts b/lib/ai-test.ts
new file mode 100644
index 00000000..0c4373ed
--- /dev/null
+++ b/lib/ai-test.ts
@@ -0,0 +1,94 @@
+/**
+ * AI Integration Test File
+ *
+ * This file contains tests to verify AI service integration.
+ * Note: These tests require a valid OpenAI API key to run successfully.
+ */
+
+import { generateContent, summarizeContent, translateContent, generateIdeas } from './openai';
+
+// Test functions for AI services
+export async function testAIGeneration() {
+ try {
+ console.log('Testing AI Content Generation...');
+ const result = await generateContent({
+ prompt: 'Write a short social media post about healthy eating',
+ type: 'social',
+ tone: 'casual',
+ length: 'short',
+ });
+ console.log('Generation Result:', result);
+ return result;
+ } catch (error) {
+ console.error('Generation test failed:', error);
+ return null;
+ }
+}
+
+export async function testAISummarization() {
+ try {
+ console.log('Testing AI Content Summarization...');
+ const result = await summarizeContent({
+ content:
+ 'This is a long article about artificial intelligence and its applications in modern business. AI has revolutionized many industries including healthcare, finance, and marketing. Machine learning algorithms can now predict customer behavior, automate customer service, and create personalized content. The future of AI looks promising with advancements in natural language processing and computer vision.',
+ length: 'brief',
+ });
+ console.log('Summarization Result:', result);
+ return result;
+ } catch (error) {
+ console.error('Summarization test failed:', error);
+ return null;
+ }
+}
+
+export async function testAITranslation() {
+ try {
+ console.log('Testing AI Content Translation...');
+ const result = await translateContent({
+ content: 'Hello, how are you today?',
+ targetLanguage: 'Spanish',
+ sourceLanguage: 'English',
+ });
+ console.log('Translation Result:', result);
+ return result;
+ } catch (error) {
+ console.error('Translation test failed:', error);
+ return null;
+ }
+}
+
+export async function testAIdeation() {
+ try {
+ console.log('Testing AI Idea Generation...');
+ const result = await generateIdeas({
+ topic: 'social media marketing',
+ count: 3,
+ type: 'general',
+ });
+ console.log('Ideation Result:', result);
+ return result;
+ } catch (error) {
+ console.error('Ideation test failed:', error);
+ return null;
+ }
+}
+
+// Main test runner
+export async function runAITests() {
+ console.log('Starting AI Integration Tests...\n');
+
+ const results = {
+ generation: await testAIGeneration(),
+ summarization: await testAISummarization(),
+ translation: await testAITranslation(),
+ ideation: await testAIdeation(),
+ };
+
+ console.log('\nAI Integration Test Results:');
+ console.log('Generation:', results.generation ? 'PASS' : 'FAIL');
+ console.log('Summarization:', results.summarization ? 'PASS' : 'FAIL');
+ console.log('Translation:', results.translation ? 'PASS' : 'FAIL');
+ console.log('Ideation:', results.ideation ? 'PASS' : 'FAIL');
+
+ return results;
+}
diff --git a/lib/auth.ts b/lib/auth.ts
new file mode 100644
index 00000000..f5b8b7ab
--- /dev/null
+++ b/lib/auth.ts
@@ -0,0 +1,59 @@
+import NextAuth from 'next-auth';
+import Credentials from 'next-auth/providers/credentials';
+import { z } from 'zod';
+import prisma from '@/lib/prisma';
+
+const credentialsSchema = z.object({
+ email: z.string().email(),
+ password: z.string().min(1),
+});
+
+type TokenWithId = Record & { id?: string };
+
+export const {
+ handlers: authHandlers,
+ auth,
+ signIn,
+ signOut,
+} = NextAuth({
+ providers: [
+ Credentials({
+ credentials: {
+ email: { label: 'Email', type: 'email' },
+ password: { label: 'Password', type: 'password' },
+ },
+ authorize: async (raw) => {
+ const parsed = credentialsSchema.safeParse(raw);
+ if (!parsed.success) return null;
+ const { email, password } = parsed.data;
+
+ const devPass = process.env.DEV_LOGIN_PASSWORD || 'dev';
+ if (password !== devPass) return null;
+
+ // Find or create a user for dev-only credentials login
+ let user = await prisma.user.findUnique({ where: { email } });
+ if (!user) {
+ user = await prisma.user.create({ data: { email, name: email.split('@')[0] } });
+ }
+ return { id: user.id, email: user.email, name: user.name };
+ },
+ }),
+ ],
+ session: { strategy: 'jwt' },
+ callbacks: {
+ async jwt({ token, user }) {
+ const t = token as TokenWithId;
+ if (user && 'id' in user && typeof (user as { id?: unknown }).id === 'string') {
+ t.id = (user as { id: string }).id;
+ }
+ return t;
+ },
+ async session({ session, token }) {
+ const t = token as TokenWithId;
+ if (session?.user && t.id) {
+ (session.user as { id?: string }).id = t.id;
+ }
+ return session;
+ },
+ },
+});
diff --git a/lib/campaigns.ts b/lib/campaigns.ts
new file mode 100644
index 00000000..43881c48
--- /dev/null
+++ b/lib/campaigns.ts
@@ -0,0 +1,102 @@
+import prisma from './prisma';
+
+export async function getCampaign(orgId: string, campaignId: string) {
+ try {
+ const campaign = await prisma.campaign.findFirst({
+ where: {
+ id: campaignId,
+ organizationId: orgId,
+ },
+ include: {
+ lead: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ members: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ tasks: {
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ subtasks: {
+ include: {
+ assignee: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true,
+ },
+ },
+ },
+ },
+ },
+ orderBy: [{ priority: 'desc' }, { createdAt: 'asc' }],
+ },
+ labels: true,
+ milestones: {
+ orderBy: { dueDate: 'asc' },
+ },
+ contents: {
+ include: {
+ assets: true,
+ },
+ },
+ schedules: true,
+ _count: {
+ select: {
+ tasks: true,
+ members: true,
+ labels: true,
+ milestones: true,
+ contents: true,
+ schedules: true,
+ },
+ },
+ },
+ });
+
+ return campaign;
+ } catch (error) {
+ console.error('Error fetching campaign:', error);
+ return null;
+ }
+}
+
+export async function getCampaigns(orgId: string) {
+ try {
+ const campaigns = await prisma.campaign.findMany({
+ where: { organizationId: orgId },
+ include: {
+ contents: true,
+ schedules: true,
+ },
+ orderBy: { createdAt: 'desc' },
+ });
+
+ return campaigns;
+ } catch (error) {
+ console.error('Error fetching campaigns:', error);
+ return [];
+ }
+}
diff --git a/lib/content.ts b/lib/content.ts
new file mode 100644
index 00000000..d31a076b
--- /dev/null
+++ b/lib/content.ts
@@ -0,0 +1,57 @@
+import prisma from './prisma';
+
+export async function getContent(orgId: string, contentId: string) {
+ try {
+ const content = await prisma.content.findFirst({
+ where: {
+ id: contentId,
+ campaign: { organizationId: orgId },
+ },
+ include: {
+ campaign: true,
+ assets: true,
+ },
+ });
+
+ return content;
+ } catch (error) {
+ console.error('Error fetching content:', error);
+ return null;
+ }
+}
+
+export async function getContents(
+ orgId: string,
+ filters?: {
+ campaignId?: string;
+ status?: string;
+ }
+) {
+ try {
+ const where: any = {
+ campaign: { organizationId: orgId },
+ };
+
+ if (filters?.campaignId) {
+ where.campaignId = filters.campaignId;
+ }
+
+ if (filters?.status) {
+ where.status = filters.status;
+ }
+
+ const contents = await prisma.content.findMany({
+ where,
+ include: {
+ campaign: true,
+ assets: true,
+ },
+ orderBy: { createdAt: 'desc' },
+ });
+
+ return contents;
+ } catch (error) {
+ console.error('Error fetching contents:', error);
+ return [];
+ }
+}
diff --git a/lib/cron-worker.ts b/lib/cron-worker.ts
new file mode 100644
index 00000000..375f6fae
--- /dev/null
+++ b/lib/cron-worker.ts
@@ -0,0 +1,76 @@
+import prisma from './prisma';
+
+export async function publishScheduledContent() {
+ try {
+ const now = new Date();
+
+ // Find all schedules that are due for publication
+ const dueSchedules = await prisma.schedule.findMany({
+ where: {
+ runAt: {
+ lte: now,
+ },
+ status: {
+ not: 'PUBLISHED',
+ },
+ },
+ include: {
+ content: true,
+ campaign: true,
+ },
+ });
+
+ console.log(`Found ${dueSchedules.length} schedules due for publication`);
+
+ for (const schedule of dueSchedules) {
+ try {
+ // Update schedule status to published
+ await prisma.schedule.update({
+ where: { id: schedule.id },
+ data: { status: 'PUBLISHED' },
+ });
+
+ // Here you would integrate with your content publishing platform
+ // For example, posting to social media, sending emails, etc.
+ console.log(
+ `Published content: ${schedule.content?.title} for campaign: ${schedule.campaign.name}`
+ );
+
+ // You could also update content status or add publishing metadata
+ if (schedule.content) {
+ // Add any publishing logic here
+ // For example, update content with publication timestamp
+ await prisma.content.update({
+ where: { id: schedule.content.id },
+ data: {
+ // Add publication metadata if needed
+ },
+ });
+ }
+ } catch (error) {
+ console.error(`Error publishing schedule ${schedule.id}:`, error);
+ // You might want to mark the schedule as failed
+ await prisma.schedule.update({
+ where: { id: schedule.id },
+ data: { status: 'FAILED' },
+ });
+ }
+ }
+
+ return { success: true, published: dueSchedules.length };
+ } catch (error) {
+ console.error('Error in publishScheduledContent:', error);
+ return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
+ }
+}
+
+// Function to run the cron job
+export async function runCronJob() {
+ console.log('Running scheduled content publication cron job...');
+ const result = await publishScheduledContent();
+ console.log('Cron job completed:', result);
+ return result;
+}
+
+// Export for use in API routes or scheduled tasks
+export default { publishScheduledContent, runCronJob };
diff --git a/lib/i18n.ts b/lib/i18n.ts
new file mode 100644
index 00000000..35bde4e0
--- /dev/null
+++ b/lib/i18n.ts
@@ -0,0 +1,16 @@
+import { notFound } from 'next/navigation';
+import { getRequestConfig } from 'next-intl/server';
+
+// Can be imported from a shared config
+export const locales = ['en', 'vi'] as const;
+export type Locale = (typeof locales)[number];
+
+export default getRequestConfig(async ({ locale }) => {
+ // Validate that the incoming `locale` parameter is valid
+ if (!locales.includes(locale as any)) notFound();
+
+ return {
+ locale: locale as string,
+ messages: (await import(`../messages/${locale}.json`)).default,
+ };
+});
diff --git a/lib/openai.ts b/lib/openai.ts
new file mode 100644
index 00000000..d22e1f35
--- /dev/null
+++ b/lib/openai.ts
@@ -0,0 +1,148 @@
+import OpenAI from 'openai';
+
+if (!process.env.OPENAI_API_KEY) {
+ throw new Error('OPENAI_API_KEY environment variable is required');
+}
+
+export const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+});
+
+export interface AIContentGenerationOptions {
+ prompt: string;
+ type?: 'blog' | 'social' | 'email' | 'general';
+ tone?: 'professional' | 'casual' | 'creative' | 'formal';
+ length?: 'short' | 'medium' | 'long';
+}
+
+export interface AISummarizationOptions {
+ content: string;
+ length?: 'brief' | 'detailed';
+}
+
+export interface AITranslationOptions {
+ content: string;
+ targetLanguage: string;
+ sourceLanguage?: string;
+}
+
+export interface AIIdeationOptions {
+ topic: string;
+ count?: number;
+ type?: 'titles' | 'hashtags' | 'campaigns' | 'general';
+}
+
+/**
+ * Generate content using OpenAI
+ */
+export async function generateContent(options: AIContentGenerationOptions): Promise {
+ const { prompt, type = 'general', tone = 'professional', length = 'medium' } = options;
+
+ const systemPrompt = `You are a content generation assistant. Generate ${type} content in a ${tone} tone. The content should be ${length} in length.`;
+
+ const userPrompt = `Generate content based on this prompt: ${prompt}`;
+
+ try {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o-mini',
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt },
+ ],
+ max_tokens: length === 'short' ? 150 : length === 'medium' ? 300 : 600,
+ temperature: 0.7,
+ });
+
+ return response.choices[0]?.message?.content?.trim() || 'Failed to generate content';
+ } catch (error) {
+ console.error('Error generating content:', error);
+ throw new Error('Failed to generate content');
+ }
+}
+
+/**
+ * Summarize content using OpenAI
+ */
+export async function summarizeContent(options: AISummarizationOptions): Promise {
+ const { content, length = 'brief' } = options;
+
+ const systemPrompt = `You are a content summarization assistant. Provide a ${length} summary of the given content.`;
+
+ try {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o-mini',
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: `Please summarize this content:\n\n${content}` },
+ ],
+ max_tokens: length === 'brief' ? 100 : 200,
+ temperature: 0.3,
+ });
+
+ return response.choices[0]?.message?.content?.trim() || 'Failed to summarize content';
+ } catch (error) {
+ console.error('Error summarizing content:', error);
+ throw new Error('Failed to summarize content');
+ }
+}
+
+/**
+ * Translate content using OpenAI
+ */
+export async function translateContent(options: AITranslationOptions): Promise {
+ const { content, targetLanguage, sourceLanguage } = options;
+
+ const systemPrompt = `You are a translation assistant. Translate the given content to ${targetLanguage}. Maintain the original tone and formatting.`;
+
+ const userPrompt = sourceLanguage
+ ? `Translate this content from ${sourceLanguage} to ${targetLanguage}:\n\n${content}`
+ : `Translate this content to ${targetLanguage}:\n\n${content}`;
+
+ try {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o-mini',
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt },
+ ],
+ temperature: 0.3,
+ });
+
+ return response.choices[0]?.message?.content?.trim() || 'Failed to translate content';
+ } catch (error) {
+ console.error('Error translating content:', error);
+ throw new Error('Failed to translate content');
+ }
+}
+
+/**
+ * Generate ideas using OpenAI
+ */
+export async function generateIdeas(options: AIIdeationOptions): Promise {
+ const { topic, count = 5, type = 'general' } = options;
+
+ const systemPrompt = `You are an ideation assistant. Generate ${count} creative ${type} ideas related to the given topic. Provide each idea as a separate item in a list.`;
+
+ const userPrompt = `Generate ${count} ${type} ideas for: ${topic}`;
+
+ try {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o-mini',
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt },
+ ],
+ max_tokens: 400,
+ temperature: 0.8,
+ });
+
+ const content = response.choices[0]?.message?.content?.trim() || '';
+ // Split by newlines and filter out empty lines
+ const ideas = content.split('\n').filter((line) => line.trim().length > 0);
+
+ return ideas.length > 0 ? ideas : ['Failed to generate ideas'];
+ } catch (error) {
+ console.error('Error generating ideas:', error);
+ throw new Error('Failed to generate ideas');
+ }
+}
diff --git a/lib/prisma.ts b/lib/prisma.ts
new file mode 100644
index 00000000..077affa4
--- /dev/null
+++ b/lib/prisma.ts
@@ -0,0 +1,14 @@
+import { PrismaClient } from '@prisma/client';
+
+// Ensure a singleton in dev and RSC-safe usage
+const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
+
+export const prisma =
+ globalForPrisma.prisma ??
+ new PrismaClient({
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
+ });
+
+if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
+
+export default prisma;
diff --git a/lib/rbac.ts b/lib/rbac.ts
new file mode 100644
index 00000000..381ce31f
--- /dev/null
+++ b/lib/rbac.ts
@@ -0,0 +1,117 @@
+import { auth } from '@/lib/auth';
+import prisma from '@/lib/prisma';
+import { OrgRole } from '@prisma/client';
+
+export type UserRole = OrgRole;
+
+// Simple in-memory cache for user roles (per request)
+const roleCache = new Map();
+
+export const PERMISSIONS = {
+ VIEW_CAMPAIGNS: 'view_campaigns',
+ MANAGE_CAMPAIGNS: 'manage_campaigns',
+ MANAGE_CONTENT: 'manage_content',
+ APPROVE_CONTENT: 'approve_content',
+ VIEW_ANALYTICS: 'view_analytics',
+ MANAGE_USERS: 'manage_users',
+ MANAGE_ORGANIZATION: 'manage_organization',
+ MANAGE_SCHEDULES: 'manage_schedules',
+} as const;
+
+export async function getUserRole(orgId: string): Promise {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return null;
+ }
+
+ const cacheKey = `${session.user.id}:${orgId}`;
+ if (roleCache.has(cacheKey)) {
+ return roleCache.get(cacheKey)!;
+ }
+
+ const membership = await prisma.membership.findFirst({
+ where: {
+ userId: session.user.id,
+ organizationId: orgId,
+ },
+ });
+
+ const role = membership?.role || null;
+ roleCache.set(cacheKey, role);
+ return role;
+}
+
+export async function requireRole(orgId: string, requiredRole: UserRole): Promise {
+ const userRole = await getUserRole(orgId);
+ if (!userRole) return false;
+
+ const roleHierarchy = {
+ [OrgRole.CREATOR]: 1,
+ [OrgRole.BRAND_OWNER]: 2,
+ [OrgRole.ADMIN]: 3,
+ };
+
+ return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
+}
+
+export async function requirePermission(
+ userId: string,
+ orgId: string,
+ permission: string
+): Promise {
+ const userRole = await getUserRole(orgId);
+ if (!userRole) {
+ throw new Error('Insufficient permissions');
+ }
+
+ const permissions = {
+ [OrgRole.CREATOR]: [PERMISSIONS.VIEW_CAMPAIGNS, PERMISSIONS.MANAGE_CONTENT],
+ [OrgRole.BRAND_OWNER]: [
+ PERMISSIONS.VIEW_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CONTENT,
+ PERMISSIONS.APPROVE_CONTENT,
+ PERMISSIONS.VIEW_ANALYTICS,
+ ],
+ [OrgRole.ADMIN]: [
+ PERMISSIONS.VIEW_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CONTENT,
+ PERMISSIONS.APPROVE_CONTENT,
+ PERMISSIONS.VIEW_ANALYTICS,
+ PERMISSIONS.MANAGE_USERS,
+ PERMISSIONS.MANAGE_ORGANIZATION,
+ ],
+ };
+
+ if (!permissions[userRole]?.includes(permission as any)) {
+ throw new Error('Insufficient permissions');
+ }
+}
+
+export async function hasPermission(orgId: string, permission: string): Promise {
+ const userRole = await getUserRole(orgId);
+ if (!userRole) return false;
+
+ const permissions = {
+ [OrgRole.CREATOR]: [PERMISSIONS.VIEW_CAMPAIGNS, PERMISSIONS.MANAGE_CONTENT],
+ [OrgRole.BRAND_OWNER]: [
+ PERMISSIONS.VIEW_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CONTENT,
+ PERMISSIONS.APPROVE_CONTENT,
+ PERMISSIONS.VIEW_ANALYTICS,
+ ],
+ [OrgRole.ADMIN]: [
+ PERMISSIONS.VIEW_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CAMPAIGNS,
+ PERMISSIONS.MANAGE_CONTENT,
+ PERMISSIONS.APPROVE_CONTENT,
+ PERMISSIONS.VIEW_ANALYTICS,
+ PERMISSIONS.MANAGE_USERS,
+ PERMISSIONS.MANAGE_ORGANIZATION,
+ ],
+ };
+
+ return permissions[userRole]?.includes(permission as any) || false;
+}
diff --git a/lib/schemas.ts b/lib/schemas.ts
new file mode 100644
index 00000000..1a4843ba
--- /dev/null
+++ b/lib/schemas.ts
@@ -0,0 +1,125 @@
+import { z } from 'zod';
+
+// User schemas
+export const createUserSchema = z.object({
+ name: z.string().min(1, 'Name is required'),
+ email: z.string().email('Invalid email address'),
+ password: z.string().min(6, 'Password must be at least 6 characters').optional(),
+});
+
+export const updateUserSchema = z.object({
+ name: z.string().min(1, 'Name is required').optional(),
+ email: z.string().email('Invalid email address').optional(),
+ image: z.string().url('Invalid URL').optional(),
+});
+
+export const userLoginSchema = z.object({
+ email: z.string().email('Invalid email address'),
+ password: z.string().min(1, 'Password is required'),
+});
+
+// Organization schemas
+export const createOrganizationSchema = z.object({
+ name: z.string().min(1, 'Organization name is required'),
+});
+
+export const updateOrganizationSchema = z.object({
+ name: z.string().min(1, 'Organization name is required').optional(),
+});
+
+// Membership schemas
+export const createMembershipSchema = z.object({
+ userId: z.string().min(1, 'User ID is required'),
+ organizationId: z.string().min(1, 'Organization ID is required'),
+ role: z.enum(['ADMIN', 'BRAND_OWNER', 'CREATOR']),
+});
+
+export const updateMembershipSchema = z.object({
+ role: z.enum(['ADMIN', 'BRAND_OWNER', 'CREATOR']).optional(),
+});
+
+// Campaign schemas
+export const createCampaignSchema = z.object({
+ name: z.string().min(1, 'Name is required'),
+ description: z.string().optional(),
+});
+
+export const updateCampaignSchema = z.object({
+ name: z.string().min(1, 'Name is required').optional(),
+ description: z.string().optional(),
+});
+
+// Content schemas
+export const createContentSchema = z.object({
+ title: z.string().min(1, 'Title is required'),
+ body: z.string().optional(),
+ status: z
+ .enum(['DRAFT', 'SUBMITTED', 'APPROVED', 'SCHEDULED', 'PUBLISHED', 'REJECTED'])
+ .optional(),
+ campaignId: z.string().min(1, 'Campaign ID is required'),
+});
+
+export const updateContentSchema = z.object({
+ title: z.string().min(1, 'Title is required').optional(),
+ body: z.string().optional(),
+ status: z
+ .enum(['DRAFT', 'SUBMITTED', 'APPROVED', 'SCHEDULED', 'PUBLISHED', 'REJECTED'])
+ .optional(),
+});
+
+export const generateContentSchema = z.object({
+ prompt: z.string().min(1, 'Prompt is required'),
+ campaignId: z.string().min(1, 'Campaign ID is required'),
+});
+
+// Asset schemas
+export const createAssetSchema = z.object({
+ url: z.string().url('Invalid URL'),
+ name: z.string().optional(),
+ type: z.string().min(1, 'Type is required'),
+ size: z.number().optional(),
+ description: z.string().optional(),
+ tags: z.array(z.string()).optional(),
+ contentId: z.string().min(1, 'Content ID is required'),
+});
+
+export const updateAssetSchema = z.object({
+ url: z.string().url('Invalid URL').optional(),
+ name: z.string().optional(),
+ type: z.string().min(1, 'Type is required').optional(),
+ size: z.number().optional(),
+ description: z.string().optional(),
+ tags: z.array(z.string()).optional(),
+});
+
+// Schedule schemas
+export const createScheduleSchema = z.object({
+ runAt: z.string().datetime('Invalid date'),
+ timezone: z.string().min(1, 'Timezone is required'),
+ channel: z.enum(['FACEBOOK', 'INSTAGRAM', 'TWITTER', 'YOUTUBE', 'LINKEDIN', 'TIKTOK', 'BLOG']),
+ status: z.enum(['PENDING', 'PUBLISHED', 'FAILED', 'CANCELLED']).optional(),
+ campaignId: z.string().min(1, 'Campaign ID is required'),
+ contentId: z.string().min(1, 'Content ID is required'),
+});
+
+export const updateScheduleSchema = z.object({
+ runAt: z.string().datetime('Invalid date').optional(),
+ timezone: z.string().min(1, 'Timezone is required').optional(),
+ channel: z
+ .enum(['FACEBOOK', 'INSTAGRAM', 'TWITTER', 'YOUTUBE', 'LINKEDIN', 'TIKTOK', 'BLOG'])
+ .optional(),
+ status: z.enum(['PENDING', 'PUBLISHED', 'FAILED', 'CANCELLED']).optional(),
+ contentId: z.string().optional(),
+});
+
+// Analytics schemas
+export const createAnalyticsEventSchema = z.object({
+ event: z.string().min(1, 'Event is required'),
+ data: z.record(z.any()).optional(),
+ organizationId: z.string().optional(),
+ campaignId: z.string().optional(),
+ contentId: z.string().optional(),
+});
+
+// OrgRole enum
+export const orgRoleSchema = z.enum(['ADMIN', 'BRAND_OWNER', 'CREATOR']);
diff --git a/lib/uploadthing.ts b/lib/uploadthing.ts
new file mode 100644
index 00000000..6ff453a6
--- /dev/null
+++ b/lib/uploadthing.ts
@@ -0,0 +1,29 @@
+import { createUploadthing, type FileRouter } from 'uploadthing/next';
+import { UploadThingError } from 'uploadthing/server';
+import { auth } from '@/lib/auth';
+
+const f = createUploadthing();
+
+export const ourFileRouter = {
+ assetUploader: f({
+ image: { maxFileSize: '4MB' },
+ video: { maxFileSize: '16MB' },
+ audio: { maxFileSize: '8MB' },
+ pdf: { maxFileSize: '4MB' },
+ })
+ .middleware(async ({ req }) => {
+ const session = await auth();
+
+ if (!session?.user?.id) throw new UploadThingError('Unauthorized');
+
+ return { userId: session.user.id };
+ })
+ .onUploadComplete(async ({ metadata, file }) => {
+ console.log('Upload complete for userId:', metadata.userId);
+ console.log('file url', file.url);
+
+ return { uploadedBy: metadata.userId, url: file.url };
+ }),
+} satisfies FileRouter;
+
+export type OurFileRouter = typeof ourFileRouter;
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 00000000..b09bbf94
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,80 @@
+{
+ "common": {
+ "loading": "Loading...",
+ "save": "Save",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "edit": "Edit",
+ "create": "Create",
+ "search": "Search",
+ "filter": "Filter",
+ "settings": "Settings",
+ "language": "Language",
+ "english": "English",
+ "vietnamese": "Vietnamese"
+ },
+ "navigation": {
+ "workspace": "Workspace",
+ "dashboard": "Dashboard",
+ "campaigns": "Campaigns",
+ "content_studio": "Content Studio",
+ "schedules": "Schedules",
+ "assets": "Assets",
+ "analytics": "Analytics",
+ "members": "Members",
+ "settings": "Settings",
+ "more": "More",
+ "views": "Views",
+ "customize_sidebar": "Customize sidebar",
+ "projects": "Projects",
+ "teams": "Teams",
+ "content": "Content",
+ "inbox": "Inbox"
+ },
+ "auth": {
+ "signin": "Sign In",
+ "signout": "Sign Out",
+ "welcome": "Welcome",
+ "username": "Username",
+ "password": "Password",
+ "email": "Email"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "overview": "Overview",
+ "analytics": "Analytics",
+ "recent_activity": "Recent Activity",
+ "admin_title": "Admin Dashboard",
+ "admin_description": "Manage organization, users, and system settings",
+ "system_settings": "System Settings",
+ "total_users": "Total Users",
+ "active_users": "active",
+ "active_campaigns": "Active Campaigns",
+ "content_pieces": "Content Pieces",
+ "pending_approvals": "Pending Approvals",
+ "requires_attention": "Requires attention",
+ "user_management": "User Management",
+ "organization": "Organization",
+ "system": "System",
+ "team_members": "Team Members",
+ "manage_user_roles": "Manage user roles and permissions",
+ "user": "User",
+ "role": "Role",
+ "status": "Status",
+ "actions": "Actions",
+ "edit": "Edit",
+ "brand_owner": "Brand Owner",
+ "creator": "Creator",
+ "admin": "Admin",
+ "pending_invites": "Pending Invites",
+ "users_waiting_join": "Users waiting to join the organization",
+ "invited_by": "Invited by",
+ "resend": "Resend",
+ "invite_new_user": "Invite New User"
+ },
+ "sidebar": {
+ "open_source_layouts": "Open-source layouts by lndev-ui",
+ "layouts_description": "Collection of beautifully crafted open-source layouts UI built with shadcn/ui.",
+ "visit_square": "square.lndev.me"
+ }
+}
diff --git a/messages/vi.json b/messages/vi.json
new file mode 100644
index 00000000..d569062f
--- /dev/null
+++ b/messages/vi.json
@@ -0,0 +1,80 @@
+{
+ "common": {
+ "loading": "Đang tải...",
+ "save": "Lưu",
+ "cancel": "Hủy",
+ "delete": "Xóa",
+ "edit": "Chỉnh sửa",
+ "create": "Tạo",
+ "search": "Tìm kiếm",
+ "filter": "Lọc",
+ "settings": "Cài đặt",
+ "language": "Ngôn ngữ",
+ "english": "Tiếng Anh",
+ "vietnamese": "Tiếng Việt"
+ },
+ "navigation": {
+ "workspace": "Không gian làm việc",
+ "dashboard": "Bảng điều khiển",
+ "campaigns": "Chiến dịch",
+ "content_studio": "Studio nội dung",
+ "schedules": "Lịch trình",
+ "assets": "Tài sản",
+ "analytics": "Phân tích",
+ "members": "Thành viên",
+ "settings": "Cài đặt",
+ "more": "Thêm",
+ "views": "Chế độ xem",
+ "customize_sidebar": "Tùy chỉnh thanh bên",
+ "projects": "Dự án",
+ "teams": "Nhóm",
+ "content": "Nội dung",
+ "inbox": "Hộp thư"
+ },
+ "auth": {
+ "signin": "Đăng nhập",
+ "signout": "Đăng xuất",
+ "welcome": "Chào mừng",
+ "username": "Tên người dùng",
+ "password": "Mật khẩu",
+ "email": "Email"
+ },
+ "dashboard": {
+ "title": "Bảng điều khiển",
+ "overview": "Tổng quan",
+ "analytics": "Phân tích",
+ "recent_activity": "Hoạt động gần đây",
+ "admin_title": "Bảng điều khiển quản trị",
+ "admin_description": "Quản lý tổ chức, người dùng và cài đặt hệ thống",
+ "system_settings": "Cài đặt hệ thống",
+ "total_users": "Tổng số người dùng",
+ "active_users": "đang hoạt động",
+ "active_campaigns": "Chiến dịch đang hoạt động",
+ "content_pieces": "Mảnh nội dung",
+ "pending_approvals": "Đang chờ phê duyệt",
+ "requires_attention": "Cần chú ý",
+ "user_management": "Quản lý người dùng",
+ "organization": "Tổ chức",
+ "system": "Hệ thống",
+ "team_members": "Thành viên nhóm",
+ "manage_user_roles": "Quản lý vai trò và quyền của người dùng",
+ "user": "Người dùng",
+ "role": "Vai trò",
+ "status": "Trạng thái",
+ "actions": "Hành động",
+ "edit": "Chỉnh sửa",
+ "brand_owner": "Chủ sở hữu thương hiệu",
+ "creator": "Người tạo",
+ "admin": "Quản trị viên",
+ "pending_invites": "Lời mời đang chờ",
+ "users_waiting_join": "Người dùng đang chờ tham gia tổ chức",
+ "invited_by": "Được mời bởi",
+ "resend": "Gửi lại",
+ "invite_new_user": "Mời người dùng mới"
+ },
+ "sidebar": {
+ "open_source_layouts": "Bố cục mã nguồn mở bởi lndev-ui",
+ "layouts_description": "Bộ sưu tập các bố cục UI mã nguồn mở được thiết kế đẹp mắt được xây dựng với shadcn/ui.",
+ "visit_square": "square.lndev.me"
+ }
+}
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 00000000..ef512d12
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,100 @@
+import { auth } from '@/lib/auth';
+import createMiddleware from 'next-intl/middleware';
+import { locales } from './i18n';
+
+const intlMiddleware = createMiddleware({
+ locales: locales,
+ defaultLocale: 'en',
+ localePrefix: 'as-needed',
+});
+
+export default auth(async (req) => {
+ const { nextUrl } = req;
+ const pathname = nextUrl.pathname;
+
+ console.log('Middleware processing:', pathname);
+
+ // Skip middleware for static files and API routes
+ if (pathname.startsWith('/_next') || pathname.startsWith('/api') || pathname.includes('.')) {
+ console.log('Skipping middleware for static/API route');
+ return null;
+ }
+
+ // Check if user is logged in first
+ const isLoggedIn = !!req.auth;
+ console.log('Is logged in:', isLoggedIn);
+
+ // If user is logged in, skip i18n middleware to prevent locale redirects
+ if (isLoggedIn) {
+ console.log('User logged in, skipping i18n middleware');
+
+ // Only guard organization routes: /[orgId]/**
+ const isOrgRoute = (() => {
+ if (pathname === '/') return false;
+ if (pathname.startsWith('/auth')) return false;
+ if (pathname.startsWith('/api')) return false;
+ if (pathname.startsWith('/_next')) return false;
+ if (pathname.startsWith('/test')) return false;
+ if (/\.[\w]+$/.test(pathname)) return false; // assets
+ // Consider any first-segment path as org route for now
+ return /^\/[^/]+(\/.*)?$/.test(pathname);
+ })();
+
+ console.log('Is org route:', isOrgRoute);
+
+ if (isOrgRoute) {
+ console.log('User accessing org route, allowing');
+ return null;
+ }
+
+ console.log('Middleware allowing request for logged in user');
+ return null;
+ }
+
+ // Handle i18n routing ONLY for unauthenticated users and specific paths
+ if (pathname === '/' || pathname.startsWith('/auth') || pathname.startsWith('/test')) {
+ console.log('Applying i18n middleware for unauthenticated user:', pathname);
+ const intlResponse = intlMiddleware(req);
+ if (intlResponse) {
+ console.log('i18n response returned');
+ return intlResponse;
+ }
+ }
+
+ // Allow test page
+ if (pathname === '/test') {
+ console.log('Allowing test page');
+ return null;
+ }
+
+ // Only guard organization routes: /[orgId]/**
+ const isOrgRoute = (() => {
+ if (pathname === '/') return false;
+ if (pathname.startsWith('/auth')) return false;
+ if (pathname.startsWith('/api')) return false;
+ if (pathname.startsWith('/_next')) return false;
+ if (pathname.startsWith('/test')) return false;
+ if (/\.[\w]+$/.test(pathname)) return false; // assets
+ // Consider any first-segment path as org route for now
+ return /^\/[^/]+(\/.*)?$/.test(pathname);
+ })();
+
+ console.log('Is org route:', isOrgRoute);
+
+ if (!isLoggedIn && isOrgRoute) {
+ console.log('Redirecting to signin');
+ const url = new URL('/auth/signin', nextUrl);
+ url.searchParams.set('callbackUrl', nextUrl.href);
+ return Response.redirect(url);
+ }
+
+ console.log('Middleware allowing request');
+ return null;
+});
+
+export const config = {
+ matcher: [
+ // Match all paths except Next internals and static files
+ '/((?!api/health|_next|.*\\..*).*)',
+ ],
+};
diff --git a/mock-data/side-bar-nav.ts b/mock-data/side-bar-nav.ts
index fe15a3b3..9b39e321 100644
--- a/mock-data/side-bar-nav.ts
+++ b/mock-data/side-bar-nav.ts
@@ -81,6 +81,16 @@ export const accountItems = [
];
export const featuresItems = [
+ {
+ name: 'Campaigns',
+ url: '/campaigns',
+ icon: Layers,
+ },
+ {
+ name: 'Content',
+ url: '/content',
+ icon: FileText,
+ },
{
name: 'Labels',
url: '/settings/labels',
diff --git a/next.config.ts b/next.config.ts
index 5d2d55ac..12063e66 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,8 +1,17 @@
import type { NextConfig } from 'next';
+import createNextIntlPlugin from 'next-intl/plugin';
+
+const withNextIntl = createNextIntlPlugin('./i18n.ts');
const nextConfig: NextConfig = {
/* config options here */
devIndicators: false,
+ experimental: {
+ optimizeCss: true,
+ },
+ images: {
+ domains: ['your-production-domain.com'], // Add your domains
+ },
};
-export default nextConfig;
+export default withNextIntl(nextConfig);
diff --git a/package.json b/package.json
index dd86351e..4d7a2ec6 100644
--- a/package.json
+++ b/package.json
@@ -8,11 +8,21 @@
"start": "next start",
"lint": "next lint",
"format": "prettier --write .",
- "prepare": "husky install"
+ "prepare": "husky install",
+ "typecheck": "tsc --noEmit",
+ "db:generate": "prisma generate",
+ "db:push": "prisma db push",
+ "db:seed": "tsx db/seed.ts",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "test:coverage": "jest --coverage",
+ "test:e2e": "playwright test",
+ "test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@hookform/resolvers": "^4.1.2",
"@kayron013/lexorank": "^2.0.0",
+ "@prisma/client": "^6.0.0",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
@@ -31,14 +41,22 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@remixicon/react": "^4.6.0",
+ "@tiptap/pm": "^3.3.0",
+ "@tiptap/react": "^3.3.0",
+ "@tiptap/starter-kit": "^3.3.0",
+ "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
+ "formidable": "^3.5.4",
"lucide-react": "^0.476.0",
"motion": "^12.4.10",
"next": "15.2.4",
+ "next-auth": "5.0.0-beta.29",
+ "next-intl": "^4.3.5",
"next-themes": "^0.4.4",
+ "openai": "^5.16.0",
"react": "^19.0.0",
"react-day-picker": "8.10.1",
"react-dnd": "^16.0.1",
@@ -51,6 +69,7 @@
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
+ "uploadthing": "^7.7.4",
"usehooks-ts": "^3.1.1",
"uuid": "^11.1.0",
"zod": "^3.24.2",
@@ -58,7 +77,14 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+ "@playwright/test": "^1.55.0",
"@tailwindcss/postcss": "^4",
+ "@testing-library/jest-dom": "^6.8.0",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/bcryptjs": "^3.0.0",
+ "@types/formidable": "^3.4.5",
+ "@types/jest": "^30.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -67,9 +93,13 @@
"eslint-config-prettier": "^10.0.2",
"eslint-plugin-prettier": "^5.2.3",
"husky": "^9.1.7",
+ "jest": "^30.1.2",
+ "jest-environment-jsdom": "^30.1.2",
"lint-staged": "^15.4.3",
"prettier": "^3.5.2",
+ "prisma": "^6.0.0",
"tailwindcss": "^4",
+ "tsx": "^4.19.2",
"typescript": "^5"
}
}
diff --git a/playwright-report/index.html b/playwright-report/index.html
new file mode 100644
index 00000000..542fd043
--- /dev/null
+++ b/playwright-report/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..a42a587a
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,71 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ */
+export default defineConfig({
+ testDir: './tests/e2e',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: 'http://localhost:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ /* Test against mobile viewports. */
+ {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://localhost:3000',
+ reuseExistingServer: !process.env.CI,
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9531b9fa..4514ee07 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@kayron013/lexorank':
specifier: ^2.0.0
version: 2.0.0
+ '@prisma/client':
+ specifier: ^6.0.0
+ version: 6.15.0(prisma@6.15.0(typescript@5.7.3))(typescript@5.7.3)
'@radix-ui/react-alert-dialog':
specifier: ^1.1.6
version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -68,6 +71,18 @@ importers:
'@remixicon/react':
specifier: ^4.6.0
version: 4.6.0(react@19.0.0)
+ '@tiptap/pm':
+ specifier: ^3.3.0
+ version: 3.3.0
+ '@tiptap/react':
+ specifier: ^3.3.0
+ version: 3.3.0(@floating-ui/dom@1.6.13)(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@tiptap/starter-kit':
+ specifier: ^3.3.0
+ version: 3.3.0
+ bcryptjs:
+ specifier: ^3.0.2
+ version: 3.0.2
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -80,6 +95,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
+ formidable:
+ specifier: ^3.5.4
+ version: 3.5.4
lucide-react:
specifier: ^0.476.0
version: 0.476.0(react@19.0.0)
@@ -88,10 +106,19 @@ importers:
version: 12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next:
specifier: 15.2.4
- version: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ version: 15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ next-auth:
+ specifier: 5.0.0-beta.29
+ version: 5.0.0-beta.29(next@15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
+ next-intl:
+ specifier: ^4.3.5
+ version: 4.3.5(next@15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.7.3)
next-themes:
specifier: ^0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ openai:
+ specifier: ^5.16.0
+ version: 5.16.0(ws@8.18.3)(zod@3.24.2)
react:
specifier: ^19.0.0
version: 19.0.0
@@ -128,6 +155,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.0.8)
+ uploadthing:
+ specifier: ^7.7.4
+ version: 7.7.4(next@15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tailwindcss@4.0.8)
usehooks-ts:
specifier: ^3.1.1
version: 3.1.1(react@19.0.0)
@@ -139,14 +169,35 @@ importers:
version: 3.24.2
zustand:
specifier: ^5.0.3
- version: 5.0.3(@types/react@19.0.10)(react@19.0.0)
+ version: 5.0.3(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0))
devDependencies:
'@eslint/eslintrc':
specifier: ^3
version: 3.3.0
+ '@playwright/test':
+ specifier: ^1.55.0
+ version: 1.55.0
'@tailwindcss/postcss':
specifier: ^4
version: 4.0.8
+ '@testing-library/jest-dom':
+ specifier: ^6.8.0
+ version: 6.8.0
+ '@testing-library/react':
+ specifier: ^16.3.0
+ version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
+ '@types/bcryptjs':
+ specifier: ^3.0.0
+ version: 3.0.0
+ '@types/formidable':
+ specifier: ^3.4.5
+ version: 3.4.5
+ '@types/jest':
+ specifier: ^30.0.0
+ version: 30.0.0
'@types/node':
specifier: ^20
version: 20.17.19
@@ -171,32 +222,431 @@ importers:
husky:
specifier: ^9.1.7
version: 9.1.7
+ jest:
+ specifier: ^30.1.2
+ version: 30.1.2(@types/node@20.17.19)
+ jest-environment-jsdom:
+ specifier: ^30.1.2
+ version: 30.1.2
lint-staged:
specifier: ^15.4.3
version: 15.4.3
prettier:
specifier: ^3.5.2
version: 3.5.2
+ prisma:
+ specifier: ^6.0.0
+ version: 6.15.0(typescript@5.7.3)
tailwindcss:
specifier: ^4
version: 4.0.8
+ tsx:
+ specifier: ^4.19.2
+ version: 4.20.5
typescript:
specifier: ^5
version: 5.7.3
packages:
+ '@adobe/css-tools@4.4.4':
+ resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
+
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@asamuzakjp/css-color@3.2.0':
+ resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
+
+ '@auth/core@0.40.0':
+ resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==}
+ peerDependencies:
+ '@simplewebauthn/browser': ^9.0.1
+ '@simplewebauthn/server': ^9.0.2
+ nodemailer: ^6.8.0
+ peerDependenciesMeta:
+ '@simplewebauthn/browser':
+ optional: true
+ '@simplewebauthn/server':
+ optional: true
+ nodemailer:
+ optional: true
+
+ '@babel/code-frame@7.27.1':
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.28.0':
+ resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.28.3':
+ resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.28.3':
+ resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.27.2':
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.27.1':
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.3':
+ resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.27.1':
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.27.1':
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.3':
+ resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.28.3':
+ resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-syntax-async-generators@7.8.4':
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-bigint@7.8.3':
+ resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-class-properties@7.12.13':
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-class-static-block@7.14.5':
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-attributes@7.27.1':
+ resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-meta@7.10.4':
+ resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-json-strings@7.8.3':
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-jsx@7.27.1':
+ resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4':
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3':
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-numeric-separator@7.10.4':
+ resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-object-rest-spread@7.8.3':
+ resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3':
+ resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-optional-chaining@7.8.3':
+ resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-private-property-in-object@7.14.5':
+ resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-top-level-await@7.14.5':
+ resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.27.1':
+ resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.26.9':
resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
engines: {node: '>=6.9.0'}
+ '@babel/template@7.27.2':
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.28.3':
+ resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.28.2':
+ resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@bcoe/v8-coverage@0.2.3':
+ resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+
+ '@csstools/color-helpers@5.1.0':
+ resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-calc@2.1.4':
+ resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-color-parser@3.1.0':
+ resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
+ '@effect/platform@0.90.3':
+ resolution: {integrity: sha512-XvQ37yzWQKih4Du2CYladd1i/MzqtgkTPNCaN6Ku6No4CK83hDtXIV/rP03nEoBg2R3Pqgz6gGWmE2id2G81HA==}
+ peerDependencies:
+ effect: ^3.17.7
+
+ '@emnapi/core@1.5.0':
+ resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
+
'@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
+ '@emnapi/runtime@1.5.0':
+ resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
+
+ '@emnapi/wasi-threads@1.1.0':
+ resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
+
+ '@esbuild/aix-ppc64@0.25.9':
+ resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.9':
+ resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.9':
+ resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.9':
+ resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.9':
+ resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.9':
+ resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.9':
+ resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.9':
+ resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.9':
+ resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.9':
+ resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.9':
+ resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.9':
+ resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.9':
+ resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.9':
+ resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.9':
+ resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.9':
+ resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.9':
+ resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.9':
+ resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.9':
+ resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.9':
+ resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.9':
+ resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.9':
+ resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.9':
+ resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.9':
+ resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.9':
+ resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.9':
+ resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@eslint-community/eslint-utils@4.4.1':
resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -246,6 +696,24 @@ packages:
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+ '@formatjs/ecma402-abstract@2.3.4':
+ resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==}
+
+ '@formatjs/fast-memoize@2.2.7':
+ resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
+
+ '@formatjs/icu-messageformat-parser@2.11.2':
+ resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==}
+
+ '@formatjs/icu-skeleton-parser@1.8.14':
+ resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==}
+
+ '@formatjs/intl-localematcher@0.5.10':
+ resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
+
+ '@formatjs/intl-localematcher@0.6.1':
+ resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==}
+
'@hookform/resolvers@4.1.2':
resolution: {integrity: sha512-wl6H9c9wLOZMJAqGLEVKzbCkxJuV+BYuLFZFCQtCwMe0b3qQk4kUBd/ZAj13SwcSqcx86rCgSCyngQfmA6DOWg==}
peerDependencies:
@@ -376,54 +844,204 @@ packages:
cpu: [x64]
os: [win32]
- '@kayron013/lexorank@2.0.0':
- resolution: {integrity: sha512-acQaZPjnsHnXm28i6YATdi/nZ3gs2c5ZFlkFhXcSw0CgDymF+U5v4SYcQT0/ryjBcEjKl1HQWpBLK+PtVOIAHA==}
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
- '@next/env@15.2.4':
- resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==}
+ '@istanbuljs/load-nyc-config@1.1.0':
+ resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
+ engines: {node: '>=8'}
- '@next/eslint-plugin-next@15.2.4':
- resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==}
+ '@istanbuljs/schema@0.1.3':
+ resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+ engines: {node: '>=8'}
- '@next/swc-darwin-arm64@15.2.4':
- resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [darwin]
+ '@jest/console@30.1.2':
+ resolution: {integrity: sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@next/swc-darwin-x64@15.2.4':
- resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [darwin]
+ '@jest/core@30.1.2':
+ resolution: {integrity: sha512-iSLOojkYgM7Lw0FF5egecZh+CiLWe4xICM3WOMjFbewhbWn+ixEoPwY7oK9jSCnLLphMFAjussXp7CE3tHa5EA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
- '@next/swc-linux-arm64-gnu@15.2.4':
- resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
+ '@jest/diff-sequences@30.0.1':
+ resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@next/swc-linux-arm64-musl@15.2.4':
- resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
+ '@jest/environment-jsdom-abstract@30.1.2':
+ resolution: {integrity: sha512-u8kTh/ZBl97GOmnGJLYK/1GuwAruMC4hoP6xuk/kwltmVWsA9u/6fH1/CsPVGt2O+Wn2yEjs8n1B1zZJ62Cx0w==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ jsdom: '*'
+ peerDependenciesMeta:
+ canvas:
+ optional: true
- '@next/swc-linux-x64-gnu@15.2.4':
- resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
+ '@jest/environment@30.1.2':
+ resolution: {integrity: sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@next/swc-linux-x64-musl@15.2.4':
- resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
+ '@jest/expect-utils@30.1.2':
+ resolution: {integrity: sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- '@next/swc-win32-arm64-msvc@15.2.4':
- resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==}
- engines: {node: '>= 10'}
+ '@jest/expect@30.1.2':
+ resolution: {integrity: sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/fake-timers@30.1.2':
+ resolution: {integrity: sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/get-type@30.1.0':
+ resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/globals@30.1.2':
+ resolution: {integrity: sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/pattern@30.0.1':
+ resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/reporters@30.1.2':
+ resolution: {integrity: sha512-8Jd7y3DUFBn8dG/bNJ2blmaJmT2Up74WAXkUJsbL0OuEZHDRRMnS4JmRtLArW2d0H5k8RDdhNN7j70Ki16Zr5g==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
+
+ '@jest/schemas@30.0.5':
+ resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/snapshot-utils@30.1.2':
+ resolution: {integrity: sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/source-map@30.0.1':
+ resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/test-result@30.1.2':
+ resolution: {integrity: sha512-mpKFr8DEpfG5aAfQYA5+3KneAsRBXhF7zwtwqT4UeYBckoOPD1MzVxU6gDHwx4gRB7I1MKL6owyJzr8QRq402Q==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/test-sequencer@30.1.2':
+ resolution: {integrity: sha512-v3vawuj2LC0XjpzF4q0pI0ZlQvMBDNqfRZZ2yHqcsGt7JEYsDK2L1WwrybEGlnOaEvnDFML/Y9xWLiW47Dda8A==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/transform@30.1.2':
+ resolution: {integrity: sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/types@30.0.5':
+ resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.30':
+ resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
+
+ '@kayron013/lexorank@2.0.0':
+ resolution: {integrity: sha512-acQaZPjnsHnXm28i6YATdi/nZ3gs2c5ZFlkFhXcSw0CgDymF+U5v4SYcQT0/ryjBcEjKl1HQWpBLK+PtVOIAHA==}
+
+ '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
+ resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
+ resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
+ resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
+ resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
+ resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
+ resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@napi-rs/wasm-runtime@0.2.12':
+ resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
+
+ '@next/env@15.2.4':
+ resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==}
+
+ '@next/eslint-plugin-next@15.2.4':
+ resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==}
+
+ '@next/swc-darwin-arm64@15.2.4':
+ resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@next/swc-darwin-x64@15.2.4':
+ resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@next/swc-linux-arm64-gnu@15.2.4':
+ resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-arm64-musl@15.2.4':
+ resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-x64-gnu@15.2.4':
+ resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-linux-x64-musl@15.2.4':
+ resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-win32-arm64-msvc@15.2.4':
+ resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==}
+ engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -433,6 +1051,10 @@ packages:
cpu: [x64]
os: [win32]
+ '@noble/hashes@1.8.0':
+ resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+ engines: {node: ^14.21.3 || >=16}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -449,10 +1071,63 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
+ '@opentelemetry/semantic-conventions@1.36.0':
+ resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==}
+ engines: {node: '>=14'}
+
+ '@panva/hkdf@1.2.1':
+ resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
+
+ '@paralleldrive/cuid2@2.2.2':
+ resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
+
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
'@pkgr/core@0.1.1':
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@pkgr/core@0.2.9':
+ resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+
+ '@playwright/test@1.55.0':
+ resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ '@prisma/client@6.15.0':
+ resolution: {integrity: sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==}
+ engines: {node: '>=18.18'}
+ peerDependencies:
+ prisma: '*'
+ typescript: '>=5.1.0'
+ peerDependenciesMeta:
+ prisma:
+ optional: true
+ typescript:
+ optional: true
+
+ '@prisma/config@6.15.0':
+ resolution: {integrity: sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==}
+
+ '@prisma/debug@6.15.0':
+ resolution: {integrity: sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==}
+
+ '@prisma/engines-version@6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb':
+ resolution: {integrity: sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==}
+
+ '@prisma/engines@6.15.0':
+ resolution: {integrity: sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==}
+
+ '@prisma/fetch-engine@6.15.0':
+ resolution: {integrity: sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==}
+
+ '@prisma/get-platform@6.15.0':
+ resolution: {integrity: sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==}
+
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
@@ -1354,6 +2029,9 @@ packages:
'@react-dnd/shallowequal@4.0.2':
resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
+ '@remirror/core-constants@3.0.0':
+ resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
+
'@remixicon/react@4.6.0':
resolution: {integrity: sha512-bY56maEgT5IYUSRotqy9h03IAKJC85vlKtWFg2FKzfs8JPrkdBAYSa9dxoUSKFwGzup8Ux6vjShs9Aec3jvr2w==}
peerDependencies:
@@ -1365,6 +2043,24 @@ packages:
'@rushstack/eslint-patch@1.10.5':
resolution: {integrity: sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==}
+ '@schummar/icu-type-parser@1.21.5':
+ resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
+
+ '@sinclair/typebox@0.34.41':
+ resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
+
+ '@sinonjs/commons@3.0.1':
+ resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
+
+ '@sinonjs/fake-timers@13.0.5':
+ resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==}
+
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
+ '@standard-schema/spec@1.0.0-beta.4':
+ resolution: {integrity: sha512-d3IxtzLo7P1oZ8s8YNvxzBUXRXojSut8pbPrTYtzsc5sn4+53jVqbk66pQerSZbZSJZQux6LkclB/+8IDordHg==}
+
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
@@ -1450,6 +2146,204 @@ packages:
'@tailwindcss/postcss@4.0.8':
resolution: {integrity: sha512-SUwlrXjn1ycmUbA0o0n3Y0LqlXqxN5R8HR+ti+OBbRS79wl2seDmiypEs3xJCuQXe07ol81s1AmRMitBmPveJA==}
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@6.8.0':
+ resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+
+ '@testing-library/react@16.3.0':
+ resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@testing-library/user-event@14.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
+ '@tiptap/core@3.3.0':
+ resolution: {integrity: sha512-YAmFITHzgp/hafA7Ety1qMo4Tl5e5b2+06LaiB9k3rAI7gfO6AXCwhXUqm3fCScmBfMMvMycq9IOIiHk946IzA==}
+ peerDependencies:
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/extension-blockquote@3.3.0':
+ resolution: {integrity: sha512-CdntInLJl2L+suvX+YVNDQ0XZ0+NruGco50Gu4XOWkxyAK18FitW8aa+MnbaulPvinrVDyo4H1PQYdZsy3PIbw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-bold@3.3.0':
+ resolution: {integrity: sha512-L/+NI+3OCpKWrcFTPOff8a1QuyTYp6PrANBmlShnnpXnv8mqE5wvQhxjn7sVF8nIDeMqgMFc9jP7pDCdfNykyw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-bubble-menu@3.3.0':
+ resolution: {integrity: sha512-339PWX//JanNK+gNIccV0K+XYRcWUIftfGPZDVPrP0xnAYdDBVyEaRh7CXh56uijPrr7UfL69f+GYvEYnpn2bQ==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/extension-bullet-list@3.3.0':
+ resolution: {integrity: sha512-f8RFBip8MO4VJGdlQ1Bl5lITj/qRgMokeknyZL3vGxm34mHIovCXmPAqoPPFm5x7BEdlRO0Wv2U8QKuDQsdChw==}
+ peerDependencies:
+ '@tiptap/extension-list': ^3.3.0
+
+ '@tiptap/extension-code-block@3.3.0':
+ resolution: {integrity: sha512-CljkYxkv1XdUEOheQQrae7uqI0gAESfLg7kTxZtIeKLUmsJ7izRn+Ynpt7s83v772rzNw7cm47g2NW+pmNLH+Q==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/extension-code@3.3.0':
+ resolution: {integrity: sha512-YBIEAjjPsp5acb16VqTS7osKd7lwzIHAt72WLBrQL52UkThfjLHBBC88ARY3E3Cg54W/Rx4lqEs7civ+1AIVbg==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-document@3.3.0':
+ resolution: {integrity: sha512-J7w2pva06OSBoEUxIyL/faXx+P97H3L0Q8tCsH5umXgUVew2xLYq6nEDqtHOFIXRp/bvrQd677UlQorBaRrWeQ==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-dropcursor@3.3.0':
+ resolution: {integrity: sha512-aPgEmW6A7K9lq7Mfw8h/ATCJKsWJ5KHimnNoQGwu1V7onWyn3Set/tSgfqKQVGoyvk5xwgRqAg41pYYBIrxF7Q==}
+ peerDependencies:
+ '@tiptap/extensions': ^3.3.0
+
+ '@tiptap/extension-floating-menu@3.3.0':
+ resolution: {integrity: sha512-+YbP4hwTM4YLgrei4A3Jqk+Q10L5z1tQ8/vpEgXrzTdUrSnQo9XbyZxmFYWen5TT0ezgPEopnx/eTru6NS2+Lg==}
+ peerDependencies:
+ '@floating-ui/dom': ^1.0.0
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/extension-gapcursor@3.3.0':
+ resolution: {integrity: sha512-6r4sYYzPJykqJGiKZE0JaC58rOas0uxEjtx0oDM7PygcBirYqSt7GHKVggEBFykrnkvnkH8D7Lh7UyKIQ1cKVg==}
+ peerDependencies:
+ '@tiptap/extensions': ^3.3.0
+
+ '@tiptap/extension-hard-break@3.3.0':
+ resolution: {integrity: sha512-kfmxS7o/m6F8LO58POrn4RVc943liLAKqXSwxSPOeCdbHr7v5bvBk+jRJcYv84EZ+sZ2axr7ePq2R2WpEBFxjw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-heading@3.3.0':
+ resolution: {integrity: sha512-oUs0NKAZlXTJ+tNz1fz8SMM28FIIlrX4NYi9ItWSyfWLz3NnOlnCU2aTg9fac9LeG0WdxiRyI7yq19GARWujSw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-horizontal-rule@3.3.0':
+ resolution: {integrity: sha512-7mt2+ETWd3XecQMRKVwG6UBbiU55Pe/RJkyyVx7AEKzzMCC2g9F+77Y4uZitIrcmqXqWsBmQEIYkIVme6h12/A==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/extension-italic@3.3.0':
+ resolution: {integrity: sha512-O43OReewZd1n//yy7M2qw/Rrz8RW/QK7dD3H4tr0+TNC+0KYYXZMhsB5P7dAkygEfWakQwftUfUlUiZ/UZKtpw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-link@3.3.0':
+ resolution: {integrity: sha512-bQa8KHj5TFnZn8bCdpqDQUzsdsSt/VahZ9ZxvGgv3szyKrbprvugmXbSmU1m0CwLegt/66OcO/r+BdUU+yciAw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/extension-list-item@3.3.0':
+ resolution: {integrity: sha512-l00wnnMq9iPcXwYaIiFGBznGyD6kxkC3fsonxF3p3QVBRgL1PxHHIIUxxNpkaEu7vB7qIpzI+ypspDmVDh1ZeA==}
+ peerDependencies:
+ '@tiptap/extension-list': ^3.3.0
+
+ '@tiptap/extension-list-keymap@3.3.0':
+ resolution: {integrity: sha512-xXdQJxF6kQXzdPXUiseflIuwTQGVh6REQ3Uq66mr1zBia8DPVAzwV0cpJoEh1TCFeGtbShb23nuWZXa+7J36Xg==}
+ peerDependencies:
+ '@tiptap/extension-list': ^3.3.0
+
+ '@tiptap/extension-list@3.3.0':
+ resolution: {integrity: sha512-nxmmkmGm2Zz+ar3+vke7/UVsea64z6tNdhC0c0aucII0JzZF1ZhTCBTYhINdkXxFrGegqatAI1fcO1T1LXRVAw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/extension-ordered-list@3.3.0':
+ resolution: {integrity: sha512-zBw3zl/3bhOmNNVAbXVnpGug0gxNCYqJ8nyMCmX16dFK9JR+WIUaX8RHBDuCLQ0PJidfGQMdm/Uf/Vc8RCaHzg==}
+ peerDependencies:
+ '@tiptap/extension-list': ^3.3.0
+
+ '@tiptap/extension-paragraph@3.3.0':
+ resolution: {integrity: sha512-5Ju3RlvlJwiIiOjA/D2Dzq/pCmx9vA/2vTB8KDyj+mkvOlq2Cp9QT7bYdw3i99IHfVF7fCYzoPoKclDfOyM7Mg==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-strike@3.3.0':
+ resolution: {integrity: sha512-qTSywaQYtlVo3B2NaLSKgmh5/O5m4XspSME8mekinGH6cTv3d+H3p1SUhQoc1Ue+65CI01+GsLU4v/Gi4B2xNw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-text@3.3.0':
+ resolution: {integrity: sha512-yhNpKfRlZsZFtjBQyiWyjg9WPUTzjgxR/ZID1UEY3SGo9aPUuvAvEnn2v2tSopHyf+qBMyN5IfSjrauauGkWMA==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extension-underline@3.3.0':
+ resolution: {integrity: sha512-ipiXsXBxmIQGqcMOaXsnP7iQ6/VO/UIxP3X3rS4ToHgVVF5RIFSs732fv5p3R88TdlVz4hqM9S39W+JesFB2Fw==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+
+ '@tiptap/extensions@3.3.0':
+ resolution: {integrity: sha512-Oey5aBg02FQHjldfjn6ebnzpH3x1Q9mPSgSuXNCjDQ51hak7LCGsVFhH+X9PrZtUALlEpEQWNcREgPBwqGM5ow==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+
+ '@tiptap/pm@3.3.0':
+ resolution: {integrity: sha512-uKsysdjP5kQbQRQN8YinN5lr71TVgsHKhxgkq/psXZzNoUh2fPoNpzkhZTaondgr0IXFwzYX+DA5cLkzu4ig/A==}
+
+ '@tiptap/react@3.3.0':
+ resolution: {integrity: sha512-7TCloYAMcEA0Z+Ke5m2X8BRM15amIzXRYPPXLlLKfDdYZgUh597jfF25gT5k/1MRPREo84p+GyP1OcY2WdTrjA==}
+ peerDependencies:
+ '@tiptap/core': ^3.3.0
+ '@tiptap/pm': ^3.3.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tiptap/starter-kit@3.3.0':
+ resolution: {integrity: sha512-F527JUR0BgLHmOSZomEQ3INdiriIzaq4AMDGuG53MtBd1s+b1lvE4/U8gnQyVBRQC921Gl1xG8eJb2a60Rhk1w==}
+
+ '@tybys/wasm-util@0.10.0':
+ resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/bcryptjs@3.0.0':
+ resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
+ deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
+
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@@ -1480,12 +2374,39 @@ packages:
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+ '@types/formidable@3.4.5':
+ resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==}
+
+ '@types/istanbul-lib-coverage@2.0.6':
+ resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
+
+ '@types/istanbul-lib-report@3.0.3':
+ resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}
+
+ '@types/istanbul-reports@3.0.4':
+ resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
+
+ '@types/jest@30.0.0':
+ resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==}
+
+ '@types/jsdom@21.1.7':
+ resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+ '@types/linkify-it@5.0.0':
+ resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
+
+ '@types/markdown-it@14.1.2':
+ resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
+
+ '@types/mdurl@2.0.0':
+ resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
+
'@types/node@20.17.19':
resolution: {integrity: sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==}
@@ -1497,6 +2418,21 @@ packages:
'@types/react@19.0.10':
resolution: {integrity: sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==}
+ '@types/stack-utils@2.0.3':
+ resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
+
+ '@types/tough-cookie@4.0.5':
+ resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
+ '@types/yargs-parser@21.0.3':
+ resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
+
+ '@types/yargs@17.0.33':
+ resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
+
'@typescript-eslint/eslint-plugin@8.25.0':
resolution: {integrity: sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1544,45 +2480,175 @@ packages:
resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- acorn-jsx@5.3.2:
- resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
- peerDependencies:
- acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+ '@ungap/structured-clone@1.3.0':
+ resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
- acorn@8.14.0:
- resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
- engines: {node: '>=0.4.0'}
- hasBin: true
+ '@unrs/resolver-binding-android-arm-eabi@1.11.1':
+ resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
+ cpu: [arm]
+ os: [android]
- ajv@6.12.6:
- resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ '@unrs/resolver-binding-android-arm64@1.11.1':
+ resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==}
+ cpu: [arm64]
+ os: [android]
- ansi-escapes@7.0.0:
- resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
- engines: {node: '>=18'}
+ '@unrs/resolver-binding-darwin-arm64@1.11.1':
+ resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==}
+ cpu: [arm64]
+ os: [darwin]
- ansi-regex@6.1.0:
- resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
- engines: {node: '>=12'}
+ '@unrs/resolver-binding-darwin-x64@1.11.1':
+ resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==}
+ cpu: [x64]
+ os: [darwin]
- ansi-styles@4.3.0:
- resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
- engines: {node: '>=8'}
+ '@unrs/resolver-binding-freebsd-x64@1.11.1':
+ resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==}
+ cpu: [x64]
+ os: [freebsd]
- ansi-styles@6.2.1:
- resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
- engines: {node: '>=12'}
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
+ resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==}
+ cpu: [arm]
+ os: [linux]
- argparse@2.0.1:
- resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
+ resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==}
+ cpu: [arm]
+ os: [linux]
- aria-hidden@1.2.4:
- resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
- engines: {node: '>=10'}
+ '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
+ resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
+ cpu: [arm64]
+ os: [linux]
- aria-query@5.3.2:
- resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
- engines: {node: '>= 0.4'}
+ '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
+ resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
+ resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
+ resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
+ resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
+ resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
+ resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
+ cpu: [x64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-x64-musl@1.11.1':
+ resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@unrs/resolver-binding-wasm32-wasi@1.11.1':
+ resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
+ resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
+ resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
+ resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==}
+ cpu: [x64]
+ os: [win32]
+
+ '@uploadthing/mime-types@0.3.6':
+ resolution: {integrity: sha512-t3tTzgwFV9+1D7lNDYc7Lr7kBwotHaX0ZsvoCGe7xGnXKo9z0jG2Sjl/msll12FeoLj77nyhsxevXyGpQDBvLg==}
+
+ '@uploadthing/shared@7.1.10':
+ resolution: {integrity: sha512-R/XSA3SfCVnLIzFpXyGaKPfbwlYlWYSTuGjTFHuJhdAomuBuhopAHLh2Ois5fJibAHzi02uP1QCKbgTAdmArqg==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.14.0:
+ resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ansi-escapes@4.3.2:
+ resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
+ engines: {node: '>=8'}
+
+ ansi-escapes@7.0.0:
+ resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
+ engines: {node: '>=18'}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.1.0:
+ resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ aria-hidden@1.2.4:
+ resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
+ engines: {node: '>=10'}
+
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
array-buffer-byte-length@1.0.2:
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
@@ -1616,6 +2682,9 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
+ asap@2.0.6:
+ resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
+
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
@@ -1635,9 +2704,38 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
+ babel-jest@30.1.2:
+ resolution: {integrity: sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ '@babel/core': ^7.11.0
+
+ babel-plugin-istanbul@7.0.0:
+ resolution: {integrity: sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==}
+ engines: {node: '>=12'}
+
+ babel-plugin-jest-hoist@30.0.1:
+ resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ babel-preset-current-node-syntax@1.2.0:
+ resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0 || ^8.0.0-0
+
+ babel-preset-jest@30.0.1:
+ resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ '@babel/core': ^7.11.0
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ bcryptjs@3.0.2:
+ resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==}
+ hasBin: true
+
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -1648,10 +2746,29 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
+ browserslist@4.25.4:
+ resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ bser@2.1.1:
+ resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
+
+ buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
+ c12@3.1.0:
+ resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==}
+ peerDependencies:
+ magicast: ^0.3.5
+ peerDependenciesMeta:
+ magicast:
+ optional: true
+
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -1668,9 +2785,20 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
+ camelcase@6.3.0:
+ resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+ engines: {node: '>=10'}
+
caniuse-lite@1.0.30001700:
resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==}
+ caniuse-lite@1.0.30001739:
+ resolution: {integrity: sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==}
+
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -1679,6 +2807,24 @@ packages:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ char-regex@1.0.2:
+ resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
+ engines: {node: '>=10'}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ ci-info@4.3.0:
+ resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==}
+ engines: {node: '>=8'}
+
+ citty@0.1.6:
+ resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
+
+ cjs-module-lexer@2.1.0:
+ resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==}
+
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -1693,6 +2839,10 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1703,6 +2853,13 @@ packages:
react: ^18.0.0
react-dom: ^18.0.0
+ co@4.6.0:
+ resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
+ engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
+
+ collect-v8-coverage@1.0.2:
+ resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1727,10 +2884,30 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ confbox@0.2.2:
+ resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
+
+ consola@3.4.2:
+ resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
+ engines: {node: ^14.18.0 || >=16.10.0}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ crelt@1.0.6:
+ resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ cssstyle@4.6.0:
+ resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
+ engines: {node: '>=18'}
+
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -1781,6 +2958,10 @@ packages:
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
+ data-urls@5.0.0:
+ resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+ engines: {node: '>=18'}
+
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -1816,9 +2997,28 @@ packages:
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
+ dedent@1.6.0:
+ resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==}
+ peerDependencies:
+ babel-plugin-macros: ^3.1.0
+ peerDependenciesMeta:
+ babel-plugin-macros:
+ optional: true
+
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ deepmerge-ts@7.1.5:
+ resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
+ engines: {node: '>=16.0.0'}
+
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -1827,6 +3027,16 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
+ defu@6.1.4:
+ resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ destr@2.0.5:
+ resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
+
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
@@ -1836,9 +3046,16 @@ packages:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
+ detect-newline@3.1.0:
+ resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
+ engines: {node: '>=8'}
+
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ dezalgo@1.0.4:
+ resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
+
dnd-core@16.0.1:
resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
@@ -1846,27 +3063,71 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+ dotenv@16.6.1:
+ resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
+ engines: {node: '>=12'}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
+ effect@3.16.12:
+ resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==}
+
+ effect@3.17.7:
+ resolution: {integrity: sha512-dpt0ONUn3zzAuul6k4nC/coTTw27AL5nhkORXgTi6NfMPzqWYa1M05oKmOMTxpVSTKepqXVcW9vIwkuaaqx9zA==}
+
+ electron-to-chromium@1.5.211:
+ resolution: {integrity: sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==}
+
+ emittery@0.13.1:
+ resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
+ engines: {node: '>=12'}
+
emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ empathic@2.0.0:
+ resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
+ engines: {node: '>=14'}
+
enhanced-resolve@5.18.1:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
+ error-ex@1.3.2:
+ resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
+
es-abstract@1.23.9:
resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==}
engines: {node: '>= 0.4'}
@@ -1899,6 +3160,19 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ esbuild@0.25.9:
+ resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@2.0.0:
+ resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
+ engines: {node: '>=8'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -2023,6 +3297,11 @@ packages:
resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
esquery@1.6.0:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
engines: {node: '>=0.10'}
@@ -2045,10 +3324,29 @@ packages:
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+ execa@5.1.1:
+ resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
+ engines: {node: '>=10'}
+
execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
+ exit-x@0.2.2:
+ resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==}
+ engines: {node: '>= 0.8.0'}
+
+ expect@30.1.2:
+ resolution: {integrity: sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ exsolve@1.0.7:
+ resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
+
+ fast-check@3.23.2:
+ resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
+ engines: {node: '>=8.0.0'}
+
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -2076,6 +3374,9 @@ packages:
fastq@1.19.0:
resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==}
+ fb-watchman@2.0.2:
+ resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
+
fdir@6.4.3:
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
peerDependencies:
@@ -2092,6 +3393,13 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ find-my-way-ts@0.1.6:
+ resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==}
+
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -2107,6 +3415,14 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
+ foreground-child@3.3.1:
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
+ engines: {node: '>=14'}
+
+ formidable@3.5.4:
+ resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
+ engines: {node: '>=14.0.0'}
+
framer-motion@12.4.10:
resolution: {integrity: sha512-3Msuyjcr1Pb5hjkn4EJcRe1HumaveP0Gbv4DBMKTPKcV/1GSMkQXj+Uqgneys+9DPcZM18Hac9qY9iUEF5LZtg==}
peerDependencies:
@@ -2121,6 +3437,19 @@ packages:
react-dom:
optional: true
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -2131,6 +3460,14 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
get-east-asian-width@1.3.0:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
@@ -2143,10 +3480,18 @@ packages:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
+ get-package-type@0.1.0:
+ resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
+ engines: {node: '>=8.0.0'}
+
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
+ get-stream@6.0.1:
+ resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+ engines: {node: '>=10'}
+
get-stream@8.0.1:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
engines: {node: '>=16'}
@@ -2158,6 +3503,10 @@ packages:
get-tsconfig@4.10.0:
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
+ giget@2.0.0:
+ resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
+ hasBin: true
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -2166,6 +3515,14 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
+ glob@10.4.5:
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ hasBin: true
+
+ glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Glob versions prior to v9 are no longer supported
+
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -2214,6 +3571,25 @@ packages:
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ human-signals@2.1.0:
+ resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
+ engines: {node: '>=10.17.0'}
+
human-signals@5.0.0:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
@@ -2223,6 +3599,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2231,10 +3611,26 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
+ import-local@3.2.0:
+ resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
+ engines: {node: '>=8'}
+ hasBin: true
+
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -2243,10 +3639,16 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
+ intl-messageformat@10.7.16:
+ resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
+
is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
@@ -2289,6 +3691,10 @@ packages:
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
engines: {node: '>= 0.4'}
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
is-fullwidth-code-point@4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
engines: {node: '>=12'}
@@ -2297,6 +3703,10 @@ packages:
resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
engines: {node: '>=18'}
+ is-generator-fn@2.1.0:
+ resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
+ engines: {node: '>=6'}
+
is-generator-function@1.1.0:
resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
engines: {node: '>= 0.4'}
@@ -2317,6 +3727,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -2329,6 +3742,10 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
+ is-stream@2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+
is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -2363,25 +3780,209 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-instrument@6.0.3:
+ resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==}
+ engines: {node: '>=10'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-lib-source-maps@5.0.6:
+ resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.2.0:
+ resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
+ engines: {node: '>=8'}
+
iterator.prototype@1.1.5:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
- jiti@2.4.2:
- resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
- hasBin: true
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
- js-tokens@4.0.0:
- resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ jest-changed-files@30.0.5:
+ resolution: {integrity: sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
- js-yaml@4.1.0:
- resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ jest-circus@30.1.2:
+ resolution: {integrity: sha512-pyqgRv00fPbU3QBjN9I5QRd77eCWA19NA7BLgI1veFvbUIFpeDCKbnG1oyRr6q5/jPEW2zDfqZ/r6fvfE85vrA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-cli@30.1.2:
+ resolution: {integrity: sha512-Q7H6GGo/0TBB8Mhm3Ab7KKJHn6GeMVff+/8PVCQ7vXXahvr5sRERnNbxuVJAMiVY2JQm5roA7CHYOYlH+gzmUg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
- json-buffer@3.0.1:
- resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ jest-config@30.1.2:
+ resolution: {integrity: sha512-gCuBeE/cksjQ3e1a8H4YglZJuVPcnLZQK9jC70E6GbkHNQKPasnOO+r9IYdsUbAekb6c7eVRR8laGLMF06gMqg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ '@types/node': '*'
+ esbuild-register: '>=3.4.0'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ esbuild-register:
+ optional: true
+ ts-node:
+ optional: true
- json-schema-traverse@0.4.1:
+ jest-diff@30.1.2:
+ resolution: {integrity: sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-docblock@30.0.1:
+ resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-each@30.1.0:
+ resolution: {integrity: sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-environment-jsdom@30.1.2:
+ resolution: {integrity: sha512-LXsfAh5+mDTuXDONGl1ZLYxtJEaS06GOoxJb2arcJTjIfh1adYg8zLD8f6P0df8VmjvCaMrLmc1PgHUI/YUTbg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
+ jest-environment-node@30.1.2:
+ resolution: {integrity: sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-haste-map@30.1.0:
+ resolution: {integrity: sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-leak-detector@30.1.0:
+ resolution: {integrity: sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-matcher-utils@30.1.2:
+ resolution: {integrity: sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-message-util@30.1.0:
+ resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-mock@30.0.5:
+ resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-pnp-resolver@1.2.3:
+ resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==}
+ engines: {node: '>=6'}
+ peerDependencies:
+ jest-resolve: '*'
+ peerDependenciesMeta:
+ jest-resolve:
+ optional: true
+
+ jest-regex-util@30.0.1:
+ resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-resolve-dependencies@30.1.2:
+ resolution: {integrity: sha512-HJjyoaedY4wrwda+eqvgjbwFykrAnQEmhuT0bMyOV3GQIyLPcunZcjfkm77Zr11ujwl34ySdc4qYnm7SG75TjA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-resolve@30.1.0:
+ resolution: {integrity: sha512-hASe7D/wRtZw8Cm607NrlF7fi3HWC5wmA5jCVc2QjQAB2pTwP9eVZILGEi6OeSLNUtE1zb04sXRowsdh5CUjwA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-runner@30.1.2:
+ resolution: {integrity: sha512-eu9AzpDY/QV+7NuMg6fZMpQ7M24cBkl5dyS1Xj7iwDPDriOmLUXR8rLojESibcIX+sCDTO4KvUeaxWCH1fbTvg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-runtime@30.1.2:
+ resolution: {integrity: sha512-zU02si+lAITgyRmVRgJn/AB4cnakq8+o7bP+5Z+N1A4r2mq40zGbmrg3UpYQWCkeim17tx8w1Tnmt6tQ6y9PGA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-snapshot@30.1.2:
+ resolution: {integrity: sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-util@30.0.5:
+ resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-validate@30.1.0:
+ resolution: {integrity: sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-watcher@30.1.2:
+ resolution: {integrity: sha512-MtoGuEgqsBM8Jkn52oEj+mXLtF94+njPlHI5ydfduZL5MHrTFr14ZG1CUX1xAbY23dbSZCCEkEPhBM3cQd12Jg==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-worker@30.1.0:
+ resolution: {integrity: sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest@30.1.2:
+ resolution: {integrity: sha512-iLreJmUWdANLD2UIbebrXxQqU9jIxv2ahvrBNfff55deL9DtVxm8ZJBLk/kmn0AQ+FyCTrNSlGbMdTgSasldYA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
+
+ jiti@2.4.2:
+ resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+ hasBin: true
+
+ jose@6.1.0:
+ resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@3.14.1:
+ resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
+ hasBin: true
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ jsdom@26.1.0:
+ resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-stable-stringify-without-jsonify@1.0.1:
@@ -2391,6 +3992,11 @@ packages:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@@ -2405,6 +4011,10 @@ packages:
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
engines: {node: '>=0.10'}
+ leven@3.1.0:
+ resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
+ engines: {node: '>=6'}
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -2477,6 +4087,15 @@ packages:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ linkify-it@5.0.0:
+ resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
+ linkifyjs@4.3.2:
+ resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
+
lint-staged@15.4.3:
resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==}
engines: {node: '>=18.12.0'}
@@ -2486,6 +4105,10 @@ packages:
resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==}
engines: {node: '>=18.0.0'}
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -2507,15 +4130,39 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
lucide-react@0.476.0:
resolution: {integrity: sha512-x6cLTk8gahdUPje0hSgLN1/MgiJH+Xl90Xoxy9bkPAsMPOUiyRSKR4JCDPGVCEpyqnZXH3exFWNItcvra9WzUQ==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
+ makeerror@1.0.12:
+ resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
+
+ markdown-it@14.1.0:
+ resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+ hasBin: true
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mdurl@2.0.0:
+ resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -2527,6 +4174,10 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
+ mimic-fn@2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+
mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
@@ -2535,6 +4186,10 @@ packages:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -2545,6 +4200,10 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
motion-dom@12.4.10:
resolution: {integrity: sha512-ISP5u6FTceoD6qKdLupIPU/LyXBrxGox+P2e3mBbm1+pLdlBbwv01YENJr7+1WZnW5ucVKzFScYsV1eXTCG4Xg==}
@@ -2568,14 +4227,59 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ msgpackr-extract@3.0.3:
+ resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==}
+ hasBin: true
+
+ msgpackr@1.11.5:
+ resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==}
+
+ multipasta@0.2.7:
+ resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==}
+
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ napi-postinstall@0.3.3:
+ resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ hasBin: true
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
+ next-auth@5.0.0-beta.29:
+ resolution: {integrity: sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==}
+ peerDependencies:
+ '@simplewebauthn/browser': ^9.0.1
+ '@simplewebauthn/server': ^9.0.2
+ next: ^14.0.0-0 || ^15.0.0-0
+ nodemailer: ^6.6.5
+ react: ^18.2.0 || ^19.0.0-0
+ peerDependenciesMeta:
+ '@simplewebauthn/browser':
+ optional: true
+ '@simplewebauthn/server':
+ optional: true
+ nodemailer:
+ optional: true
+
+ next-intl@4.3.5:
+ resolution: {integrity: sha512-tT3SltfpPOCAQ9kVNr+8t6FUtVf8G0WFlJcVc8zj4WCMfuF8XFk4gZCN/MtjgDgkUISw5aKamOClJB4EsV95WQ==}
+ peerDependencies:
+ next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
+ typescript: ^5.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
next-themes@0.4.4:
resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
peerDependencies:
@@ -2603,10 +4307,42 @@ packages:
sass:
optional: true
+ node-fetch-native@1.6.7:
+ resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
+
+ node-gyp-build-optional-packages@5.2.2:
+ resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
+ hasBin: true
+
+ node-int64@0.4.0:
+ resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
+
+ node-releases@2.0.19:
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ npm-run-path@4.0.1:
+ resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
+ engines: {node: '>=8'}
+
npm-run-path@5.3.0:
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ nwsapi@2.2.21:
+ resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==}
+
+ nypm@0.6.1:
+ resolution: {integrity: sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==}
+ engines: {node: ^14.16.0 || >=16.10.0}
+ hasBin: true
+
+ oauth4webapi@3.8.1:
+ resolution: {integrity: sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==}
+
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -2639,6 +4375,16 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ onetime@5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+
onetime@6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
@@ -2647,30 +4393,71 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
+ openai@5.16.0:
+ resolution: {integrity: sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw==}
+ hasBin: true
+ peerDependencies:
+ ws: ^8.18.0
+ zod: ^3.23.8
+ peerDependenciesMeta:
+ ws:
+ optional: true
+ zod:
+ optional: true
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
+ orderedmap@2.1.1:
+ resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
+
own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -2682,6 +4469,16 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ perfect-debounce@1.0.0:
+ resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2698,6 +4495,27 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
+ pirates@4.0.7:
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
+ engines: {node: '>= 6'}
+
+ pkg-dir@4.2.0:
+ resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
+ engines: {node: '>=8'}
+
+ pkg-types@2.3.0:
+ resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+
+ playwright-core@1.55.0:
+ resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.55.0:
+ resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -2710,6 +4528,14 @@ packages:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
+ preact-render-to-string@6.5.11:
+ resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
+ peerDependencies:
+ preact: '>=10'
+
+ preact@10.24.3:
+ resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2723,16 +4549,105 @@ packages:
engines: {node: '>=14'}
hasBin: true
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
+ pretty-format@30.0.5:
+ resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ prisma@6.15.0:
+ resolution: {integrity: sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==}
+ engines: {node: '>=18.18'}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.1.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ prosemirror-changeset@2.3.1:
+ resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==}
+
+ prosemirror-collab@1.3.1:
+ resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
+
+ prosemirror-commands@1.7.1:
+ resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
+
+ prosemirror-dropcursor@1.8.2:
+ resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
+
+ prosemirror-gapcursor@1.3.2:
+ resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
+
+ prosemirror-history@1.4.1:
+ resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==}
+
+ prosemirror-inputrules@1.5.0:
+ resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==}
+
+ prosemirror-keymap@1.2.3:
+ resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
+
+ prosemirror-markdown@1.13.2:
+ resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==}
+
+ prosemirror-menu@1.2.5:
+ resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==}
+
+ prosemirror-model@1.25.3:
+ resolution: {integrity: sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==}
+
+ prosemirror-schema-basic@1.2.4:
+ resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==}
+
+ prosemirror-schema-list@1.5.1:
+ resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
+
+ prosemirror-state@1.4.3:
+ resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
+
+ prosemirror-tables@1.8.1:
+ resolution: {integrity: sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==}
+
+ prosemirror-trailing-node@3.0.0:
+ resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==}
+ peerDependencies:
+ prosemirror-model: ^1.22.1
+ prosemirror-state: ^1.4.2
+ prosemirror-view: ^1.33.8
+
+ prosemirror-transform@1.10.4:
+ resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==}
+
+ prosemirror-view@1.40.1:
+ resolution: {integrity: sha512-pbwUjt3G7TlsQQHDiYSupWBhJswpLVB09xXm1YiJPdkjkh9Pe7Y51XdLh5VWIZmROLY8UpUpG03lkdhm9lzIBA==}
+
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ pure-rand@6.1.0:
+ resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
+
+ pure-rand@7.0.1:
+ resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
+
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ rc9@2.1.2:
+ resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
+
react-day-picker@8.10.1:
resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==}
peerDependencies:
@@ -2776,6 +4691,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
@@ -2841,6 +4759,10 @@ packages:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'}
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
recharts-scale@0.4.5:
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
@@ -2851,6 +4773,10 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
@@ -2865,10 +4791,22 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-cwd@3.0.0:
+ resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
+ engines: {node: '>=8'}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -2892,6 +4830,12 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+ rope-sequence@1.3.4:
+ resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
+
+ rrweb-cssom@0.8.0:
+ resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -2907,6 +4851,13 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.25.0:
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
@@ -2919,6 +4870,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -2959,6 +4915,9 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -2966,6 +4925,10 @@ packages:
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
+ slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+
slice-ansi@5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
@@ -2984,9 +4947,26 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ source-map-support@0.5.13:
+ resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
+ sqids@0.3.0:
+ resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==}
+
stable-hash@0.0.4:
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
+ stack-utils@2.0.6:
+ resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
+ engines: {node: '>=10'}
+
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -2995,6 +4975,18 @@ packages:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
+ string-length@4.0.2:
+ resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
+ engines: {node: '>=10'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
@@ -3022,6 +5014,10 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
@@ -3030,10 +5026,22 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
+ strip-bom@4.0.0:
+ resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
+ engines: {node: '>=8'}
+
+ strip-final-newline@2.0.0:
+ resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
+ engines: {node: '>=6'}
+
strip-final-newline@3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
engines: {node: '>=12'}
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -3055,10 +5063,21 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
+ supports-color@8.1.1:
+ resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+ engines: {node: '>=10'}
+
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
+ synckit@0.11.11:
+ resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+
synckit@0.9.2:
resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -3078,17 +5097,42 @@ packages:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
+ test-exclude@6.0.0:
+ resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
+ engines: {node: '>=8'}
+
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+ tinyexec@1.0.1:
+ resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
+
tinyglobby@0.2.12:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
engines: {node: '>=12.0.0'}
+ tldts-core@6.1.86:
+ resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
+
+ tldts@6.1.86:
+ resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
+ hasBin: true
+
+ tmpl@1.0.5:
+ resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ tough-cookie@5.1.2:
+ resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
+ engines: {node: '>=16'}
+
+ tr46@5.1.1:
+ resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
+ engines: {node: '>=18'}
+
ts-api-utils@2.0.1:
resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
engines: {node: '>=18.12'}
@@ -3101,10 +5145,23 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tsx@4.20.5:
+ resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-detect@4.0.8:
+ resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
+ engines: {node: '>=4'}
+
+ type-fest@0.21.3:
+ resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
+ engines: {node: '>=10'}
+
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -3126,6 +5183,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
@@ -3133,6 +5193,36 @@ packages:
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+ unrs-resolver@1.11.1:
+ resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
+
+ update-browserslist-db@1.1.3:
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uploadthing@7.7.4:
+ resolution: {integrity: sha512-rlK/4JWHW5jP30syzWGBFDDXv3WJDdT8gn9OoxRJmXLoXi94hBmyyjxihGlNrKhBc81czyv8TkzMioe/OuKGfA==}
+ engines: {node: '>=18.13.0'}
+ peerDependencies:
+ express: '*'
+ fastify: '*'
+ h3: '*'
+ next: '*'
+ tailwindcss: ^3.0.0 || ^4.0.0-beta.0
+ peerDependenciesMeta:
+ express:
+ optional: true
+ fastify:
+ optional: true
+ h3:
+ optional: true
+ next:
+ optional: true
+ tailwindcss:
+ optional: true
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -3146,6 +5236,11 @@ packages:
'@types/react':
optional: true
+ use-intl@4.3.5:
+ resolution: {integrity: sha512-qyL1TZNesVbzj/75ZbYsi+xzNSiFqp5rIVsiAN0JT8rPMSjX0/3KQz76aJIrngI1/wIQdVYFVdImWh5yAv+dWA==}
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
+
use-sidecar@1.1.3:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
@@ -3156,6 +5251,11 @@ packages:
'@types/react':
optional: true
+ use-sync-external-store@1.5.0:
+ resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
usehooks-ts@3.1.1:
resolution: {integrity: sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==}
engines: {node: '>=16.15.0'}
@@ -3166,9 +5266,39 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
+ v8-to-istanbul@9.3.0:
+ resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
+ engines: {node: '>=10.12.0'}
+
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
+ walker@1.0.8:
+ resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
+
+ webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+
+ whatwg-mimetype@4.0.0:
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+ engines: {node: '>=18'}
+
+ whatwg-url@14.2.0:
+ resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
+ engines: {node: '>=18'}
+
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -3194,15 +5324,64 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
wrap-ansi@9.0.0:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
engines: {node: '>=18'}
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ write-file-atomic@5.0.1:
+ resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ ws@8.18.3:
+ resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
yaml@2.7.0:
resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==}
engines: {node: '>= 14'}
hasBin: true
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -3230,17 +5409,351 @@ packages:
snapshots:
+ '@adobe/css-tools@4.4.4': {}
+
'@alloc/quick-lru@5.2.0': {}
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.30
+
+ '@asamuzakjp/css-color@3.2.0':
+ dependencies:
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+ lru-cache: 10.4.3
+
+ '@auth/core@0.40.0':
+ dependencies:
+ '@panva/hkdf': 1.2.1
+ jose: 6.1.0
+ oauth4webapi: 3.8.1
+ preact: 10.24.3
+ preact-render-to-string: 6.5.11(preact@10.24.3)
+
+ '@babel/code-frame@7.27.1':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.27.1
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.28.0': {}
+
+ '@babel/core@7.28.3':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.3
+ '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3)
+ '@babel/helpers': 7.28.3
+ '@babel/parser': 7.28.3
+ '@babel/template': 7.27.2
+ '@babel/traverse': 7.28.3
+ '@babel/types': 7.28.2
+ convert-source-map: 2.0.0
+ debug: 4.4.0
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.28.3':
+ dependencies:
+ '@babel/parser': 7.28.3
+ '@babel/types': 7.28.2
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.30
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.27.2':
+ dependencies:
+ '@babel/compat-data': 7.28.0
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.25.4
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.28.3
+ '@babel/types': 7.28.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+ '@babel/traverse': 7.28.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.27.1': {}
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.27.1': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.3':
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.2
+
+ '@babel/parser@7.28.3':
+ dependencies:
+ '@babel/types': 7.28.2
+
+ '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/helper-plugin-utils': 7.27.1
+
'@babel/runtime@7.26.9':
dependencies:
regenerator-runtime: 0.14.1
+ '@babel/template@7.27.2':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/parser': 7.28.3
+ '@babel/types': 7.28.2
+
+ '@babel/traverse@7.28.3':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.3
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.28.3
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.2
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.28.2':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+
+ '@bcoe/v8-coverage@0.2.3': {}
+
+ '@csstools/color-helpers@5.1.0': {}
+
+ '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/color-helpers': 5.1.0
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
+ '@effect/platform@0.90.3(effect@3.17.7)':
+ dependencies:
+ '@opentelemetry/semantic-conventions': 1.36.0
+ effect: 3.17.7
+ find-my-way-ts: 0.1.6
+ msgpackr: 1.11.5
+ multipasta: 0.2.7
+
+ '@emnapi/core@1.5.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.1.0
+ tslib: 2.8.1
+ optional: true
+
'@emnapi/runtime@1.3.1':
dependencies:
tslib: 2.8.1
optional: true
+ '@emnapi/runtime@1.5.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.1.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@esbuild/aix-ppc64@0.25.9':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/android-arm@0.25.9':
+ optional: true
+
+ '@esbuild/android-x64@0.25.9':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.9':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.9':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.9':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.9':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.9':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.9':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.9':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.9':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.9':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.9':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.9':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.9':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.9':
+ optional: true
+
'@eslint-community/eslint-utils@4.4.1(eslint@9.21.0(jiti@2.4.2))':
dependencies:
eslint: 9.21.0(jiti@2.4.2)
@@ -3300,6 +5813,36 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
+ '@formatjs/ecma402-abstract@2.3.4':
+ dependencies:
+ '@formatjs/fast-memoize': 2.2.7
+ '@formatjs/intl-localematcher': 0.6.1
+ decimal.js: 10.6.0
+ tslib: 2.8.1
+
+ '@formatjs/fast-memoize@2.2.7':
+ dependencies:
+ tslib: 2.8.1
+
+ '@formatjs/icu-messageformat-parser@2.11.2':
+ dependencies:
+ '@formatjs/ecma402-abstract': 2.3.4
+ '@formatjs/icu-skeleton-parser': 1.8.14
+ tslib: 2.8.1
+
+ '@formatjs/icu-skeleton-parser@1.8.14':
+ dependencies:
+ '@formatjs/ecma402-abstract': 2.3.4
+ tslib: 2.8.1
+
+ '@formatjs/intl-localematcher@0.5.10':
+ dependencies:
+ tslib: 2.8.1
+
+ '@formatjs/intl-localematcher@0.6.1':
+ dependencies:
+ tslib: 2.8.1
+
'@hookform/resolvers@4.1.2(react-hook-form@7.54.2(react@19.0.0))':
dependencies:
'@standard-schema/utils': 0.3.0
@@ -3372,28 +5915,276 @@ snapshots:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
- '@img/sharp-linuxmusl-arm64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
+ '@img/sharp-linuxmusl-arm64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.33.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.0.4
+ optional: true
+
+ '@img/sharp-wasm32@0.33.5':
+ dependencies:
+ '@emnapi/runtime': 1.3.1
+ optional: true
+
+ '@img/sharp-win32-ia32@0.33.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.33.5':
+ optional: true
+
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@istanbuljs/load-nyc-config@1.1.0':
+ dependencies:
+ camelcase: 5.3.1
+ find-up: 4.1.0
+ get-package-type: 0.1.0
+ js-yaml: 3.14.1
+ resolve-from: 5.0.0
+
+ '@istanbuljs/schema@0.1.3': {}
+
+ '@jest/console@30.1.2':
+ dependencies:
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ chalk: 4.1.2
+ jest-message-util: 30.1.0
+ jest-util: 30.0.5
+ slash: 3.0.0
+
+ '@jest/core@30.1.2':
+ dependencies:
+ '@jest/console': 30.1.2
+ '@jest/pattern': 30.0.1
+ '@jest/reporters': 30.1.2
+ '@jest/test-result': 30.1.2
+ '@jest/transform': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ ansi-escapes: 4.3.2
+ chalk: 4.1.2
+ ci-info: 4.3.0
+ exit-x: 0.2.2
+ graceful-fs: 4.2.11
+ jest-changed-files: 30.0.5
+ jest-config: 30.1.2(@types/node@20.17.19)
+ jest-haste-map: 30.1.0
+ jest-message-util: 30.1.0
+ jest-regex-util: 30.0.1
+ jest-resolve: 30.1.0
+ jest-resolve-dependencies: 30.1.2
+ jest-runner: 30.1.2
+ jest-runtime: 30.1.2
+ jest-snapshot: 30.1.2
+ jest-util: 30.0.5
+ jest-validate: 30.1.0
+ jest-watcher: 30.1.2
+ micromatch: 4.0.8
+ pretty-format: 30.0.5
+ slash: 3.0.0
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - esbuild-register
+ - supports-color
+ - ts-node
+
+ '@jest/diff-sequences@30.0.1': {}
+
+ '@jest/environment-jsdom-abstract@30.1.2(jsdom@26.1.0)':
+ dependencies:
+ '@jest/environment': 30.1.2
+ '@jest/fake-timers': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/jsdom': 21.1.7
+ '@types/node': 20.17.19
+ jest-mock: 30.0.5
+ jest-util: 30.0.5
+ jsdom: 26.1.0
+
+ '@jest/environment@30.1.2':
+ dependencies:
+ '@jest/fake-timers': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ jest-mock: 30.0.5
+
+ '@jest/expect-utils@30.1.2':
+ dependencies:
+ '@jest/get-type': 30.1.0
+
+ '@jest/expect@30.1.2':
+ dependencies:
+ expect: 30.1.2
+ jest-snapshot: 30.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@jest/fake-timers@30.1.2':
+ dependencies:
+ '@jest/types': 30.0.5
+ '@sinonjs/fake-timers': 13.0.5
+ '@types/node': 20.17.19
+ jest-message-util: 30.1.0
+ jest-mock: 30.0.5
+ jest-util: 30.0.5
+
+ '@jest/get-type@30.1.0': {}
+
+ '@jest/globals@30.1.2':
+ dependencies:
+ '@jest/environment': 30.1.2
+ '@jest/expect': 30.1.2
+ '@jest/types': 30.0.5
+ jest-mock: 30.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@jest/pattern@30.0.1':
+ dependencies:
+ '@types/node': 20.17.19
+ jest-regex-util: 30.0.1
+
+ '@jest/reporters@30.1.2':
+ dependencies:
+ '@bcoe/v8-coverage': 0.2.3
+ '@jest/console': 30.1.2
+ '@jest/test-result': 30.1.2
+ '@jest/transform': 30.1.2
+ '@jest/types': 30.0.5
+ '@jridgewell/trace-mapping': 0.3.30
+ '@types/node': 20.17.19
+ chalk: 4.1.2
+ collect-v8-coverage: 1.0.2
+ exit-x: 0.2.2
+ glob: 10.4.5
+ graceful-fs: 4.2.11
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-instrument: 6.0.3
+ istanbul-lib-report: 3.0.1
+ istanbul-lib-source-maps: 5.0.6
+ istanbul-reports: 3.2.0
+ jest-message-util: 30.1.0
+ jest-util: 30.0.5
+ jest-worker: 30.1.0
+ slash: 3.0.0
+ string-length: 4.0.2
+ v8-to-istanbul: 9.3.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@jest/schemas@30.0.5':
+ dependencies:
+ '@sinclair/typebox': 0.34.41
+
+ '@jest/snapshot-utils@30.1.2':
+ dependencies:
+ '@jest/types': 30.0.5
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ natural-compare: 1.4.0
+
+ '@jest/source-map@30.0.1':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.30
+ callsites: 3.1.0
+ graceful-fs: 4.2.11
+
+ '@jest/test-result@30.1.2':
+ dependencies:
+ '@jest/console': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/istanbul-lib-coverage': 2.0.6
+ collect-v8-coverage: 1.0.2
+
+ '@jest/test-sequencer@30.1.2':
+ dependencies:
+ '@jest/test-result': 30.1.2
+ graceful-fs: 4.2.11
+ jest-haste-map: 30.1.0
+ slash: 3.0.0
+
+ '@jest/transform@30.1.2':
+ dependencies:
+ '@babel/core': 7.28.3
+ '@jest/types': 30.0.5
+ '@jridgewell/trace-mapping': 0.3.30
+ babel-plugin-istanbul: 7.0.0
+ chalk: 4.1.2
+ convert-source-map: 2.0.0
+ fast-json-stable-stringify: 2.1.0
+ graceful-fs: 4.2.11
+ jest-haste-map: 30.1.0
+ jest-regex-util: 30.0.1
+ jest-util: 30.0.5
+ micromatch: 4.0.8
+ pirates: 4.0.7
+ slash: 3.0.0
+ write-file-atomic: 5.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@jest/types@30.0.5':
+ dependencies:
+ '@jest/pattern': 30.0.1
+ '@jest/schemas': 30.0.5
+ '@types/istanbul-lib-coverage': 2.0.6
+ '@types/istanbul-reports': 3.0.4
+ '@types/node': 20.17.19
+ '@types/yargs': 17.0.33
+ chalk: 4.1.2
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.30
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.30':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@kayron013/lexorank@2.0.0': {}
+
+ '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
+ optional: true
+
+ '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
optional: true
- '@img/sharp-linuxmusl-x64@0.33.5':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.0.4
+ '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
optional: true
- '@img/sharp-wasm32@0.33.5':
- dependencies:
- '@emnapi/runtime': 1.3.1
+ '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
optional: true
- '@img/sharp-win32-ia32@0.33.5':
+ '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
optional: true
- '@img/sharp-win32-x64@0.33.5':
+ '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true
- '@kayron013/lexorank@2.0.0': {}
+ '@napi-rs/wasm-runtime@0.2.12':
+ dependencies:
+ '@emnapi/core': 1.5.0
+ '@emnapi/runtime': 1.5.0
+ '@tybys/wasm-util': 0.10.0
+ optional: true
'@next/env@15.2.4': {}
@@ -3425,6 +6216,8 @@ snapshots:
'@next/swc-win32-x64-msvc@15.2.4':
optional: true
+ '@noble/hashes@1.8.0': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -3439,8 +6232,60 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
+ '@opentelemetry/semantic-conventions@1.36.0': {}
+
+ '@panva/hkdf@1.2.1': {}
+
+ '@paralleldrive/cuid2@2.2.2':
+ dependencies:
+ '@noble/hashes': 1.8.0
+
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
'@pkgr/core@0.1.1': {}
+ '@pkgr/core@0.2.9': {}
+
+ '@playwright/test@1.55.0':
+ dependencies:
+ playwright: 1.55.0
+
+ '@prisma/client@6.15.0(prisma@6.15.0(typescript@5.7.3))(typescript@5.7.3)':
+ optionalDependencies:
+ prisma: 6.15.0(typescript@5.7.3)
+ typescript: 5.7.3
+
+ '@prisma/config@6.15.0':
+ dependencies:
+ c12: 3.1.0
+ deepmerge-ts: 7.1.5
+ effect: 3.16.12
+ empathic: 2.0.0
+ transitivePeerDependencies:
+ - magicast
+
+ '@prisma/debug@6.15.0': {}
+
+ '@prisma/engines-version@6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb': {}
+
+ '@prisma/engines@6.15.0':
+ dependencies:
+ '@prisma/debug': 6.15.0
+ '@prisma/engines-version': 6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb
+ '@prisma/fetch-engine': 6.15.0
+ '@prisma/get-platform': 6.15.0
+
+ '@prisma/fetch-engine@6.15.0':
+ dependencies:
+ '@prisma/debug': 6.15.0
+ '@prisma/engines-version': 6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb
+ '@prisma/get-platform': 6.15.0
+
+ '@prisma/get-platform@6.15.0':
+ dependencies:
+ '@prisma/debug': 6.15.0
+
'@radix-ui/number@1.1.0': {}
'@radix-ui/primitive@1.0.1':
@@ -4308,6 +7153,8 @@ snapshots:
'@react-dnd/shallowequal@4.0.2': {}
+ '@remirror/core-constants@3.0.0': {}
+
'@remixicon/react@4.6.0(react@19.0.0)':
dependencies:
react: 19.0.0
@@ -4316,6 +7163,22 @@ snapshots:
'@rushstack/eslint-patch@1.10.5': {}
+ '@schummar/icu-type-parser@1.21.5': {}
+
+ '@sinclair/typebox@0.34.41': {}
+
+ '@sinonjs/commons@3.0.1':
+ dependencies:
+ type-detect: 4.0.8
+
+ '@sinonjs/fake-timers@13.0.5':
+ dependencies:
+ '@sinonjs/commons': 3.0.1
+
+ '@standard-schema/spec@1.0.0': {}
+
+ '@standard-schema/spec@1.0.0-beta.4': {}
+
'@standard-schema/utils@0.3.0': {}
'@swc/counter@0.1.3': {}
@@ -4386,6 +7249,247 @@ snapshots:
postcss: 8.5.3
tailwindcss: 4.0.8
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/runtime': 7.26.9
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@6.8.0':
+ dependencies:
+ '@adobe/css-tools': 4.4.4
+ aria-query: 5.3.2
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ picocolors: 1.1.1
+ redent: 3.0.0
+
+ '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.9
+ '@testing-library/dom': 10.4.1
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ optionalDependencies:
+ '@types/react': 19.0.10
+ '@types/react-dom': 19.0.4(@types/react@19.0.10)
+
+ '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
+ '@tiptap/core@3.3.0(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@tiptap/pm': 3.3.0
+
+ '@tiptap/extension-blockquote@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-bold@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-bubble-menu@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@floating-ui/dom': 1.6.13
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+ optional: true
+
+ '@tiptap/extension-bullet-list@3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-code-block@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+
+ '@tiptap/extension-code@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-document@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-dropcursor@3.3.0(@tiptap/extensions@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/extensions': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-floating-menu@3.3.0(@floating-ui/dom@1.6.13)(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@floating-ui/dom': 1.6.13
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+ optional: true
+
+ '@tiptap/extension-gapcursor@3.3.0(@tiptap/extensions@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/extensions': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-hard-break@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-heading@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-horizontal-rule@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+
+ '@tiptap/extension-italic@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-link@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+ linkifyjs: 4.3.2
+
+ '@tiptap/extension-list-item@3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-list-keymap@3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+
+ '@tiptap/extension-ordered-list@3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/extension-list': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-paragraph@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-strike@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-text@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extension-underline@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+
+ '@tiptap/extensions@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+
+ '@tiptap/pm@3.3.0':
+ dependencies:
+ prosemirror-changeset: 2.3.1
+ prosemirror-collab: 1.3.1
+ prosemirror-commands: 1.7.1
+ prosemirror-dropcursor: 1.8.2
+ prosemirror-gapcursor: 1.3.2
+ prosemirror-history: 1.4.1
+ prosemirror-inputrules: 1.5.0
+ prosemirror-keymap: 1.2.3
+ prosemirror-markdown: 1.13.2
+ prosemirror-menu: 1.2.5
+ prosemirror-model: 1.25.3
+ prosemirror-schema-basic: 1.2.4
+ prosemirror-schema-list: 1.5.1
+ prosemirror-state: 1.4.3
+ prosemirror-tables: 1.8.1
+ prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)
+ prosemirror-transform: 1.10.4
+ prosemirror-view: 1.40.1
+
+ '@tiptap/react@3.3.0(@floating-ui/dom@1.6.13)(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+ '@types/use-sync-external-store': 0.0.6
+ fast-deep-equal: 3.1.3
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ use-sync-external-store: 1.5.0(react@19.0.0)
+ optionalDependencies:
+ '@tiptap/extension-bubble-menu': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+ '@tiptap/extension-floating-menu': 3.3.0(@floating-ui/dom@1.6.13)(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+ transitivePeerDependencies:
+ - '@floating-ui/dom'
+
+ '@tiptap/starter-kit@3.3.0':
+ dependencies:
+ '@tiptap/core': 3.3.0(@tiptap/pm@3.3.0)
+ '@tiptap/extension-blockquote': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-bold': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-bullet-list': 3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))
+ '@tiptap/extension-code': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-code-block': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+ '@tiptap/extension-document': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-dropcursor': 3.3.0(@tiptap/extensions@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))
+ '@tiptap/extension-gapcursor': 3.3.0(@tiptap/extensions@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))
+ '@tiptap/extension-hard-break': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-heading': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-horizontal-rule': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+ '@tiptap/extension-italic': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-link': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+ '@tiptap/extension-list': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+ '@tiptap/extension-list-item': 3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))
+ '@tiptap/extension-list-keymap': 3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))
+ '@tiptap/extension-ordered-list': 3.3.0(@tiptap/extension-list@3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0))
+ '@tiptap/extension-paragraph': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-strike': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-text': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extension-underline': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))
+ '@tiptap/extensions': 3.3.0(@tiptap/core@3.3.0(@tiptap/pm@3.3.0))(@tiptap/pm@3.3.0)
+ '@tiptap/pm': 3.3.0
+
+ '@tybys/wasm-util@0.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/aria-query@5.0.4': {}
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.28.3
+ '@babel/types': 7.28.2
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.28.2
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.28.3
+ '@babel/types': 7.28.2
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.28.2
+
+ '@types/bcryptjs@3.0.0':
+ dependencies:
+ bcryptjs: 3.0.2
+
'@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {}
@@ -4412,10 +7516,44 @@ snapshots:
'@types/estree@1.0.6': {}
+ '@types/formidable@3.4.5':
+ dependencies:
+ '@types/node': 20.17.19
+
+ '@types/istanbul-lib-coverage@2.0.6': {}
+
+ '@types/istanbul-lib-report@3.0.3':
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.6
+
+ '@types/istanbul-reports@3.0.4':
+ dependencies:
+ '@types/istanbul-lib-report': 3.0.3
+
+ '@types/jest@30.0.0':
+ dependencies:
+ expect: 30.1.2
+ pretty-format: 30.0.5
+
+ '@types/jsdom@21.1.7':
+ dependencies:
+ '@types/node': 20.17.19
+ '@types/tough-cookie': 4.0.5
+ parse5: 7.3.0
+
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
+ '@types/linkify-it@5.0.0': {}
+
+ '@types/markdown-it@14.1.2':
+ dependencies:
+ '@types/linkify-it': 5.0.0
+ '@types/mdurl': 2.0.0
+
+ '@types/mdurl@2.0.0': {}
+
'@types/node@20.17.19':
dependencies:
undici-types: 6.19.8
@@ -4428,6 +7566,18 @@ snapshots:
dependencies:
csstype: 3.1.3
+ '@types/stack-utils@2.0.3': {}
+
+ '@types/tough-cookie@4.0.5': {}
+
+ '@types/use-sync-external-store@0.0.6': {}
+
+ '@types/yargs-parser@21.0.3': {}
+
+ '@types/yargs@17.0.33':
+ dependencies:
+ '@types/yargs-parser': 21.0.3
+
'@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -4505,12 +7655,83 @@ snapshots:
'@typescript-eslint/types': 8.25.0
eslint-visitor-keys: 4.2.0
+ '@ungap/structured-clone@1.3.0': {}
+
+ '@unrs/resolver-binding-android-arm-eabi@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-android-arm64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-arm64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-x64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-freebsd-x64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-wasm32-wasi@1.11.1':
+ dependencies:
+ '@napi-rs/wasm-runtime': 0.2.12
+ optional: true
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
+ optional: true
+
+ '@uploadthing/mime-types@0.3.6': {}
+
+ '@uploadthing/shared@7.1.10':
+ dependencies:
+ '@uploadthing/mime-types': 0.3.6
+ effect: 3.17.7
+ sqids: 0.3.0
+
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
acorn: 8.14.0
acorn@8.14.0: {}
+ agent-base@7.1.4: {}
+
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -4518,24 +7739,45 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
+ ansi-escapes@4.3.2:
+ dependencies:
+ type-fest: 0.21.3
+
ansi-escapes@7.0.0:
dependencies:
environment: 1.1.0
+ ansi-regex@5.0.1: {}
+
ansi-regex@6.1.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
+ ansi-styles@5.2.0: {}
+
ansi-styles@6.2.1: {}
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+
+ argparse@1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+
argparse@2.0.1: {}
aria-hidden@1.2.4:
dependencies:
tslib: 2.8.1
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
aria-query@5.3.2: {}
array-buffer-byte-length@1.0.2:
@@ -4602,6 +7844,8 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
+ asap@2.0.6: {}
+
ast-types-flow@0.0.8: {}
async-function@1.0.0: {}
@@ -4614,8 +7858,64 @@ snapshots:
axobject-query@4.1.0: {}
+ babel-jest@30.1.2(@babel/core@7.28.3):
+ dependencies:
+ '@babel/core': 7.28.3
+ '@jest/transform': 30.1.2
+ '@types/babel__core': 7.20.5
+ babel-plugin-istanbul: 7.0.0
+ babel-preset-jest: 30.0.1(@babel/core@7.28.3)
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ slash: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-istanbul@7.0.0:
+ dependencies:
+ '@babel/helper-plugin-utils': 7.27.1
+ '@istanbuljs/load-nyc-config': 1.1.0
+ '@istanbuljs/schema': 0.1.3
+ istanbul-lib-instrument: 6.0.3
+ test-exclude: 6.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-jest-hoist@30.0.1:
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.2
+ '@types/babel__core': 7.20.5
+
+ babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.3):
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.3)
+ '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.3)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.3)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.3)
+ '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.3)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.3)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.3)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.3)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.3)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.3)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.3)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.3)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.3)
+
+ babel-preset-jest@30.0.1(@babel/core@7.28.3):
+ dependencies:
+ '@babel/core': 7.28.3
+ babel-plugin-jest-hoist: 30.0.1
+ babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3)
+
balanced-match@1.0.2: {}
+ bcryptjs@3.0.2: {}
+
brace-expansion@1.1.11:
dependencies:
balanced-match: 1.0.2
@@ -4629,10 +7929,38 @@ snapshots:
dependencies:
fill-range: 7.1.1
+ browserslist@4.25.4:
+ dependencies:
+ caniuse-lite: 1.0.30001739
+ electron-to-chromium: 1.5.211
+ node-releases: 2.0.19
+ update-browserslist-db: 1.1.3(browserslist@4.25.4)
+
+ bser@2.1.1:
+ dependencies:
+ node-int64: 0.4.0
+
+ buffer-from@1.1.2: {}
+
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
+ c12@3.1.0:
+ dependencies:
+ chokidar: 4.0.3
+ confbox: 0.2.2
+ defu: 6.1.4
+ dotenv: 16.6.1
+ exsolve: 1.0.7
+ giget: 2.0.0
+ jiti: 2.4.2
+ ohash: 2.0.11
+ pathe: 2.0.3
+ perfect-debounce: 1.0.0
+ pkg-types: 2.3.0
+ rc9: 2.1.2
+
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -4652,14 +7980,34 @@ snapshots:
callsites@3.1.0: {}
- caniuse-lite@1.0.30001700: {}
+ camelcase@5.3.1: {}
+
+ camelcase@6.3.0: {}
+
+ caniuse-lite@1.0.30001700: {}
+
+ caniuse-lite@1.0.30001739: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ chalk@5.4.1: {}
+
+ char-regex@1.0.2: {}
+
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.2
+
+ ci-info@4.3.0: {}
- chalk@4.1.2:
+ citty@0.1.6:
dependencies:
- ansi-styles: 4.3.0
- supports-color: 7.2.0
+ consola: 3.4.2
- chalk@5.4.1: {}
+ cjs-module-lexer@2.1.0: {}
class-variance-authority@0.7.1:
dependencies:
@@ -4676,6 +8024,12 @@ snapshots:
client-only@0.0.1: {}
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
clsx@2.1.1: {}
cmdk@1.0.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
@@ -4688,6 +8042,10 @@ snapshots:
- '@types/react'
- '@types/react-dom'
+ co@4.6.0: {}
+
+ collect-v8-coverage@1.0.2: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -4712,12 +8070,27 @@ snapshots:
concat-map@0.0.1: {}
+ confbox@0.2.2: {}
+
+ consola@3.4.2: {}
+
+ convert-source-map@2.0.0: {}
+
+ crelt@1.0.6: {}
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
+ css.escape@1.5.1: {}
+
+ cssstyle@4.6.0:
+ dependencies:
+ '@asamuzakjp/css-color': 3.2.0
+ rrweb-cssom: 0.8.0
+
csstype@3.1.3: {}
d3-array@3.2.4:
@@ -4760,6 +8133,11 @@ snapshots:
damerau-levenshtein@1.0.8: {}
+ data-urls@5.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.3
@@ -4790,8 +8168,16 @@ snapshots:
decimal.js-light@2.5.1: {}
+ decimal.js@10.6.0: {}
+
+ dedent@1.6.0: {}
+
deep-is@0.1.4: {}
+ deepmerge-ts@7.1.5: {}
+
+ deepmerge@4.3.1: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -4804,13 +8190,26 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
+ defu@6.1.4: {}
+
+ dequal@2.0.3: {}
+
+ destr@2.0.5: {}
+
detect-libc@1.0.3: {}
detect-libc@2.0.3:
optional: true
+ detect-newline@3.1.0: {}
+
detect-node-es@1.1.0: {}
+ dezalgo@1.0.4:
+ dependencies:
+ asap: 2.0.6
+ wrappy: 1.0.2
+
dnd-core@16.0.1:
dependencies:
'@react-dnd/asap': 5.0.2
@@ -4821,28 +8220,62 @@ snapshots:
dependencies:
esutils: 2.0.3
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.26.9
csstype: 3.1.3
+ dotenv@16.6.1: {}
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
+ eastasianwidth@0.2.0: {}
+
+ effect@3.16.12:
+ dependencies:
+ '@standard-schema/spec': 1.0.0
+ fast-check: 3.23.2
+
+ effect@3.17.7:
+ dependencies:
+ '@standard-schema/spec': 1.0.0
+ fast-check: 3.23.2
+
+ electron-to-chromium@1.5.211: {}
+
+ emittery@0.13.1: {}
+
emoji-regex@10.4.0: {}
+ emoji-regex@8.0.0: {}
+
emoji-regex@9.2.2: {}
+ empathic@2.0.0: {}
+
enhanced-resolve@5.18.1:
dependencies:
graceful-fs: 4.2.11
tapable: 2.2.1
+ entities@4.5.0: {}
+
+ entities@6.0.1: {}
+
environment@1.1.0: {}
+ error-ex@1.3.2:
+ dependencies:
+ is-arrayish: 0.2.1
+
es-abstract@1.23.9:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -4941,6 +8374,39 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ esbuild@0.25.9:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.9
+ '@esbuild/android-arm': 0.25.9
+ '@esbuild/android-arm64': 0.25.9
+ '@esbuild/android-x64': 0.25.9
+ '@esbuild/darwin-arm64': 0.25.9
+ '@esbuild/darwin-x64': 0.25.9
+ '@esbuild/freebsd-arm64': 0.25.9
+ '@esbuild/freebsd-x64': 0.25.9
+ '@esbuild/linux-arm': 0.25.9
+ '@esbuild/linux-arm64': 0.25.9
+ '@esbuild/linux-ia32': 0.25.9
+ '@esbuild/linux-loong64': 0.25.9
+ '@esbuild/linux-mips64el': 0.25.9
+ '@esbuild/linux-ppc64': 0.25.9
+ '@esbuild/linux-riscv64': 0.25.9
+ '@esbuild/linux-s390x': 0.25.9
+ '@esbuild/linux-x64': 0.25.9
+ '@esbuild/netbsd-arm64': 0.25.9
+ '@esbuild/netbsd-x64': 0.25.9
+ '@esbuild/openbsd-arm64': 0.25.9
+ '@esbuild/openbsd-x64': 0.25.9
+ '@esbuild/openharmony-arm64': 0.25.9
+ '@esbuild/sunos-x64': 0.25.9
+ '@esbuild/win32-arm64': 0.25.9
+ '@esbuild/win32-ia32': 0.25.9
+ '@esbuild/win32-x64': 0.25.9
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@2.0.0: {}
+
escape-string-regexp@4.0.0: {}
eslint-config-next@15.2.4(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3):
@@ -5140,6 +8606,8 @@ snapshots:
acorn-jsx: 5.3.2(acorn@8.14.0)
eslint-visitor-keys: 4.2.0
+ esprima@4.0.1: {}
+
esquery@1.6.0:
dependencies:
estraverse: 5.3.0
@@ -5156,6 +8624,18 @@ snapshots:
eventemitter3@5.0.1: {}
+ execa@5.1.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ get-stream: 6.0.1
+ human-signals: 2.1.0
+ is-stream: 2.0.1
+ merge-stream: 2.0.0
+ npm-run-path: 4.0.1
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+ strip-final-newline: 2.0.0
+
execa@8.0.1:
dependencies:
cross-spawn: 7.0.6
@@ -5168,6 +8648,23 @@ snapshots:
signal-exit: 4.1.0
strip-final-newline: 3.0.0
+ exit-x@0.2.2: {}
+
+ expect@30.1.2:
+ dependencies:
+ '@jest/expect-utils': 30.1.2
+ '@jest/get-type': 30.1.0
+ jest-matcher-utils: 30.1.2
+ jest-message-util: 30.1.0
+ jest-mock: 30.0.5
+ jest-util: 30.0.5
+
+ exsolve@1.0.7: {}
+
+ fast-check@3.23.2:
+ dependencies:
+ pure-rand: 6.1.0
+
fast-deep-equal@3.1.3: {}
fast-diff@1.3.0: {}
@@ -5198,6 +8695,10 @@ snapshots:
dependencies:
reusify: 1.1.0
+ fb-watchman@2.0.2:
+ dependencies:
+ bser: 2.1.1
+
fdir@6.4.3(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
@@ -5210,6 +8711,13 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
+ find-my-way-ts@0.1.6: {}
+
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -5226,6 +8734,17 @@ snapshots:
dependencies:
is-callable: 1.2.7
+ foreground-child@3.3.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
+ formidable@3.5.4:
+ dependencies:
+ '@paralleldrive/cuid2': 2.2.2
+ dezalgo: 1.0.4
+ once: 1.4.0
+
framer-motion@12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
motion-dom: 12.4.10
@@ -5235,6 +8754,14 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
+ fs.realpath@1.0.0: {}
+
+ fsevents@2.3.2:
+ optional: true
+
+ fsevents@2.3.3:
+ optional: true
+
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@@ -5248,6 +8775,10 @@ snapshots:
functions-have-names@1.2.3: {}
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
get-east-asian-width@1.3.0: {}
get-intrinsic@1.3.0:
@@ -5265,11 +8796,15 @@ snapshots:
get-nonce@1.0.1: {}
+ get-package-type@0.1.0: {}
+
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
+ get-stream@6.0.1: {}
+
get-stream@8.0.1: {}
get-symbol-description@1.1.0:
@@ -5282,6 +8817,15 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
+ giget@2.0.0:
+ dependencies:
+ citty: 0.1.6
+ consola: 3.4.2
+ defu: 6.1.4
+ node-fetch-native: 1.6.7
+ nypm: 0.6.1
+ pathe: 2.0.3
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -5290,6 +8834,24 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob@10.4.5:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
+ glob@7.2.3:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
globals@14.0.0: {}
globalthis@1.0.4:
@@ -5329,10 +8891,36 @@ snapshots:
dependencies:
react-is: 16.13.1
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
+ html-escaper@2.0.2: {}
+
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+
+ human-signals@2.1.0: {}
+
human-signals@5.0.0: {}
husky@9.1.7: {}
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
ignore@5.3.2: {}
import-fresh@3.3.1:
@@ -5340,8 +8928,22 @@ snapshots:
parent-module: 1.0.1
resolve-from: 4.0.0
+ import-local@3.2.0:
+ dependencies:
+ pkg-dir: 4.2.0
+ resolve-cwd: 3.0.0
+
imurmurhash@0.1.4: {}
+ indent-string@4.0.0: {}
+
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ inherits@2.0.4: {}
+
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -5350,12 +8952,21 @@ snapshots:
internmap@2.0.3: {}
+ intl-messageformat@10.7.16:
+ dependencies:
+ '@formatjs/ecma402-abstract': 2.3.4
+ '@formatjs/fast-memoize': 2.2.7
+ '@formatjs/icu-messageformat-parser': 2.11.2
+ tslib: 2.8.1
+
is-array-buffer@3.0.5:
dependencies:
call-bind: 1.0.8
call-bound: 1.0.3
get-intrinsic: 1.3.0
+ is-arrayish@0.2.1: {}
+
is-arrayish@0.3.2:
optional: true
@@ -5403,12 +9014,16 @@ snapshots:
dependencies:
call-bound: 1.0.3
+ is-fullwidth-code-point@3.0.0: {}
+
is-fullwidth-code-point@4.0.0: {}
is-fullwidth-code-point@5.0.0:
dependencies:
get-east-asian-width: 1.3.0
+ is-generator-fn@2.1.0: {}
+
is-generator-function@1.1.0:
dependencies:
call-bound: 1.0.3
@@ -5429,6 +9044,8 @@ snapshots:
is-number@7.0.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-regex@1.2.1:
dependencies:
call-bound: 1.0.3
@@ -5442,6 +9059,8 @@ snapshots:
dependencies:
call-bound: 1.0.3
+ is-stream@2.0.1: {}
+
is-stream@3.0.0: {}
is-string@1.1.1:
@@ -5474,6 +9093,37 @@ snapshots:
isexe@2.0.0: {}
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-instrument@6.0.3:
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/parser': 7.28.3
+ '@istanbuljs/schema': 0.1.3
+ istanbul-lib-coverage: 3.2.2
+ semver: 7.7.1
+ transitivePeerDependencies:
+ - supports-color
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-lib-source-maps@5.0.6:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.30
+ debug: 4.4.0
+ istanbul-lib-coverage: 3.2.2
+ transitivePeerDependencies:
+ - supports-color
+
+ istanbul-reports@3.2.0:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
iterator.prototype@1.1.5:
dependencies:
define-data-property: 1.1.4
@@ -5483,16 +9133,383 @@ snapshots:
has-symbols: 1.1.0
set-function-name: 2.0.2
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
+ jest-changed-files@30.0.5:
+ dependencies:
+ execa: 5.1.1
+ jest-util: 30.0.5
+ p-limit: 3.1.0
+
+ jest-circus@30.1.2:
+ dependencies:
+ '@jest/environment': 30.1.2
+ '@jest/expect': 30.1.2
+ '@jest/test-result': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ chalk: 4.1.2
+ co: 4.6.0
+ dedent: 1.6.0
+ is-generator-fn: 2.1.0
+ jest-each: 30.1.0
+ jest-matcher-utils: 30.1.2
+ jest-message-util: 30.1.0
+ jest-runtime: 30.1.2
+ jest-snapshot: 30.1.2
+ jest-util: 30.0.5
+ p-limit: 3.1.0
+ pretty-format: 30.0.5
+ pure-rand: 7.0.1
+ slash: 3.0.0
+ stack-utils: 2.0.6
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+
+ jest-cli@30.1.2(@types/node@20.17.19):
+ dependencies:
+ '@jest/core': 30.1.2
+ '@jest/test-result': 30.1.2
+ '@jest/types': 30.0.5
+ chalk: 4.1.2
+ exit-x: 0.2.2
+ import-local: 3.2.0
+ jest-config: 30.1.2(@types/node@20.17.19)
+ jest-util: 30.0.5
+ jest-validate: 30.1.0
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - esbuild-register
+ - supports-color
+ - ts-node
+
+ jest-config@30.1.2(@types/node@20.17.19):
+ dependencies:
+ '@babel/core': 7.28.3
+ '@jest/get-type': 30.1.0
+ '@jest/pattern': 30.0.1
+ '@jest/test-sequencer': 30.1.2
+ '@jest/types': 30.0.5
+ babel-jest: 30.1.2(@babel/core@7.28.3)
+ chalk: 4.1.2
+ ci-info: 4.3.0
+ deepmerge: 4.3.1
+ glob: 10.4.5
+ graceful-fs: 4.2.11
+ jest-circus: 30.1.2
+ jest-docblock: 30.0.1
+ jest-environment-node: 30.1.2
+ jest-regex-util: 30.0.1
+ jest-resolve: 30.1.0
+ jest-runner: 30.1.2
+ jest-util: 30.0.5
+ jest-validate: 30.1.0
+ micromatch: 4.0.8
+ parse-json: 5.2.0
+ pretty-format: 30.0.5
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 20.17.19
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+
+ jest-diff@30.1.2:
+ dependencies:
+ '@jest/diff-sequences': 30.0.1
+ '@jest/get-type': 30.1.0
+ chalk: 4.1.2
+ pretty-format: 30.0.5
+
+ jest-docblock@30.0.1:
+ dependencies:
+ detect-newline: 3.1.0
+
+ jest-each@30.1.0:
+ dependencies:
+ '@jest/get-type': 30.1.0
+ '@jest/types': 30.0.5
+ chalk: 4.1.2
+ jest-util: 30.0.5
+ pretty-format: 30.0.5
+
+ jest-environment-jsdom@30.1.2:
+ dependencies:
+ '@jest/environment': 30.1.2
+ '@jest/environment-jsdom-abstract': 30.1.2(jsdom@26.1.0)
+ '@types/jsdom': 21.1.7
+ '@types/node': 20.17.19
+ jsdom: 26.1.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ jest-environment-node@30.1.2:
+ dependencies:
+ '@jest/environment': 30.1.2
+ '@jest/fake-timers': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ jest-mock: 30.0.5
+ jest-util: 30.0.5
+ jest-validate: 30.1.0
+
+ jest-haste-map@30.1.0:
+ dependencies:
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ anymatch: 3.1.3
+ fb-watchman: 2.0.2
+ graceful-fs: 4.2.11
+ jest-regex-util: 30.0.1
+ jest-util: 30.0.5
+ jest-worker: 30.1.0
+ micromatch: 4.0.8
+ walker: 1.0.8
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ jest-leak-detector@30.1.0:
+ dependencies:
+ '@jest/get-type': 30.1.0
+ pretty-format: 30.0.5
+
+ jest-matcher-utils@30.1.2:
+ dependencies:
+ '@jest/get-type': 30.1.0
+ chalk: 4.1.2
+ jest-diff: 30.1.2
+ pretty-format: 30.0.5
+
+ jest-message-util@30.1.0:
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@jest/types': 30.0.5
+ '@types/stack-utils': 2.0.3
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ micromatch: 4.0.8
+ pretty-format: 30.0.5
+ slash: 3.0.0
+ stack-utils: 2.0.6
+
+ jest-mock@30.0.5:
+ dependencies:
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ jest-util: 30.0.5
+
+ jest-pnp-resolver@1.2.3(jest-resolve@30.1.0):
+ optionalDependencies:
+ jest-resolve: 30.1.0
+
+ jest-regex-util@30.0.1: {}
+
+ jest-resolve-dependencies@30.1.2:
+ dependencies:
+ jest-regex-util: 30.0.1
+ jest-snapshot: 30.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ jest-resolve@30.1.0:
+ dependencies:
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ jest-haste-map: 30.1.0
+ jest-pnp-resolver: 1.2.3(jest-resolve@30.1.0)
+ jest-util: 30.0.5
+ jest-validate: 30.1.0
+ slash: 3.0.0
+ unrs-resolver: 1.11.1
+
+ jest-runner@30.1.2:
+ dependencies:
+ '@jest/console': 30.1.2
+ '@jest/environment': 30.1.2
+ '@jest/test-result': 30.1.2
+ '@jest/transform': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ chalk: 4.1.2
+ emittery: 0.13.1
+ exit-x: 0.2.2
+ graceful-fs: 4.2.11
+ jest-docblock: 30.0.1
+ jest-environment-node: 30.1.2
+ jest-haste-map: 30.1.0
+ jest-leak-detector: 30.1.0
+ jest-message-util: 30.1.0
+ jest-resolve: 30.1.0
+ jest-runtime: 30.1.2
+ jest-util: 30.0.5
+ jest-watcher: 30.1.2
+ jest-worker: 30.1.0
+ p-limit: 3.1.0
+ source-map-support: 0.5.13
+ transitivePeerDependencies:
+ - supports-color
+
+ jest-runtime@30.1.2:
+ dependencies:
+ '@jest/environment': 30.1.2
+ '@jest/fake-timers': 30.1.2
+ '@jest/globals': 30.1.2
+ '@jest/source-map': 30.0.1
+ '@jest/test-result': 30.1.2
+ '@jest/transform': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ chalk: 4.1.2
+ cjs-module-lexer: 2.1.0
+ collect-v8-coverage: 1.0.2
+ glob: 10.4.5
+ graceful-fs: 4.2.11
+ jest-haste-map: 30.1.0
+ jest-message-util: 30.1.0
+ jest-mock: 30.0.5
+ jest-regex-util: 30.0.1
+ jest-resolve: 30.1.0
+ jest-snapshot: 30.1.2
+ jest-util: 30.0.5
+ slash: 3.0.0
+ strip-bom: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ jest-snapshot@30.1.2:
+ dependencies:
+ '@babel/core': 7.28.3
+ '@babel/generator': 7.28.3
+ '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3)
+ '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3)
+ '@babel/types': 7.28.2
+ '@jest/expect-utils': 30.1.2
+ '@jest/get-type': 30.1.0
+ '@jest/snapshot-utils': 30.1.2
+ '@jest/transform': 30.1.2
+ '@jest/types': 30.0.5
+ babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3)
+ chalk: 4.1.2
+ expect: 30.1.2
+ graceful-fs: 4.2.11
+ jest-diff: 30.1.2
+ jest-matcher-utils: 30.1.2
+ jest-message-util: 30.1.0
+ jest-util: 30.0.5
+ pretty-format: 30.0.5
+ semver: 7.7.2
+ synckit: 0.11.11
+ transitivePeerDependencies:
+ - supports-color
+
+ jest-util@30.0.5:
+ dependencies:
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ chalk: 4.1.2
+ ci-info: 4.3.0
+ graceful-fs: 4.2.11
+ picomatch: 4.0.2
+
+ jest-validate@30.1.0:
+ dependencies:
+ '@jest/get-type': 30.1.0
+ '@jest/types': 30.0.5
+ camelcase: 6.3.0
+ chalk: 4.1.2
+ leven: 3.1.0
+ pretty-format: 30.0.5
+
+ jest-watcher@30.1.2:
+ dependencies:
+ '@jest/test-result': 30.1.2
+ '@jest/types': 30.0.5
+ '@types/node': 20.17.19
+ ansi-escapes: 4.3.2
+ chalk: 4.1.2
+ emittery: 0.13.1
+ jest-util: 30.0.5
+ string-length: 4.0.2
+
+ jest-worker@30.1.0:
+ dependencies:
+ '@types/node': 20.17.19
+ '@ungap/structured-clone': 1.3.0
+ jest-util: 30.0.5
+ merge-stream: 2.0.0
+ supports-color: 8.1.1
+
+ jest@30.1.2(@types/node@20.17.19):
+ dependencies:
+ '@jest/core': 30.1.2
+ '@jest/types': 30.0.5
+ import-local: 3.2.0
+ jest-cli: 30.1.2(@types/node@20.17.19)
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - esbuild-register
+ - supports-color
+ - ts-node
+
jiti@2.4.2: {}
+ jose@6.1.0: {}
+
js-tokens@4.0.0: {}
+ js-yaml@3.14.1:
+ dependencies:
+ argparse: 1.0.10
+ esprima: 4.0.1
+
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
+ jsdom@26.1.0:
+ dependencies:
+ cssstyle: 4.6.0
+ data-urls: 5.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ is-potential-custom-element-name: 1.0.1
+ nwsapi: 2.2.21
+ parse5: 7.3.0
+ rrweb-cssom: 0.8.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 5.1.2
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+ ws: 8.18.3
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ jsesc@3.1.0: {}
+
json-buffer@3.0.1: {}
+ json-parse-even-better-errors@2.3.1: {}
+
json-schema-traverse@0.4.1: {}
json-stable-stringify-without-jsonify@1.0.1: {}
@@ -5501,6 +9518,8 @@ snapshots:
dependencies:
minimist: 1.2.8
+ json5@2.2.3: {}
+
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.8
@@ -5518,6 +9537,8 @@ snapshots:
dependencies:
language-subtag-registry: 0.3.23
+ leven@3.1.0: {}
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -5570,6 +9591,14 @@ snapshots:
lilconfig@3.1.3: {}
+ lines-and-columns@1.2.4: {}
+
+ linkify-it@5.0.0:
+ dependencies:
+ uc.micro: 2.1.0
+
+ linkifyjs@4.3.2: {}
+
lint-staged@15.4.3:
dependencies:
chalk: 5.4.1
@@ -5594,6 +9623,10 @@ snapshots:
rfdc: 1.4.1
wrap-ansi: 9.0.0
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -5616,12 +9649,39 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ lru-cache@10.4.3: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
lucide-react@0.476.0(react@19.0.0):
dependencies:
react: 19.0.0
+ lz-string@1.5.0: {}
+
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.7.1
+
+ makeerror@1.0.12:
+ dependencies:
+ tmpl: 1.0.5
+
+ markdown-it@14.1.0:
+ dependencies:
+ argparse: 2.0.1
+ entities: 4.5.0
+ linkify-it: 5.0.0
+ mdurl: 2.0.0
+ punycode.js: 2.3.1
+ uc.micro: 2.1.0
+
math-intrinsics@1.1.0: {}
+ mdurl@2.0.0: {}
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -5631,10 +9691,14 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
+ mimic-fn@2.1.0: {}
+
mimic-fn@4.0.0: {}
mimic-function@5.0.1: {}
+ min-indent@1.0.1: {}
+
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11
@@ -5645,6 +9709,8 @@ snapshots:
minimist@1.2.8: {}
+ minipass@7.1.2: {}
+
motion-dom@12.4.10:
dependencies:
motion-utils: 12.4.10
@@ -5661,16 +9727,54 @@ snapshots:
ms@2.1.3: {}
+ msgpackr-extract@3.0.3:
+ dependencies:
+ node-gyp-build-optional-packages: 5.2.2
+ optionalDependencies:
+ '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3
+ optional: true
+
+ msgpackr@1.11.5:
+ optionalDependencies:
+ msgpackr-extract: 3.0.3
+
+ multipasta@0.2.7: {}
+
nanoid@3.3.8: {}
+ napi-postinstall@0.3.3: {}
+
natural-compare@1.4.0: {}
+ negotiator@1.0.0: {}
+
+ next-auth@5.0.0-beta.29(next@15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@auth/core': 0.40.0
+ next: 15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react: 19.0.0
+
+ next-intl@4.3.5(next@15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.7.3):
+ dependencies:
+ '@formatjs/intl-localematcher': 0.5.10
+ negotiator: 1.0.0
+ next: 15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react: 19.0.0
+ use-intl: 4.3.5(react@19.0.0)
+ optionalDependencies:
+ typescript: 5.7.3
+
next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
- next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ next@15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@next/env': 15.2.4
'@swc/counter': 0.1.3
@@ -5680,7 +9784,7 @@ snapshots:
postcss: 8.4.31
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
- styled-jsx: 5.1.6(react@19.0.0)
+ styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.0.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.2.4
'@next/swc-darwin-x64': 15.2.4
@@ -5690,15 +9794,45 @@ snapshots:
'@next/swc-linux-x64-musl': 15.2.4
'@next/swc-win32-arm64-msvc': 15.2.4
'@next/swc-win32-x64-msvc': 15.2.4
+ '@playwright/test': 1.55.0
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
+ node-fetch-native@1.6.7: {}
+
+ node-gyp-build-optional-packages@5.2.2:
+ dependencies:
+ detect-libc: 2.0.3
+ optional: true
+
+ node-int64@0.4.0: {}
+
+ node-releases@2.0.19: {}
+
+ normalize-path@3.0.0: {}
+
+ npm-run-path@4.0.1:
+ dependencies:
+ path-key: 3.1.1
+
npm-run-path@5.3.0:
dependencies:
path-key: 4.0.0
+ nwsapi@2.2.21: {}
+
+ nypm@0.6.1:
+ dependencies:
+ citty: 0.1.6
+ consola: 3.4.2
+ pathe: 2.0.3
+ pkg-types: 2.3.0
+ tinyexec: 1.0.1
+
+ oauth4webapi@3.8.1: {}
+
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -5740,6 +9874,16 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ ohash@2.0.11: {}
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ onetime@5.1.2:
+ dependencies:
+ mimic-fn: 2.1.0
+
onetime@6.0.0:
dependencies:
mimic-fn: 4.0.0
@@ -5748,6 +9892,11 @@ snapshots:
dependencies:
mimic-function: 5.0.1
+ openai@5.16.0(ws@8.18.3)(zod@3.24.2):
+ optionalDependencies:
+ ws: 8.18.3
+ zod: 3.24.2
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -5757,32 +9906,68 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
+ orderedmap@2.1.1: {}
+
own-keys@1.0.1:
dependencies:
get-intrinsic: 1.3.0
object-keys: 1.1.1
safe-push-apply: 1.0.0
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
+ p-try@2.2.0: {}
+
+ package-json-from-dist@1.0.1: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ error-ex: 1.3.2
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
path-exists@4.0.0: {}
+ path-is-absolute@1.0.1: {}
+
path-key@3.1.1: {}
path-key@4.0.0: {}
path-parse@1.0.7: {}
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.2
+
+ pathe@2.0.3: {}
+
+ perfect-debounce@1.0.0: {}
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -5791,6 +9976,26 @@ snapshots:
pidtree@0.6.0: {}
+ pirates@4.0.7: {}
+
+ pkg-dir@4.2.0:
+ dependencies:
+ find-up: 4.1.0
+
+ pkg-types@2.3.0:
+ dependencies:
+ confbox: 0.2.2
+ exsolve: 1.0.7
+ pathe: 2.0.3
+
+ playwright-core@1.55.0: {}
+
+ playwright@1.55.0:
+ dependencies:
+ playwright-core: 1.55.0
+ optionalDependencies:
+ fsevents: 2.3.2
+
possible-typed-array-names@1.1.0: {}
postcss@8.4.31:
@@ -5805,6 +10010,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ preact-render-to-string@6.5.11(preact@10.24.3):
+ dependencies:
+ preact: 10.24.3
+
+ preact@10.24.3: {}
+
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
@@ -5813,16 +10024,151 @@ snapshots:
prettier@3.5.2: {}
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
+ pretty-format@30.0.5:
+ dependencies:
+ '@jest/schemas': 30.0.5
+ ansi-styles: 5.2.0
+ react-is: 18.3.1
+
+ prisma@6.15.0(typescript@5.7.3):
+ dependencies:
+ '@prisma/config': 6.15.0
+ '@prisma/engines': 6.15.0
+ optionalDependencies:
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - magicast
+
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
+ prosemirror-changeset@2.3.1:
+ dependencies:
+ prosemirror-transform: 1.10.4
+
+ prosemirror-collab@1.3.1:
+ dependencies:
+ prosemirror-state: 1.4.3
+
+ prosemirror-commands@1.7.1:
+ dependencies:
+ prosemirror-model: 1.25.3
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.4
+
+ prosemirror-dropcursor@1.8.2:
+ dependencies:
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.4
+ prosemirror-view: 1.40.1
+
+ prosemirror-gapcursor@1.3.2:
+ dependencies:
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.3
+ prosemirror-state: 1.4.3
+ prosemirror-view: 1.40.1
+
+ prosemirror-history@1.4.1:
+ dependencies:
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.4
+ prosemirror-view: 1.40.1
+ rope-sequence: 1.3.4
+
+ prosemirror-inputrules@1.5.0:
+ dependencies:
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.4
+
+ prosemirror-keymap@1.2.3:
+ dependencies:
+ prosemirror-state: 1.4.3
+ w3c-keyname: 2.2.8
+
+ prosemirror-markdown@1.13.2:
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+ prosemirror-model: 1.25.3
+
+ prosemirror-menu@1.2.5:
+ dependencies:
+ crelt: 1.0.6
+ prosemirror-commands: 1.7.1
+ prosemirror-history: 1.4.1
+ prosemirror-state: 1.4.3
+
+ prosemirror-model@1.25.3:
+ dependencies:
+ orderedmap: 2.1.1
+
+ prosemirror-schema-basic@1.2.4:
+ dependencies:
+ prosemirror-model: 1.25.3
+
+ prosemirror-schema-list@1.5.1:
+ dependencies:
+ prosemirror-model: 1.25.3
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.4
+
+ prosemirror-state@1.4.3:
+ dependencies:
+ prosemirror-model: 1.25.3
+ prosemirror-transform: 1.10.4
+ prosemirror-view: 1.40.1
+
+ prosemirror-tables@1.8.1:
+ dependencies:
+ prosemirror-keymap: 1.2.3
+ prosemirror-model: 1.25.3
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.4
+ prosemirror-view: 1.40.1
+
+ prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1):
+ dependencies:
+ '@remirror/core-constants': 3.0.0
+ escape-string-regexp: 4.0.0
+ prosemirror-model: 1.25.3
+ prosemirror-state: 1.4.3
+ prosemirror-view: 1.40.1
+
+ prosemirror-transform@1.10.4:
+ dependencies:
+ prosemirror-model: 1.25.3
+
+ prosemirror-view@1.40.1:
+ dependencies:
+ prosemirror-model: 1.25.3
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.4
+
+ punycode.js@2.3.1: {}
+
punycode@2.3.1: {}
+ pure-rand@6.1.0: {}
+
+ pure-rand@7.0.1: {}
+
queue-microtask@1.2.3: {}
+ rc9@2.1.2:
+ dependencies:
+ defu: 6.1.4
+ destr: 2.0.5
+
react-day-picker@8.10.1(date-fns@4.1.0)(react@19.0.0):
dependencies:
date-fns: 4.1.0
@@ -5859,6 +10205,8 @@ snapshots:
react-is@16.13.1: {}
+ react-is@17.0.2: {}
+
react-is@18.3.1: {}
react-remove-scroll-bar@2.3.8(@types/react@19.0.10)(react@19.0.0):
@@ -5923,6 +10271,8 @@ snapshots:
react@19.0.0: {}
+ readdirp@4.1.2: {}
+
recharts-scale@0.4.5:
dependencies:
decimal.js-light: 2.5.1
@@ -5940,6 +10290,11 @@ snapshots:
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
redux@4.2.1:
dependencies:
'@babel/runtime': 7.26.9
@@ -5966,8 +10321,16 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ require-directory@2.1.1: {}
+
+ resolve-cwd@3.0.0:
+ dependencies:
+ resolve-from: 5.0.0
+
resolve-from@4.0.0: {}
+ resolve-from@5.0.0: {}
+
resolve-pkg-maps@1.0.0: {}
resolve@1.22.10:
@@ -5991,6 +10354,10 @@ snapshots:
rfdc@1.4.1: {}
+ rope-sequence@1.3.4: {}
+
+ rrweb-cssom@0.8.0: {}
+
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -6014,12 +10381,20 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
+ safer-buffer@2.1.2: {}
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.25.0: {}
semver@6.3.1: {}
semver@7.7.1: {}
+ semver@7.7.2: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -6103,6 +10478,8 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
+ signal-exit@3.0.7: {}
+
signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
@@ -6110,6 +10487,8 @@ snapshots:
is-arrayish: 0.3.2
optional: true
+ slash@3.0.0: {}
+
slice-ansi@5.0.0:
dependencies:
ansi-styles: 6.2.1
@@ -6127,12 +10506,44 @@ snapshots:
source-map-js@1.2.1: {}
+ source-map-support@0.5.13:
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+
+ source-map@0.6.1: {}
+
+ sprintf-js@1.0.3: {}
+
+ sqids@0.3.0: {}
+
stable-hash@0.0.4: {}
+ stack-utils@2.0.6:
+ dependencies:
+ escape-string-regexp: 2.0.0
+
streamsearch@1.1.0: {}
string-argv@0.3.2: {}
+ string-length@4.0.2:
+ dependencies:
+ char-regex: 1.0.2
+ strip-ansi: 6.0.1
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+
string-width@7.2.0:
dependencies:
emoji-regex: 10.4.0
@@ -6189,27 +10600,51 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
strip-bom@3.0.0: {}
+ strip-bom@4.0.0: {}
+
+ strip-final-newline@2.0.0: {}
+
strip-final-newline@3.0.0: {}
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
+
strip-json-comments@3.1.1: {}
- styled-jsx@5.1.6(react@19.0.0):
+ styled-jsx@5.1.6(@babel/core@7.28.3)(react@19.0.0):
dependencies:
client-only: 0.0.1
react: 19.0.0
+ optionalDependencies:
+ '@babel/core': 7.28.3
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
+ supports-color@8.1.1:
+ dependencies:
+ has-flag: 4.0.0
+
supports-preserve-symlinks-flag@1.0.0: {}
+ symbol-tree@3.2.4: {}
+
+ synckit@0.11.11:
+ dependencies:
+ '@pkgr/core': 0.2.9
+
synckit@0.9.2:
dependencies:
'@pkgr/core': 0.1.1
@@ -6225,17 +10660,41 @@ snapshots:
tapable@2.2.1: {}
+ test-exclude@6.0.0:
+ dependencies:
+ '@istanbuljs/schema': 0.1.3
+ glob: 7.2.3
+ minimatch: 3.1.2
+
tiny-invariant@1.3.3: {}
+ tinyexec@1.0.1: {}
+
tinyglobby@0.2.12:
dependencies:
fdir: 6.4.3(picomatch@4.0.2)
picomatch: 4.0.2
+ tldts-core@6.1.86: {}
+
+ tldts@6.1.86:
+ dependencies:
+ tldts-core: 6.1.86
+
+ tmpl@1.0.5: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
+ tough-cookie@5.1.2:
+ dependencies:
+ tldts: 6.1.86
+
+ tr46@5.1.1:
+ dependencies:
+ punycode: 2.3.1
+
ts-api-utils@2.0.1(typescript@5.7.3):
dependencies:
typescript: 5.7.3
@@ -6249,10 +10708,21 @@ snapshots:
tslib@2.8.1: {}
+ tsx@4.20.5:
+ dependencies:
+ esbuild: 0.25.9
+ get-tsconfig: 4.10.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
+ type-detect@4.0.8: {}
+
+ type-fest@0.21.3: {}
+
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.3
@@ -6288,6 +10758,8 @@ snapshots:
typescript@5.7.3: {}
+ uc.micro@2.1.0: {}
+
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.3
@@ -6297,6 +10769,47 @@ snapshots:
undici-types@6.19.8: {}
+ unrs-resolver@1.11.1:
+ dependencies:
+ napi-postinstall: 0.3.3
+ optionalDependencies:
+ '@unrs/resolver-binding-android-arm-eabi': 1.11.1
+ '@unrs/resolver-binding-android-arm64': 1.11.1
+ '@unrs/resolver-binding-darwin-arm64': 1.11.1
+ '@unrs/resolver-binding-darwin-x64': 1.11.1
+ '@unrs/resolver-binding-freebsd-x64': 1.11.1
+ '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1
+ '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1
+ '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-arm64-musl': 1.11.1
+ '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1
+ '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-x64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-x64-musl': 1.11.1
+ '@unrs/resolver-binding-wasm32-wasi': 1.11.1
+ '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1
+ '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
+ '@unrs/resolver-binding-win32-x64-msvc': 1.11.1
+
+ update-browserslist-db@1.1.3(browserslist@4.25.4):
+ dependencies:
+ browserslist: 4.25.4
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uploadthing@7.7.4(next@15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(tailwindcss@4.0.8):
+ dependencies:
+ '@effect/platform': 0.90.3(effect@3.17.7)
+ '@standard-schema/spec': 1.0.0-beta.4
+ '@uploadthing/mime-types': 0.3.6
+ '@uploadthing/shared': 7.1.10
+ effect: 3.17.7
+ optionalDependencies:
+ next: 15.2.4(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ tailwindcss: 4.0.8
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -6308,6 +10821,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
+ use-intl@4.3.5(react@19.0.0):
+ dependencies:
+ '@formatjs/fast-memoize': 2.2.7
+ '@schummar/icu-type-parser': 1.21.5
+ intl-messageformat: 10.7.16
+ react: 19.0.0
+
use-sidecar@1.1.3(@types/react@19.0.10)(react@19.0.0):
dependencies:
detect-node-es: 1.1.0
@@ -6316,6 +10836,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
+ use-sync-external-store@1.5.0(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+
usehooks-ts@3.1.1(react@19.0.0):
dependencies:
lodash.debounce: 4.0.8
@@ -6323,6 +10847,12 @@ snapshots:
uuid@11.1.0: {}
+ v8-to-istanbul@9.3.0:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.30
+ '@types/istanbul-lib-coverage': 2.0.6
+ convert-source-map: 2.0.0
+
victory-vendor@36.9.2:
dependencies:
'@types/d3-array': 3.2.1
@@ -6340,6 +10870,29 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
+ w3c-keyname@2.2.8: {}
+
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
+ walker@1.0.8:
+ dependencies:
+ makeerror: 1.0.12
+
+ webidl-conversions@7.0.0: {}
+
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@4.0.0: {}
+
+ whatwg-url@14.2.0:
+ dependencies:
+ tr46: 5.1.1
+ webidl-conversions: 7.0.0
+
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -6386,19 +10939,61 @@ snapshots:
word-wrap@1.2.5: {}
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+
wrap-ansi@9.0.0:
dependencies:
ansi-styles: 6.2.1
string-width: 7.2.0
strip-ansi: 7.1.0
+ wrappy@1.0.2: {}
+
+ write-file-atomic@5.0.1:
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 4.1.0
+
+ ws@8.18.3: {}
+
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
yaml@2.7.0: {}
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
yocto-queue@0.1.0: {}
zod@3.24.2: {}
- zustand@5.0.3(@types/react@19.0.10)(react@19.0.0):
+ zustand@5.0.3(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)):
optionalDependencies:
'@types/react': 19.0.10
react: 19.0.0
+ use-sync-external-store: 1.5.0(react@19.0.0)
diff --git a/prisma/migrations/20250901064406_update_schema/migration.sql b/prisma/migrations/20250901064406_update_schema/migration.sql
new file mode 100644
index 00000000..0b1087af
--- /dev/null
+++ b/prisma/migrations/20250901064406_update_schema/migration.sql
@@ -0,0 +1,178 @@
+-- CreateEnum
+CREATE TYPE "public"."OrgRole" AS ENUM ('ADMIN', 'MEMBER');
+
+-- CreateTable
+CREATE TABLE "public"."User" (
+ "id" TEXT NOT NULL,
+ "name" TEXT,
+ "email" TEXT NOT NULL,
+ "emailVerified" TIMESTAMP(3),
+ "image" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."Account" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerAccountId" TEXT NOT NULL,
+ "refresh_token" TEXT,
+ "access_token" TEXT,
+ "expires_at" INTEGER,
+ "token_type" TEXT,
+ "scope" TEXT,
+ "id_token" TEXT,
+ "session_state" TEXT,
+ "oauth_token_secret" TEXT,
+ "oauth_token" TEXT,
+
+ CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."Session" (
+ "id" TEXT NOT NULL,
+ "sessionToken" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "expires" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."VerificationToken" (
+ "identifier" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expires" TIMESTAMP(3) NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "public"."Organization" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."Membership" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "organizationId" TEXT NOT NULL,
+ "role" "public"."OrgRole" NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Membership_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."Campaign" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "organizationId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Campaign_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."Content" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "body" TEXT,
+ "campaignId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Content_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."Asset" (
+ "id" TEXT NOT NULL,
+ "url" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "contentId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Asset_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."Schedule" (
+ "id" TEXT NOT NULL,
+ "date" TIMESTAMP(3) NOT NULL,
+ "status" TEXT NOT NULL,
+ "campaignId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Schedule_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."AnalyticsEvent" (
+ "id" TEXT NOT NULL,
+ "event" TEXT NOT NULL,
+ "data" JSONB,
+ "userId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "AnalyticsEvent_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "public"."Account"("provider", "providerAccountId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Session_sessionToken_key" ON "public"."Session"("sessionToken");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "VerificationToken_token_key" ON "public"."VerificationToken"("token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "public"."VerificationToken"("identifier", "token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Membership_userId_organizationId_key" ON "public"."Membership"("userId", "organizationId");
+
+-- AddForeignKey
+ALTER TABLE "public"."Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."Membership" ADD CONSTRAINT "Membership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."Membership" ADD CONSTRAINT "Membership_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."Campaign" ADD CONSTRAINT "Campaign_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."Content" ADD CONSTRAINT "Content_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "public"."Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."Asset" ADD CONSTRAINT "Asset_contentId_fkey" FOREIGN KEY ("contentId") REFERENCES "public"."Content"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."Schedule" ADD CONSTRAINT "Schedule_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "public"."Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."AnalyticsEvent" ADD CONSTRAINT "AnalyticsEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250902173201_add_campaign_fields/migration.sql b/prisma/migrations/20250902173201_add_campaign_fields/migration.sql
new file mode 100644
index 00000000..428865a4
--- /dev/null
+++ b/prisma/migrations/20250902173201_add_campaign_fields/migration.sql
@@ -0,0 +1,99 @@
+/*
+ Warnings:
+
+ - The values [MEMBER] on the enum `OrgRole` will be removed. If these variants are still used in the database, this will fail.
+ - You are about to drop the column `date` on the `Schedule` table. All the data in the column will be lost.
+ - The `status` column on the `Schedule` table would be dropped and recreated. This will lead to data loss if there is data in the column.
+ - Added the required column `channel` to the `Schedule` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `runAt` to the `Schedule` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- CreateEnum
+CREATE TYPE "public"."ContentStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'APPROVED', 'SCHEDULED', 'PUBLISHED', 'REJECTED');
+
+-- CreateEnum
+CREATE TYPE "public"."ScheduleStatus" AS ENUM ('PENDING', 'PUBLISHED', 'FAILED', 'CANCELLED');
+
+-- CreateEnum
+CREATE TYPE "public"."Channel" AS ENUM ('FACEBOOK', 'INSTAGRAM', 'TWITTER', 'YOUTUBE', 'LINKEDIN', 'TIKTOK', 'BLOG');
+
+-- CreateEnum
+CREATE TYPE "public"."CampaignHealth" AS ENUM ('ON_TRACK', 'AT_RISK', 'OFF_TRACK');
+
+-- CreateEnum
+CREATE TYPE "public"."CampaignStatus" AS ENUM ('DRAFT', 'PLANNING', 'READY', 'DONE', 'CANCELED');
+
+-- AlterEnum
+BEGIN;
+CREATE TYPE "public"."OrgRole_new" AS ENUM ('ADMIN', 'BRAND_OWNER', 'CREATOR');
+ALTER TABLE "public"."Membership" ALTER COLUMN "role" TYPE "public"."OrgRole_new" USING ("role"::text::"public"."OrgRole_new");
+ALTER TYPE "public"."OrgRole" RENAME TO "OrgRole_old";
+ALTER TYPE "public"."OrgRole_new" RENAME TO "OrgRole";
+DROP TYPE "public"."OrgRole_old";
+COMMIT;
+
+-- AlterTable
+ALTER TABLE "public"."AnalyticsEvent" ADD COLUMN "campaignId" TEXT,
+ADD COLUMN "contentId" TEXT,
+ADD COLUMN "organizationId" TEXT;
+
+-- AlterTable
+ALTER TABLE "public"."Asset" ADD COLUMN "description" TEXT,
+ADD COLUMN "name" TEXT,
+ADD COLUMN "size" INTEGER,
+ADD COLUMN "tags" TEXT[];
+
+-- AlterTable
+ALTER TABLE "public"."Campaign" ADD COLUMN "endDate" TIMESTAMP(3),
+ADD COLUMN "health" "public"."CampaignHealth" NOT NULL DEFAULT 'ON_TRACK',
+ADD COLUMN "startDate" TIMESTAMP(3),
+ADD COLUMN "status" "public"."CampaignStatus" NOT NULL DEFAULT 'DRAFT';
+
+-- AlterTable
+ALTER TABLE "public"."Content" ADD COLUMN "status" "public"."ContentStatus" NOT NULL DEFAULT 'DRAFT';
+
+-- AlterTable
+ALTER TABLE "public"."Schedule" DROP COLUMN "date",
+ADD COLUMN "channel" "public"."Channel" NOT NULL,
+ADD COLUMN "contentId" TEXT,
+ADD COLUMN "name" TEXT,
+ADD COLUMN "runAt" TIMESTAMP(3) NOT NULL,
+ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'UTC',
+DROP COLUMN "status",
+ADD COLUMN "status" "public"."ScheduleStatus" NOT NULL DEFAULT 'PENDING';
+
+-- AlterTable
+ALTER TABLE "public"."User" ADD COLUMN "password" TEXT;
+
+-- CreateTable
+CREATE TABLE "public"."CampaignMember" (
+ "id" TEXT NOT NULL,
+ "campaignId" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "role" TEXT NOT NULL DEFAULT 'MEMBER',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "CampaignMember_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CampaignMember_campaignId_userId_key" ON "public"."CampaignMember"("campaignId", "userId");
+
+-- AddForeignKey
+ALTER TABLE "public"."Schedule" ADD CONSTRAINT "Schedule_contentId_fkey" FOREIGN KEY ("contentId") REFERENCES "public"."Content"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."AnalyticsEvent" ADD CONSTRAINT "AnalyticsEvent_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "public"."Campaign"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."AnalyticsEvent" ADD CONSTRAINT "AnalyticsEvent_contentId_fkey" FOREIGN KEY ("contentId") REFERENCES "public"."Content"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."AnalyticsEvent" ADD CONSTRAINT "AnalyticsEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."CampaignMember" ADD CONSTRAINT "CampaignMember_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "public"."Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."CampaignMember" ADD CONSTRAINT "CampaignMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250902180000_complete_campaign_system/migration.sql b/prisma/migrations/20250902180000_complete_campaign_system/migration.sql
new file mode 100644
index 00000000..6c3bd6f7
--- /dev/null
+++ b/prisma/migrations/20250902180000_complete_campaign_system/migration.sql
@@ -0,0 +1,87 @@
+-- CreateEnum
+CREATE TYPE "public"."CampaignPriority" AS ENUM ('NO_PRIORITY', 'LOW', 'MEDIUM', 'HIGH', 'URGENT');
+
+-- CreateEnum
+CREATE TYPE "public"."CampaignMemberRole" AS ENUM ('OWNER', 'MANAGER', 'MEMBER', 'VIEWER');
+
+-- CreateEnum
+CREATE TYPE "public"."TaskStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'REVIEW', 'DONE', 'CANCELLED');
+
+-- CreateEnum
+CREATE TYPE "public"."TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
+
+-- AlterTable
+ALTER TABLE "public"."Campaign"
+ADD COLUMN "priority" "public"."CampaignPriority" NOT NULL DEFAULT 'NO_PRIORITY',
+ADD COLUMN "leadId" TEXT,
+ADD COLUMN "targetDate" TIMESTAMP(3),
+DROP COLUMN "endDate";
+
+-- AlterTable
+ALTER TABLE "public"."CampaignMember"
+ALTER COLUMN "role" TYPE "public"."CampaignMemberRole" USING ("role"::text::"public"."CampaignMemberRole");
+
+-- CreateTable
+CREATE TABLE "public"."CampaignTask" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "status" "public"."TaskStatus" NOT NULL DEFAULT 'TODO',
+ "priority" "public"."TaskPriority" NOT NULL DEFAULT 'MEDIUM',
+ "assigneeId" TEXT,
+ "campaignId" TEXT NOT NULL,
+ "parentTaskId" TEXT,
+ "dueDate" TIMESTAMP(3),
+ "completedAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "CampaignTask_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."CampaignLabel" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "color" TEXT NOT NULL DEFAULT '#3B82F6',
+ "campaignId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "CampaignLabel_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "public"."CampaignMilestone" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "dueDate" TIMESTAMP(3) NOT NULL,
+ "completedAt" TIMESTAMP(3),
+ "campaignId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "CampaignMilestone_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CampaignLabel_campaignId_name_key" ON "public"."CampaignLabel"("campaignId", "name");
+
+-- AddForeignKey
+ALTER TABLE "public"."Campaign" ADD CONSTRAINT "Campaign_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."CampaignTask" ADD CONSTRAINT "CampaignTask_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "public"."Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."CampaignTask" ADD CONSTRAINT "CampaignTask_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."CampaignTask" ADD CONSTRAINT "CampaignTask_parentTaskId_fkey" FOREIGN KEY ("parentTaskId") REFERENCES "public"."CampaignTask"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."CampaignLabel" ADD CONSTRAINT "CampaignLabel_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "public"."Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "public"."CampaignMilestone" ADD CONSTRAINT "CampaignMilestone_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "public"."Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 00000000..044d57cd
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "postgresql"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 00000000..ae378d15
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,304 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id String @id @default(cuid())
+ name String?
+ email String @unique
+ password String?
+ emailVerified DateTime?
+ image String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ accounts Account[]
+ analyticsEvents AnalyticsEvent[]
+ memberships Membership[]
+ sessions Session[]
+ campaignMembers CampaignMember[]
+ assignedTasks CampaignTask[] @relation("TaskAssignee")
+ leadCampaigns Campaign[] @relation("CampaignLead")
+}
+
+model Account {
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String?
+ access_token String?
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String?
+ session_state String?
+ oauth_token_secret String?
+ oauth_token String?
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([provider, providerAccountId])
+}
+
+model Session {
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model VerificationToken {
+ identifier String
+ token String @unique
+ expires DateTime
+
+ @@unique([identifier, token])
+}
+
+model Organization {
+ id String @id @default(cuid())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ analyticsEvents AnalyticsEvent[]
+ campaigns Campaign[]
+ memberships Membership[]
+}
+
+model Membership {
+ id String @id @default(cuid())
+ userId String
+ organizationId String
+ role OrgRole
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([userId, organizationId])
+}
+
+model Campaign {
+ id String @id @default(cuid())
+ name String
+ description String?
+ summary String?
+ organizationId String
+ health CampaignHealth @default(ON_TRACK)
+ status CampaignStatus @default(DRAFT)
+ priority CampaignPriority @default(NO_PRIORITY)
+ leadId String? // Changed from lead string to leadId reference
+ startDate DateTime? // Changed from startDate to match requirements
+ targetDate DateTime? // Changed from endDate to targetDate
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ analyticsEvents AnalyticsEvent[]
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ contents Content[]
+ schedules Schedule[]
+ members CampaignMember[]
+ tasks CampaignTask[] // New relation
+ labels CampaignLabel[] // New relation
+ milestones CampaignMilestone[] // New relation
+ lead User? @relation("CampaignLead", fields: [leadId], references: [id]) // New relation
+}
+
+model Content {
+ id String @id @default(cuid())
+ title String
+ body String?
+ campaignId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ status ContentStatus @default(DRAFT)
+ analyticsEvents AnalyticsEvent[]
+ assets Asset[]
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+ schedules Schedule[]
+}
+
+model Asset {
+ id String @id @default(cuid())
+ url String
+ name String?
+ type String
+ size Int?
+ description String?
+ tags String[]
+ contentId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
+}
+
+model Schedule {
+ id String @id @default(cuid())
+ name String?
+ campaignId String
+ contentId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ channel Channel
+ runAt DateTime
+ timezone String @default("UTC")
+ status ScheduleStatus @default(PENDING)
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+ content Content? @relation(fields: [contentId], references: [id])
+}
+
+model AnalyticsEvent {
+ id String @id @default(cuid())
+ event String
+ data Json?
+ userId String?
+ organizationId String?
+ campaignId String?
+ contentId String?
+ createdAt DateTime @default(now())
+ campaign Campaign? @relation(fields: [campaignId], references: [id])
+ content Content? @relation(fields: [contentId], references: [id])
+ organization Organization? @relation(fields: [organizationId], references: [id])
+ user User? @relation(fields: [userId], references: [id])
+}
+
+model CampaignMember {
+ id String @id @default(cuid())
+ campaignId String
+ userId String
+ role CampaignMemberRole @default(MEMBER) // Changed from String to enum
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([campaignId, userId])
+}
+
+// New models for complete campaign system
+model CampaignTask {
+ id String @id @default(cuid())
+ title String
+ description String?
+ status TaskStatus @default(TODO)
+ priority TaskPriority @default(MEDIUM)
+ assigneeId String?
+ campaignId String
+ parentTaskId String? // For subtasks
+ dueDate DateTime?
+ completedAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+ assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id])
+ parentTask CampaignTask? @relation("TaskSubtasks", fields: [parentTaskId], references: [id])
+ subtasks CampaignTask[] @relation("TaskSubtasks")
+}
+
+model CampaignLabel {
+ id String @id @default(cuid())
+ name String
+ color String @default("#3B82F6") // Default blue color
+ campaignId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+
+ @@unique([campaignId, name])
+}
+
+model CampaignMilestone {
+ id String @id @default(cuid())
+ title String
+ description String?
+ dueDate DateTime
+ completedAt DateTime?
+ campaignId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
+}
+
+enum OrgRole {
+ ADMIN
+ BRAND_OWNER
+ CREATOR
+}
+
+enum ContentStatus {
+ DRAFT
+ SUBMITTED
+ APPROVED
+ SCHEDULED
+ PUBLISHED
+ REJECTED
+}
+
+enum ScheduleStatus {
+ PENDING
+ PUBLISHED
+ FAILED
+ CANCELLED
+}
+
+enum Channel {
+ FACEBOOK
+ INSTAGRAM
+ TWITTER
+ YOUTUBE
+ LINKEDIN
+ TIKTOK
+ BLOG
+}
+
+enum CampaignHealth {
+ ON_TRACK
+ AT_RISK
+ OFF_TRACK
+}
+
+enum CampaignStatus {
+ DRAFT
+ PLANNING
+ READY
+ DONE
+ CANCELED
+}
+
+enum CampaignPriority {
+ NO_PRIORITY
+ LOW
+ MEDIUM
+ HIGH
+ URGENT
+}
+
+enum CampaignMemberRole {
+ OWNER
+ MANAGER
+ MEMBER
+ VIEWER
+}
+
+enum TaskStatus {
+ TODO
+ IN_PROGRESS
+ REVIEW
+ DONE
+ CANCELLED
+}
+
+enum TaskPriority {
+ LOW
+ MEDIUM
+ HIGH
+ URGENT
+}
diff --git a/spec.md b/spec.md
new file mode 100644
index 00000000..a79e7056
--- /dev/null
+++ b/spec.md
@@ -0,0 +1,688 @@
+Comprehensive Analysis & Migration Plan for the AiM Platform
+
+1. Tóm tắt điều hành (Executive Summary)
+
+Repository hiện tại (ioqwfoihas, tên Circle) là một ứng dụng Next.js lấy cảm hứng từ công cụ quản lý dự án Linear. Ứng dụng theo dõi issues, projects và teams với UI hiện đại, responsive. package.json cho thấy đây là client-only stack với các dependencies như Next.js 15, React 19, Tailwind CSS, Radix UI, Zustand, Recharts và Zod. Chưa có database hay API layer; dữ liệu đang nằm ở local state.
+
+AiM hướng tới chuyển hoá codebase này thành một AI-powered content platform phục vụ Creators và Brands. Hệ thống tương lai cần hỗ trợ role-based dashboards, campaign planning, AI-assisted content creation, scheduling, asset management, analytics và administrative controls. Báo cáo này phân tích repo, map năng lực hiện tại sang yêu cầu AiM, xác định gaps/risks, đề xuất target architecture và data model, đồng thời cung cấp roadmap migration với 150+ nhiệm vụ khả dụng ngay.
+
+2. Kiểm kê Repo & Tech Stack (Repo Inventory & Tech Stack)
+ Cây thư mục (ước lượng, depth ≤3)
+
+Ghi chú: do không có listing đầy đủ, một số đường dẫn được suy luận theo pattern Linear/Next.js phổ biến.
+
+ioqwfoihas/
+├─ README.md
+├─ package.json
+├─ tsconfig.json
+├─ next.config.ts
+├─ app/
+│ ├─ page.tsx
+│ ├─ layout.tsx
+│ ├─ lndev-ui/
+│ │ ├─ team/[team]/[view]/
+│ │ │ ├─ page.tsx
+│ │ │ └─ layout.tsx
+│ │ ├─ settings/…
+│ │ └─ …
+│ └─ (auth)/
+├─ components/
+│ ├─ ui/
+│ ├─ board/
+│ ├─ modal/
+│ ├─ chart/
+│ └─ navigation/
+├─ lib/
+│ ├─ store/
+│ ├─ hooks/
+│ └─ utils.ts
+├─ public/
+├─ styles/
+├─ .husky/
+└─ .eslintrc, .prettierrc
+
+Công nghệ & phiên bản (Tech & Versions)
+Khu vực Chi tiết
+Framework Next.js 15 (App Router, React Server Components).
+Ngôn ngữ TypeScript (strict).
+UI Library shadcn/ui (trên nền Radix UI).
+Styling Tailwind CSS v4 + tailwindcss-animate.
+State Mgmt Zustand cho client state.
+Forms & Validation React Hook Form + Zod.
+Charts Recharts.
+Utilities clsx, date-fns, uuid, …
+Testing Chưa có Jest/Vitest/Cypress.
+CI & Scripts Husky, lint-staged, ESLint, Prettier.
+Database/ORM Chưa có (no Prisma/ORM). 3) Đánh giá kiến trúc (Architecture Assessment)
+
+Single-app: cấu trúc Next.js thuần, không monorepo/workspaces.
+
+Client-only: không có API routes/server logic; dữ liệu nằm ở Zustand → không phù hợp cho AiM (cần persistence).
+
+UI/UX: responsive, dark/light, sidebars, modals; dùng shadcn/ui + Radix; có command palette (cmdk) và Recharts có thể tái sử dụng.
+
+Routing: App Router; root page redirect sang board; dynamic routes theo team/view.
+
+State: Zustand cho local data; để lên AiM cần React Query (data fetching/cache) + backend.
+
+Auth: Chưa có authentication/roles.
+
+Config/Env: Tối thiểu; chưa có .env.example hay secret management.
+
+Quality/DX: Có lint/format cơ bản; chưa có tests/CI đầy đủ.
+
+Security: Không có ranh giới bảo mật (no auth, no server validation).
+
+4. Mapping sang AiM (Reuse / Extend / Replace / Remove)
+ Module/Screen hiện có Mapping sang AiM Action Lý do
+ UI Shell (layout, navigation) Base dashboard cho mọi role Reuse & Extend Giữ responsive & theme; mở rộng navigation: Campaigns, Content Studio, Calendar, Assets, Analytics, Settings.
+ Team/Project Boards Campaign Kanban Extend Chuyển issue card → campaign card (Draft/Active/Completed), drag-drop persist.
+ List/Issue Views Content lists trong campaign Replace Thay issue list → content items; thêm trạng thái AI (Draft/AI-generated/Approved).
+ Zustand Stores (mock data) Server data qua React Query Replace Zustand chỉ giữ UI state; data persist qua Postgres/API.
+ Modals & Forms Wizards tạo Campaign/Content Reuse & Extend Reuse modal patterns + multi-step, zod validation, AI suggestions.
+ Charts (Recharts) Campaign/Content analytics Reuse & Extend Nối analytics API, hiển thị metrics (impressions/CTR/ROI).
+ Command palette AI assistant & global search Extend Palette nhận prompt AI, thực thi lệnh (“Generate LinkedIn post for X”).
+ Auth/RBAC (missing) Auth & role-based access Add Thêm NextAuth + Prisma, roles: Creator/Brand/Admin (+Reviewer/Publisher nếu cần).
+ API & ORM (missing) API endpoints/tRPC + Prisma Add CRUD cho campaigns/contents/schedules/assets/analytics.
+ Tests/CI (missing) Testing & CI pipelines Add Unit/integration/e2e; GitHub Actions: lint, test, build, health check.
+ Assets (missing) Asset storage Add Upload → S3/UploadThing; Asset Library UI/API.
+ Analytics (missing) Event tracking & dashboards Add Thu thập events (view/click/conv), tổng hợp hiển thị.
+ AI (missing) AI content generation Add Server route gọi AI provider; UI để generate/refine/translate.
+ i18n (missing) Đa ngôn ngữ Extend Thêm next-intl (English/Vietnamese).
+ Settings (missing) Settings (profile/org/billing/AI) Add Trang cấu hình user/org, API keys, preferences, …
+5. Gaps, Risks & Mitigations
+ Gaps
+
+Persistence: chưa có DB → Prisma + Postgres (schema cho Users/Orgs/Campaign/Content/Asset/Schedule/Analytics).
+
+Auth & RBAC: thiếu → NextAuth + role enum/middleware.
+
+API layer: thiếu CRUD → API routes hoặc tRPC + zod validation.
+
+Testing/CI: thiếu → thêm Jest/Vitest, Playwright, GitHub Actions.
+
+Security: chưa có validation, rate limit, secrets → thêm server validation, rate limiting, secret mgmt.
+
+Observability: thiếu logs/monitoring → pino/winston, Sentry.
+
+i18n: English-only → next-intl.
+
+Scalability: dễ thành monolith → module boundaries, cân nhắc packages sau.
+
+Risks & Mitigations
+Risk Tác động Xác suất Giảm thiểu
+Migration phức tạp Dễ phát sinh bug Cao Chia nhỏ PR, giữ trạng thái chạy được, test tự động.
+Dependencies chưa ổn định (Next 15) Compatibility TB Pin version, thử nghiệm trước khi merge.
+Security (auth/upload) Lỗ hổng TB Lib an toàn, OWASP, rate limit, file-type validation.
+Hiệu năng (board nhiều items) Lag TB Pagination/virtualization, lazy load, React Query cache.
+Adoption/training Chậm triển khai TB Tài liệu, code samples, workshop.
+Compliance (GDPR, …) Rủi ro pháp lý Thấp Privacy by design, retention policy, consent. 6) Kiến trúc mục tiêu (Target Architecture for AiM)
+flowchart TD
+subgraph Client
+U[Creators/Brands/Admins] -->|Browser| FE[Next.js 15 App]
+FE -->|Fetch| API
+FE -->|Auth Hooks| AuthClient
+end
+subgraph Server
+API --> DB[(PostgreSQL via Prisma)]
+API --> AIService[(External AI API)]
+API --> Storage[(Asset Storage: S3/UploadThing)]
+API --> Analytics[(Analytics Service)]
+API --> RBAC[(RBAC Middleware)]
+AuthService[(NextAuth.js)] --> DB
+RBAC --> AuthService
+end
+Analytics --> DB
+FE -.->|WebSockets| Notifications[(Real-time Notifications)]
+
+Mô tả ngắn:
+
+Frontend (Next.js): server/client components; Zustand cho UI state; React Query để fetch/cache.
+
+API layer (Next API routes/tRPC): endpoints cho campaign/content/asset/schedule/analytics/ai; zod + RBAC middleware.
+
+Auth: NextAuth.js, session lưu DB.
+
+DB: PostgreSQL qua Prisma (migrations).
+
+AI Service: proxy tới provider (OpenAI/…).
+
+Asset Storage: S3/UploadThing, metadata trong DB.
+
+Analytics: thu thập & tổng hợp events.
+
+Real-time: WebSockets/Pusher (tuỳ chọn).
+
+Data model đề xuất (Proposed Data Model)
+Entity Fields chính Quan hệ
+User id, name, email (unique), passwordHash, role (creator/brand/admin/reviewer/publisher), organizationId, timestamps Thuộc Organization; tạo Campaign/Content/Schedule/Asset/AnalyticsEvent
+Organization id, name, slug, timestamps Có nhiều Users/Campaigns/Assets
+Campaign id, organizationId, createdById, name, description, status (draft/active/completed), budget, startDate, endDate, timestamps Thuộc Organization/User; có Contents/Schedules/AnalyticsEvents
+Content id, campaignId, authorId, assetId?, title, body, type, aiGenerated, status, metadata (JSON), timestamps Thuộc Campaign; tham chiếu Asset; có Schedules/AnalyticsEvents
+Asset id, organizationId, uploadedById, fileName, fileType, size, url, timestamps Thuộc Organization; liên kết nhiều Contents
+Schedule id, contentId, campaignId, scheduledAt, platform, status, createdById Lịch đăng cho Content trong Campaign
+AnalyticsEvent id, contentId, userId?, eventType, timestamp, metadata Log tương tác, dùng để tổng hợp
+Role (tuỳ chọn) id, name, permissions (JSON) Many-to-many với User nếu cần RBAC chi tiết
+UI shell & điều hướng theo role (Role-Based Navigation)
+
+Creator: Dashboard nhanh vào Campaigns, Content Studio, Calendar, Assets, Analytics; có AI assistant; hiển thị deadlines/assignments.
+
+Brand: Tổng quan campaigns, approval queue, budget/ROI; duyệt nội dung, review assets, giám sát lịch.
+
+Admin: Quản trị organizations/users/roles/settings; cấu hình billing/AI quotas.
+
+7. Kế hoạch Delta theo file (Delta Plan – File-level)
+
+Bảng ADD/MODIFY/REMOVE chi tiết — xem đầy đủ trong bản gốc; giữ nguyên danh sách đường dẫn và mô tả như đã đề xuất (Prisma schema, API routes, pages, components, workflows CI, Docker, scripts…).
+(Bản dưới đây giữ nguyên nội dung, chỉ chuyển diễn giải sang tiếng Việt; tên file/path giữ English.)
+
+ADD: prisma/schema.prisma, prisma/migrations/_, lib/prisma.ts, app/api/auth/[...nextauth]/route.ts, app/api/auth/register/route.ts, app/api/campaigns/_, app/api/contents/_, app/api/assets/_, app/api/schedules/_, app/api/analytics/_, app/api/ai/generate/route.ts, app/middleware.ts, lib/rbac.ts, lib/hooks/useCurrentUser.ts, (auth)/login, (auth)/register, (dashboard)/creator, (dashboard)/brand, (dashboard)/admin, campaigns/_, content/_, calendar/page.tsx, assets/page.tsx, settings/page.tsx, các components forms/ai/navigation/dashboard…, .env.example, .github/workflows/\*.yml, Dockerfile, docker-compose.yml, scripts/publish-worker.ts…
+
+MODIFY: package.json, tsconfig.json, next.config.ts, app/layout.tsx, app/page.tsx, components/board/_, components/navigation/_, styles/globals.css…
+
+REMOVE: mock data trong lib/store, routes cũ dưới lndev-ui không dùng, assets placeholder.
+
+8. Lộ trình 30/60/90 ngày (Roadmap)
+
+0–30 ngày: Nền tảng (Prisma/Postgres, Auth + RBAC, API cơ bản, Dashboard shell, CI tối thiểu, Docs).
+
+31–60 ngày: Tính năng chính (Campaign Kanban + Wizard, Content Studio + AI, Calendar & Scheduling, Asset Library, Analytics cơ bản, i18n, Testing).
+
+61–90 ngày: Nâng cao & polishing (RBAC chi tiết, Realtime notifications, Observability, Performance, Collaboration, Deployment, Extensibility).
+
+(Chi tiết từng hạng mục đã được giữ nguyên và diễn giải tiếng Việt ở phần backlog.)
+
+9. Backlog – To-Do chi tiết (≥150 tasks)
+
+Giữ nguyên cấu trúc Epic → Story → Task như bản gốc. Nội dung dưới đây được dịch phần diễn giải, giữ thuật ngữ gốc trong ngoặc cho dễ đọc.
+Mỗi task có: Acceptance Criteria, Files touched, Effort (S/M/L), Priority (P0–P3).
+
+Epic: Authentication & RBAC
+Story: Cài đặt & cấu hình NextAuth.js
+
+Cài dependencies Auth… (như bản gốc; acceptance/files/effort/priority giữ nguyên)
+Chú thích: dùng NextAuth (credentials provider), bcrypt, JWT.
+
+Tạo route app/api/auth/[...nextauth]/route.ts (credentials) với callbacks (session/JWT)…
+
+Hash password (lib/auth.ts) bằng bcrypt…
+
+Endpoint register tạo User + Organization…
+
+Bọc …
+
+Trang login (zod validation, signIn())…
+
+Trang register (tạo org, role mặc định)…
+
+Logout (signOut())…
+
+/api/auth/session trả user hiện tại…
+
+Story: Quản lý vai trò (Role Management & RBAC)
+
+lib/roles.ts khai báo enum roles…
+
+Thêm field role vào User (Prisma enum)…
+
+lib/rbac.ts helpers hasRole/hasPermission + tests…
+
+middleware.ts bảo vệ route theo session/role…
+
+Gán role khi register (first user = admin)…
+
+Role switcher (dev only)…
+
+Role-based navigation (Sidebar)…
+
+Role-based API checks…
+
+Epic: Database & Prisma
+Story: Định nghĩa schema
+
+npx prisma init…
+
+Models User/Organization…
+
+Campaign…
+
+Content…
+
+Asset…
+
+Schedule…
+
+AnalyticsEvent…
+
+(Tuỳ chọn) Role/UserRole…
+
+Story: Migrations & Seeding
+
+prisma migrate dev --name init…
+
+scripts/seed.ts (roles, admin, org mẫu, campaigns, content)…
+
+pnpm seed…
+
+Document DB setup trong README…
+
+Epic: API Layer
+Story: Campaign Endpoints
+
+GET /campaigns (pagination/search, RBAC)…
+
+POST /campaigns (zod, createdBy, status draft)…
+
+GET /campaigns/[id] (details + relations)…
+
+PUT /campaigns/[id]…
+
+DELETE /campaigns/[id] (cascade theo rule)…
+
+GET /campaigns/[id]/analytics (summary)…
+
+POST /campaigns/[id]/duplicate…
+
+Story: Content Endpoints
+
+GET /contents (filter status/type)…
+
+POST /contents (create)…
+
+GET /contents/[id] (asset/author/schedules)…
+
+PUT /contents/[id] (restrictions by role/status)…
+
+DELETE /contents/[id]…
+
+POST /contents/[id]/submit (submit for approval)…
+
+POST /contents/[id]/approve|reject (Brand role)…
+
+POST /ai/generate (OpenAI; cache; audit)…
+
+POST /ai/translate…
+
+POST /ai/summarise…
+
+Story: Asset Endpoints
+
+POST /assets (UploadThing/S3; validate type/size; metadata)…
+
+GET /assets (org scope; pagination/search)…
+
+DELETE /assets/[id] (prevent delete if in-use)…
+
+GET /assets/upload-url (pre-signed S3)…
+
+Story: Schedule Endpoints
+
+GET /schedules (filters)…
+
+POST /schedules (validate future; content must be approved)…
+
+PUT /schedules/[id]…
+
+POST /schedules/[id]/cancel…
+
+scripts/publish-worker.ts (cron publish + analytics + notifications)…
+
+Story: Analytics Endpoints
+
+POST /analytics (record event)…
+
+GET /analytics (filters)…
+
+GET /analytics/summary (aggregate)…
+
+GET /analytics/export (CSV/Excel; admin/brand only)…
+
+Epic: Frontend – Global Layout & Navigation
+Story: Providers & Setup
+
+React Query provider (QueryClientProvider, Hydrate)…
+
+next-intl (en/vi) + …
+
+Theme provider (dark/light)…
+
+Story: Sidebar & Topbar
+
+Sidebar (role-specific menus)…
+
+Topbar (search, notifications, user menu)…
+
+Command palette (global search + AI commands)…
+
+Notifications panel (Radix UI + realtime)…
+
+Epic: Dashboards
+Story: Creator Dashboard
+
+Summary cards (active campaigns, drafts, scheduled, impressions)…
+
+Campaign list…
+
+Quick actions…
+
+Draft reminders…
+
+AI suggestions widget…
+
+Story: Brand Dashboard
+
+Brand metrics cards (budget/ROI)…
+
+Approval queue…
+
+Budget vs ROI chart…
+
+Creator leaderboard…
+
+Campaign health summary…
+
+Story: Admin Dashboard
+
+UserTable (enable/disable)…
+
+OrgTable (rename/delete cascade)…
+
+Role manager (nếu dùng Role model)…
+
+Audit logs…
+
+Feature flags…
+
+Epic: Campaign Module
+Story: Campaign List & Board
+
+Campaign table (sort/filter/search)…
+
+Pagination/infinite scroll…
+
+Board view (Draft/Active/Completed; drag-drop persist)…
+
+Campaign detail (Overview/Content/Schedule/Analytics/Settings)…
+
+Create wizard (multi-step)…
+
+Duplicate…
+
+Delete confirm…
+
+Search debounce…
+
+Permissions UI…
+
+Epic: Content Module
+Story: Content Editor
+
+Rich text editor (TipTap)…
+
+AI assistant panel (prompt, rewrite, tone)…
+
+Auto-save drafts…
+
+Asset picker…
+
+Preview…
+
+Translate & summarise…
+
+Status management (Draft/Submitted/Approved/Published/Rejected)…
+
+Comments & versioning…
+
+Story: Content List & Cards
+
+Content list (trong Campaign)…
+
+Content card (badge, quick actions)…
+
+Filtering & search…
+
+Bulk actions…
+
+Epic: Calendar & Scheduling
+Story: Calendar View
+
+Calendar page (react-day-picker) — color-code theo platform…
+
+Schedule form modal (platform/date/time/timezone)…
+
+Timezone support (user preference, store UTC)…
+
+Recurring schedules (RRULE model)…
+
+Epic: Assets Module
+Story: Asset Library UI
+
+Assets page (search/filter/thumbnail)…
+
+Upload dialog (multi files, progress)…
+
+Asset detail drawer (preview, metadata, usage)…
+
+Asset usage indicator…
+
+Epic: Analytics Module
+Story: Analytics Dashboards
+
+Overview chart (7/30/90)…
+
+Campaign analytics tab (line/bar/pie)…
+
+Content analytics tab…
+
+Creator performance…
+
+Export analytics UI…
+
+Epic: Settings Module
+Story: User & Organization Settings
+
+Profile settings (name/email/password; AI usage)…
+
+Organization settings (slug/logo/default language)…
+
+Notification preferences…
+
+Billing & quotas (Stripe)…
+
+API keys management…
+
+Epic: Internationalisation
+Story: i18n
+
+Install next-intl (en/vi)…
+
+Create translation files…
+
+Refactor components dùng t('key')…
+
+Language switcher…
+
+Epic: Testing & Quality
+Story: Unit & Integration Tests
+
+Jest/Vitest setup…
+
+Tests cho utilities (auth, rbac, utils)…
+
+Tests forms & components…
+
+Tests API routes…
+
+Story: End-to-End Tests
+
+Playwright setup…
+
+e2e login…
+
+e2e create campaign…
+
+e2e content workflow…
+
+e2e schedule & publish…
+
+Epic: CI/CD & Observability
+Story: Continuous Integration
+
+ci.yml (lint/build/test)…
+
+e2e.yml (Playwright artifacts)…
+
+Health check (/api/health)…
+
+Story: Delivery & Deployment
+
+Dockerfile (Node 20; prisma migrate deploy)…
+
+docker-compose.yml (app + postgres + mailhog)…
+
+deploy.yml (push image, deploy staging/prod)…
+
+Story: Observability & Monitoring
+
+Logging (pino/winston)…
+
+Error tracking (Sentry)…
+
+/api/health…
+
+/api/metrics (Prometheus prom-client)…
+
+Epic: AI Integration
+Story: AI Services
+
+Select AI provider (đánh giá cost/perf/language)…
+
+AI proxy endpoint (/api/ai/generate) — prompt engineering, rate limit, cache…
+
+Prompt library (lib/prompts.ts)…
+
+AI usage tracking (model AIUsage hoặc gắn vào Analytics)…
+
+AI tone tuning (formal/friendly/professional)…
+
+Content idea generator (/api/ai/ideas)…
+
+Epic: Performance & Scalability
+Story: Optimisations
+
+Bundle analysis (next build --profile, dynamic imports)…
+
+Image optimization ( , lazy)…
+
+Virtualization (lists >100 rows)…
+
+Caching & SWR (React Query strategies)…
+
+⚠️ Tổng số mục checklist vượt 150 (đủ chuẩn để tạo GitHub Issues ngay).
+
+10. CI/CD & Quality Gates
+
+Static Analysis (ESLint/Prettier, Husky pre-commit).
+
+Type Checking (tsc --noEmit trong CI).
+
+Unit & Integration Tests (≥80% cho core logic).
+
+E2E Tests (Playwright trên PR).
+
+Build & Health Check (ping /api/health).
+
+Code Review (branch protection + Conventional Commits).
+
+Secrets Management (GitHub Secrets, .env.example).
+
+CD Pipeline (deploy staging tự động; smoke test; promote to prod).
+
+11. .env & xử lý secrets (Secrets Handling)
+
+Tạo .env.example với các biến:
+
+DATABASE_URL="postgresql://aim_user:password@localhost:5432/aim_db"
+NEXTAUTH_SECRET="set-a-random-secret-here"
+NEXTAUTH_URL="http://localhost:3000"
+S3_BUCKET="aim-assets"
+S3_REGION="ap-southeast-1"
+S3_ACCESS_KEY_ID=""
+S3_SECRET_ACCESS_KEY=""
+AI_API_KEY=""
+PUBLIC_SITE_URL="http://localhost:3000"
+DEFAULT_LANGUAGE="en"
+
+# Optional
+
+SMTP_HOST=""
+SMTP_PORT=""
+SMTP_USER=""
+SMTP_PASS=""
+
+Dev sao chép .env.example → .env (được .gitignore).
+
+Prod dùng platform secrets (Vercel/AWS/GitHub Actions).
+
+Tránh log secrets; rotate định kỳ; least privilege.
+
+12. Câu hỏi mở (Open Questions)
+
+Stack Upgrade Tolerance: Giữ Next.js 15 hay cân nhắc Next.js 14 LTS để ổn định?
+
+Roles bổ sung: Có cần Reviewer/Publisher không?
+
+Compliance: Có yêu cầu GDPR/ISO cụ thể? (ảnh hưởng đến retention, consent, audit).
+
+13. Phụ lục (Appendix)
+ Scripts/Commands đề xuất
+
+pnpm prisma generate
+
+pnpm prisma migrate dev --name
+
+pnpm seed
+
+pnpm dev
+
+pnpm build
+
+pnpm test
+
+pnpm e2e
+
+node scripts/publish-worker.js
+
+Quy ước thư mục (Folder Conventions)
+
+app/ (App Router; groups theo role).
+
+components/ (UI tái sử dụng theo domain).
+
+lib/ (Prisma/RBAC/logger/metrics/prompts/hooks).
+
+prisma/ (schema + migrations).
+
+public/ (assets).
+
+scripts/ (seed/cron/test data).
+
+tests/ (unit/integration/e2e).
+
+Coding Guidelines
+
+TypeScript nghiêm ngặt, tránh any.
+
+Conventional Commits.
+
+ESLint + Prettier (Next preset).
+
+Ưu tiên Server Components; Client Components khi cần interactivity.
+
+React Query cho data fetching/cache.
+
+zod validate ở boundary (API/forms).
+
+A11y với Radix/ARIA.
+
+Viết tests cho components/APIs (ưu tiên critical paths).
diff --git a/store/campaign-store.ts b/store/campaign-store.ts
new file mode 100644
index 00000000..18867688
--- /dev/null
+++ b/store/campaign-store.ts
@@ -0,0 +1,648 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import type {
+ Campaign,
+ CampaignTask,
+ CampaignMember,
+ CampaignLabel,
+ CampaignMilestone,
+ CampaignFilters,
+ TaskFilters,
+ PaginationMeta,
+ CreateCampaignData,
+ UpdateCampaignData,
+ CreateTaskData,
+ UpdateTaskData,
+ CreateLabelData,
+ UpdateLabelData,
+ CreateMilestoneData,
+ UpdateMilestoneData,
+ AddMemberData,
+ UpdateMemberData,
+} from '@/types/campaign';
+
+// Types for the store
+interface CampaignState {
+ // Data
+ campaigns: Campaign[];
+ currentCampaign: Campaign | null;
+ tasks: CampaignTask[];
+ members: CampaignMember[];
+ labels: CampaignLabel[];
+ milestones: CampaignMilestone[];
+
+ // UI State
+ loading: boolean;
+ error: string | null;
+ filters: CampaignFilters;
+ taskFilters: TaskFilters;
+ pagination: PaginationMeta;
+ selectedCampaignId: string | null;
+
+ // View State
+ viewMode: 'table' | 'grid' | 'compact';
+ sidebarOpen: boolean;
+ detailPanelOpen: boolean;
+
+ // Search state
+ searchQuery: string;
+ searchResults: Campaign[];
+
+ // Actions
+ // Campaign CRUD
+ setCampaigns: (campaigns: Campaign[]) => void;
+ addCampaign: (campaign: Campaign) => void;
+ updateCampaign: (id: string, data: Partial) => void;
+ removeCampaign: (id: string) => void;
+ setCurrentCampaign: (campaign: Campaign | null) => void;
+
+ // Task management
+ setTasks: (tasks: CampaignTask[]) => void;
+ addTask: (task: CampaignTask) => void;
+ updateTask: (id: string, data: Partial) => void;
+ removeTask: (id: string) => void;
+ addSubtask: (parentId: string, subtask: CampaignTask) => void;
+
+ // Member management
+ setMembers: (members: CampaignMember[]) => void;
+ addMember: (member: CampaignMember) => void;
+ updateMember: (id: string, data: Partial) => void;
+ removeMember: (id: string) => void;
+
+ // Label management
+ setLabels: (labels: CampaignLabel[]) => void;
+ addLabel: (label: CampaignLabel) => void;
+ updateLabel: (id: string, data: Partial) => void;
+ removeLabel: (id: string) => void;
+
+ // Milestone management
+ setMilestones: (milestones: CampaignMilestone[]) => void;
+ addMilestone: (milestone: CampaignMilestone) => void;
+ updateMilestone: (id: string, data: Partial) => void;
+ removeMilestone: (id: string) => void;
+
+ // Filtering and search
+ setFilters: (filters: Partial) => void;
+ clearFilters: () => void;
+ setTaskFilters: (filters: Partial) => void;
+ clearTaskFilters: () => void;
+ setSearchQuery: (query: string) => void;
+ setSearchResults: (results: Campaign[]) => void;
+
+ // Pagination
+ setPagination: (pagination: PaginationMeta) => void;
+ setPage: (page: number) => void;
+
+ // UI state
+ setLoading: (loading: boolean) => void;
+ setError: (error: string | null) => void;
+ setViewMode: (mode: 'table' | 'grid' | 'compact') => void;
+ setSidebarOpen: (open: boolean) => void;
+ setDetailPanelOpen: (open: boolean) => void;
+ setSelectedCampaignId: (id: string | null) => void;
+
+ // Computed getters
+ getFilteredCampaigns: () => Campaign[];
+ getFilteredTasks: () => CampaignTask[];
+ getCampaignById: (id: string) => Campaign | undefined;
+ getTaskById: (id: string) => CampaignTask | undefined;
+ getCampaignTasks: (campaignId: string) => CampaignTask[];
+ getCampaignMembers: (campaignId: string) => CampaignMember[];
+
+ // Utility actions
+ reset: () => void;
+ resetCurrentCampaign: () => void;
+}
+
+const initialFilters: CampaignFilters = {};
+const initialTaskFilters: TaskFilters = {};
+const initialPagination: PaginationMeta = {
+ page: 1,
+ limit: 20,
+ total: 0,
+ totalPages: 0,
+ hasNextPage: false,
+ hasPrevPage: false,
+};
+
+export const useCampaignStore = create()(
+ devtools(
+ (set, get) => ({
+ // Initial state
+ campaigns: [],
+ currentCampaign: null,
+ tasks: [],
+ members: [],
+ labels: [],
+ milestones: [],
+
+ loading: false,
+ error: null,
+ filters: initialFilters,
+ taskFilters: initialTaskFilters,
+ pagination: initialPagination,
+ selectedCampaignId: null,
+
+ viewMode: 'table',
+ sidebarOpen: true,
+ detailPanelOpen: false,
+
+ searchQuery: '',
+ searchResults: [],
+
+ // Campaign CRUD actions
+ setCampaigns: (campaigns) => set({ campaigns }, false, 'setCampaigns'),
+
+ addCampaign: (campaign) =>
+ set(
+ (state) => ({
+ campaigns: [campaign, ...state.campaigns],
+ }),
+ false,
+ 'addCampaign'
+ ),
+
+ updateCampaign: (id, data) =>
+ set(
+ (state) => ({
+ campaigns: state.campaigns.map((campaign) =>
+ campaign.id === id ? { ...campaign, ...data } : campaign
+ ),
+ currentCampaign:
+ state.currentCampaign?.id === id
+ ? { ...state.currentCampaign, ...data }
+ : state.currentCampaign,
+ }),
+ false,
+ 'updateCampaign'
+ ),
+
+ removeCampaign: (id) =>
+ set(
+ (state) => ({
+ campaigns: state.campaigns.filter((campaign) => campaign.id !== id),
+ currentCampaign: state.currentCampaign?.id === id ? null : state.currentCampaign,
+ selectedCampaignId:
+ state.selectedCampaignId === id ? null : state.selectedCampaignId,
+ }),
+ false,
+ 'removeCampaign'
+ ),
+
+ setCurrentCampaign: (campaign) =>
+ set({ currentCampaign: campaign }, false, 'setCurrentCampaign'),
+
+ // Task management actions
+ setTasks: (tasks) => set({ tasks }, false, 'setTasks'),
+
+ addTask: (task) =>
+ set(
+ (state) => ({
+ tasks: [...state.tasks, task],
+ }),
+ false,
+ 'addTask'
+ ),
+
+ updateTask: (id, data) =>
+ set(
+ (state) => ({
+ tasks: state.tasks.map((task) => (task.id === id ? { ...task, ...data } : task)),
+ }),
+ false,
+ 'updateTask'
+ ),
+
+ removeTask: (id) =>
+ set(
+ (state) => ({
+ tasks: state.tasks.filter((task) => task.id !== id),
+ }),
+ false,
+ 'removeTask'
+ ),
+
+ addSubtask: (parentId, subtask) =>
+ set(
+ (state) => ({
+ tasks: state.tasks.map((task) =>
+ task.id === parentId
+ ? {
+ ...task,
+ subtasks: [...(task.subtasks || []), subtask],
+ _count: {
+ ...task._count,
+ subtasks: (task._count?.subtasks || 0) + 1,
+ },
+ }
+ : task
+ ),
+ }),
+ false,
+ 'addSubtask'
+ ),
+
+ // Member management actions
+ setMembers: (members) => set({ members }, false, 'setMembers'),
+
+ addMember: (member) =>
+ set(
+ (state) => ({
+ members: [...state.members, member],
+ }),
+ false,
+ 'addMember'
+ ),
+
+ updateMember: (id, data) =>
+ set(
+ (state) => ({
+ members: state.members.map((member) =>
+ member.id === id ? { ...member, ...data } : member
+ ),
+ }),
+ false,
+ 'updateMember'
+ ),
+
+ removeMember: (id) =>
+ set(
+ (state) => ({
+ members: state.members.filter((member) => member.id !== id),
+ }),
+ false,
+ 'removeMember'
+ ),
+
+ // Label management actions
+ setLabels: (labels) => set({ labels }, false, 'setLabels'),
+
+ addLabel: (label) =>
+ set(
+ (state) => ({
+ labels: [...state.labels, label],
+ }),
+ false,
+ 'addLabel'
+ ),
+
+ updateLabel: (id, data) =>
+ set(
+ (state) => ({
+ labels: state.labels.map((label) =>
+ label.id === id ? { ...label, ...data } : label
+ ),
+ }),
+ false,
+ 'updateLabel'
+ ),
+
+ removeLabel: (id) =>
+ set(
+ (state) => ({
+ labels: state.labels.filter((label) => label.id !== id),
+ }),
+ false,
+ 'removeLabel'
+ ),
+
+ // Milestone management actions
+ setMilestones: (milestones) => set({ milestones }, false, 'setMilestones'),
+
+ addMilestone: (milestone) =>
+ set(
+ (state) => ({
+ milestones: [...state.milestones, milestone],
+ }),
+ false,
+ 'addMilestone'
+ ),
+
+ updateMilestone: (id, data) =>
+ set(
+ (state) => ({
+ milestones: state.milestones.map((milestone) =>
+ milestone.id === id ? { ...milestone, ...data } : milestone
+ ),
+ }),
+ false,
+ 'updateMilestone'
+ ),
+
+ removeMilestone: (id) =>
+ set(
+ (state) => ({
+ milestones: state.milestones.filter((milestone) => milestone.id !== id),
+ }),
+ false,
+ 'removeMilestone'
+ ),
+
+ // Filtering and search actions
+ setFilters: (newFilters) =>
+ set(
+ (state) => ({
+ filters: { ...state.filters, ...newFilters },
+ }),
+ false,
+ 'setFilters'
+ ),
+
+ clearFilters: () => set({ filters: initialFilters }, false, 'clearFilters'),
+
+ setTaskFilters: (newFilters) =>
+ set(
+ (state) => ({
+ taskFilters: { ...state.taskFilters, ...newFilters },
+ }),
+ false,
+ 'setTaskFilters'
+ ),
+
+ clearTaskFilters: () =>
+ set({ taskFilters: initialTaskFilters }, false, 'clearTaskFilters'),
+
+ setSearchQuery: (query) => set({ searchQuery: query }, false, 'setSearchQuery'),
+
+ setSearchResults: (results) => set({ searchResults: results }, false, 'setSearchResults'),
+
+ // Pagination actions
+ setPagination: (pagination) => set({ pagination }, false, 'setPagination'),
+
+ setPage: (page) =>
+ set(
+ (state) => ({
+ pagination: { ...state.pagination, page },
+ }),
+ false,
+ 'setPage'
+ ),
+
+ // UI state actions
+ setLoading: (loading) => set({ loading }, false, 'setLoading'),
+
+ setError: (error) => set({ error }, false, 'setError'),
+
+ setViewMode: (mode) => set({ viewMode: mode }, false, 'setViewMode'),
+
+ setSidebarOpen: (open) => set({ sidebarOpen: open }, false, 'setSidebarOpen'),
+
+ setDetailPanelOpen: (open) => set({ detailPanelOpen: open }, false, 'setDetailPanelOpen'),
+
+ setSelectedCampaignId: (id) =>
+ set({ selectedCampaignId: id }, false, 'setSelectedCampaignId'),
+
+ // Computed getters
+ getFilteredCampaigns: () => {
+ const { campaigns, filters, searchQuery } = get();
+ let filtered = campaigns;
+
+ // Apply status filter
+ if (filters.status) {
+ filtered = filtered.filter((campaign) => campaign.status === filters.status);
+ }
+
+ // Apply health filter
+ if (filters.health) {
+ filtered = filtered.filter((campaign) => campaign.health === filters.health);
+ }
+
+ // Apply priority filter
+ if (filters.priority) {
+ filtered = filtered.filter((campaign) => campaign.priority === filters.priority);
+ }
+
+ // Apply lead filter
+ if (filters.leadId) {
+ filtered = filtered.filter((campaign) => campaign.leadId === filters.leadId);
+ }
+
+ // Apply member filter
+ if (filters.memberId) {
+ filtered = filtered.filter((campaign) =>
+ campaign.members?.some((member) => member.userId === filters.memberId)
+ );
+ }
+
+ // Apply search filter
+ const query = filters.search || searchQuery;
+ if (query) {
+ const lowerQuery = query.toLowerCase();
+ filtered = filtered.filter(
+ (campaign) =>
+ campaign.name.toLowerCase().includes(lowerQuery) ||
+ campaign.summary?.toLowerCase().includes(lowerQuery) ||
+ campaign.description?.toLowerCase().includes(lowerQuery)
+ );
+ }
+
+ // Apply date filters
+ if (filters.startDate) {
+ const startDate = new Date(filters.startDate);
+ filtered = filtered.filter(
+ (campaign) => campaign.startDate && new Date(campaign.startDate) >= startDate
+ );
+ }
+
+ if (filters.endDate) {
+ const endDate = new Date(filters.endDate);
+ filtered = filtered.filter(
+ (campaign) => campaign.targetDate && new Date(campaign.targetDate) <= endDate
+ );
+ }
+
+ return filtered;
+ },
+
+ getFilteredTasks: () => {
+ const { tasks, taskFilters } = get();
+ let filtered = tasks;
+
+ // Apply status filter
+ if (taskFilters.status) {
+ filtered = filtered.filter((task) => task.status === taskFilters.status);
+ }
+
+ // Apply priority filter
+ if (taskFilters.priority) {
+ filtered = filtered.filter((task) => task.priority === taskFilters.priority);
+ }
+
+ // Apply assignee filter
+ if (taskFilters.assigneeId) {
+ filtered = filtered.filter((task) => task.assigneeId === taskFilters.assigneeId);
+ }
+
+ // Apply parent task filter
+ if (taskFilters.parentTaskId) {
+ filtered = filtered.filter((task) => task.parentTaskId === taskFilters.parentTaskId);
+ }
+
+ // Apply search filter
+ if (taskFilters.search) {
+ const lowerQuery = taskFilters.search.toLowerCase();
+ filtered = filtered.filter(
+ (task) =>
+ task.title.toLowerCase().includes(lowerQuery) ||
+ task.description?.toLowerCase().includes(lowerQuery)
+ );
+ }
+
+ // Apply due date filter
+ if (taskFilters.dueDate) {
+ const dueDate = new Date(taskFilters.dueDate);
+ filtered = filtered.filter(
+ (task) => task.dueDate && new Date(task.dueDate) <= dueDate
+ );
+ }
+
+ return filtered;
+ },
+
+ getCampaignById: (id) => {
+ return get().campaigns.find((campaign) => campaign.id === id);
+ },
+
+ getTaskById: (id) => {
+ return get().tasks.find((task) => task.id === id);
+ },
+
+ getCampaignTasks: (campaignId) => {
+ return get().tasks.filter((task) => task.campaignId === campaignId);
+ },
+
+ getCampaignMembers: (campaignId) => {
+ return get().members.filter((member) => member.campaignId === campaignId);
+ },
+
+ // Utility actions
+ reset: () =>
+ set(
+ {
+ campaigns: [],
+ currentCampaign: null,
+ tasks: [],
+ members: [],
+ labels: [],
+ milestones: [],
+ loading: false,
+ error: null,
+ filters: initialFilters,
+ taskFilters: initialTaskFilters,
+ pagination: initialPagination,
+ selectedCampaignId: null,
+ viewMode: 'table',
+ sidebarOpen: true,
+ detailPanelOpen: false,
+ searchQuery: '',
+ searchResults: [],
+ },
+ false,
+ 'reset'
+ ),
+
+ resetCurrentCampaign: () =>
+ set(
+ {
+ currentCampaign: null,
+ tasks: [],
+ members: [],
+ labels: [],
+ milestones: [],
+ selectedCampaignId: null,
+ detailPanelOpen: false,
+ },
+ false,
+ 'resetCurrentCampaign'
+ ),
+ }),
+ {
+ name: 'campaign-store',
+ }
+ )
+);
+
+// Selector hooks for performance optimization
+export const useCampaigns = () => useCampaignStore((state) => state.campaigns);
+export const useCurrentCampaign = () => useCampaignStore((state) => state.currentCampaign);
+export const useCampaignTasks = () => useCampaignStore((state) => state.tasks);
+export const useCampaignMembers = () => useCampaignStore((state) => state.members);
+export const useCampaignLabels = () => useCampaignStore((state) => state.labels);
+export const useCampaignMilestones = () => useCampaignStore((state) => state.milestones);
+export const useCampaignLoading = () => useCampaignStore((state) => state.loading);
+export const useCampaignError = () => useCampaignStore((state) => state.error);
+export const useCampaignFilters = () => useCampaignStore((state) => state.filters);
+export const useCampaignPagination = () => useCampaignStore((state) => state.pagination);
+export const useCampaignViewMode = () => useCampaignStore((state) => state.viewMode);
+export const useSelectedCampaignId = () => useCampaignStore((state) => state.selectedCampaignId);
+
+// Actions selector hooks
+export const useCampaignActions = () =>
+ useCampaignStore((state) => ({
+ setCampaigns: state.setCampaigns,
+ addCampaign: state.addCampaign,
+ updateCampaign: state.updateCampaign,
+ removeCampaign: state.removeCampaign,
+ setCurrentCampaign: state.setCurrentCampaign,
+ setLoading: state.setLoading,
+ setError: state.setError,
+ setFilters: state.setFilters,
+ clearFilters: state.clearFilters,
+ setPagination: state.setPagination,
+ setPage: state.setPage,
+ setViewMode: state.setViewMode,
+ setSelectedCampaignId: state.setSelectedCampaignId,
+ reset: state.reset,
+ resetCurrentCampaign: state.resetCurrentCampaign,
+ }));
+
+export const useTaskActions = () =>
+ useCampaignStore((state) => ({
+ setTasks: state.setTasks,
+ addTask: state.addTask,
+ updateTask: state.updateTask,
+ removeTask: state.removeTask,
+ addSubtask: state.addSubtask,
+ setTaskFilters: state.setTaskFilters,
+ clearTaskFilters: state.clearTaskFilters,
+ }));
+
+export const useMemberActions = () =>
+ useCampaignStore((state) => ({
+ setMembers: state.setMembers,
+ addMember: state.addMember,
+ updateMember: state.updateMember,
+ removeMember: state.removeMember,
+ }));
+
+export const useLabelActions = () =>
+ useCampaignStore((state) => ({
+ setLabels: state.setLabels,
+ addLabel: state.addLabel,
+ updateLabel: state.updateLabel,
+ removeLabel: state.removeLabel,
+ }));
+
+export const useMilestoneActions = () =>
+ useCampaignStore((state) => ({
+ setMilestones: state.setMilestones,
+ addMilestone: state.addMilestone,
+ updateMilestone: state.updateMilestone,
+ removeMilestone: state.removeMilestone,
+ }));
+
+// Computed selector hooks
+export const useFilteredCampaigns = () => useCampaignStore((state) => state.getFilteredCampaigns());
+
+export const useFilteredTasks = () => useCampaignStore((state) => state.getFilteredTasks());
+
+export const useCampaignById = (id: string) =>
+ useCampaignStore((state) => state.getCampaignById(id));
+
+export const useTaskById = (id: string) => useCampaignStore((state) => state.getTaskById(id));
+
+export const useCampaignTasksById = (campaignId: string) =>
+ useCampaignStore((state) => state.getCampaignTasks(campaignId));
+
+export const useCampaignMembersById = (campaignId: string) =>
+ useCampaignStore((state) => state.getCampaignMembers(campaignId));
diff --git a/test-results/.last-run.json b/test-results/.last-run.json
new file mode 100644
index 00000000..028d6dbc
--- /dev/null
+++ b/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
diff --git a/tests/e2e/basic.spec.ts b/tests/e2e/basic.spec.ts
new file mode 100644
index 00000000..04deca25
--- /dev/null
+++ b/tests/e2e/basic.spec.ts
@@ -0,0 +1,21 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Basic functionality', () => {
+ test('should load the homepage', async ({ page }) => {
+ await page.goto('/');
+ await expect(page).toHaveTitle(/Circle/);
+ });
+
+ test('should have working navigation', async ({ page }) => {
+ await page.goto('/');
+ // Add more navigation tests based on your app structure
+ });
+
+ test('API health check', async ({ request }) => {
+ const response = await request.get('/api/health');
+ expect(response.ok()).toBeTruthy();
+
+ const data = await response.json();
+ expect(data).toHaveProperty('status');
+ });
+});
diff --git a/types/campaign.ts b/types/campaign.ts
new file mode 100644
index 00000000..9e87eb64
--- /dev/null
+++ b/types/campaign.ts
@@ -0,0 +1,394 @@
+import type {
+ Campaign as PrismaCampaign,
+ CampaignMember as PrismaCampaignMember,
+ CampaignTask as PrismaCampaignTask,
+ CampaignLabel as PrismaCampaignLabel,
+ CampaignMilestone as PrismaCampaignMilestone,
+ User,
+ Content,
+ Schedule,
+ CampaignStatus,
+ CampaignHealth,
+ CampaignPriority,
+ CampaignMemberRole,
+ TaskStatus,
+ TaskPriority,
+} from '@prisma/client';
+
+// Base interfaces
+export interface Campaign extends PrismaCampaign {
+ lead?: {
+ id: string;
+ name: string | null;
+ email: string;
+ image: string | null;
+ } | null;
+ members?: CampaignMember[];
+ tasks?: CampaignTask[];
+ labels?: CampaignLabel[];
+ milestones?: CampaignMilestone[];
+ contents?: Content[];
+ schedules?: Schedule[];
+ _count?: {
+ tasks: number;
+ members: number;
+ labels: number;
+ milestones: number;
+ contents: number;
+ schedules: number;
+ };
+}
+
+export interface CampaignMember extends PrismaCampaignMember {
+ user: {
+ id: string;
+ name: string | null;
+ email: string;
+ image: string | null;
+ };
+}
+
+export interface CampaignTask extends PrismaCampaignTask {
+ assignee?: {
+ id: string;
+ name: string | null;
+ email: string;
+ image: string | null;
+ } | null;
+ subtasks?: CampaignTask[];
+ parentTask?: CampaignTask | null;
+ _count?: {
+ subtasks: number;
+ };
+}
+
+export interface CampaignLabel extends PrismaCampaignLabel {}
+
+export interface CampaignMilestone extends PrismaCampaignMilestone {}
+
+// Form interfaces
+export interface CreateCampaignData {
+ name: string;
+ summary?: string;
+ description?: string;
+ status?: CampaignStatus;
+ health?: CampaignHealth;
+ priority?: CampaignPriority;
+ leadId?: string;
+ startDate?: string;
+ targetDate?: string;
+}
+
+export interface UpdateCampaignData {
+ name?: string;
+ summary?: string;
+ description?: string;
+ status?: CampaignStatus;
+ health?: CampaignHealth;
+ priority?: CampaignPriority;
+ leadId?: string;
+ startDate?: string;
+ targetDate?: string;
+}
+
+export interface CreateTaskData {
+ title: string;
+ description?: string;
+ status?: TaskStatus;
+ priority?: TaskPriority;
+ assigneeId?: string;
+ parentTaskId?: string;
+ dueDate?: string;
+ campaignId: string;
+}
+
+export interface UpdateTaskData {
+ title?: string;
+ description?: string;
+ status?: TaskStatus;
+ priority?: TaskPriority;
+ assigneeId?: string;
+ parentTaskId?: string;
+ dueDate?: string;
+ completedAt?: string;
+}
+
+export interface CreateLabelData {
+ name: string;
+ color?: string;
+}
+
+export interface UpdateLabelData {
+ name?: string;
+ color?: string;
+}
+
+export interface CreateMilestoneData {
+ title: string;
+ description?: string;
+ dueDate: string;
+}
+
+export interface UpdateMilestoneData {
+ title?: string;
+ description?: string;
+ dueDate?: string;
+ completedAt?: string;
+}
+
+export interface AddMemberData {
+ userId: string;
+ role?: CampaignMemberRole;
+}
+
+export interface UpdateMemberData {
+ role: CampaignMemberRole;
+}
+
+// Filter interfaces
+export interface CampaignFilters {
+ status?: CampaignStatus;
+ health?: CampaignHealth;
+ priority?: CampaignPriority;
+ search?: string;
+ leadId?: string;
+ memberId?: string;
+ startDate?: string;
+ endDate?: string;
+}
+
+export interface TaskFilters {
+ status?: TaskStatus;
+ priority?: TaskPriority;
+ assigneeId?: string;
+ search?: string;
+ parentTaskId?: string;
+ dueDate?: string;
+}
+
+// Pagination interfaces
+export interface PaginationParams {
+ page?: number;
+ limit?: number;
+}
+
+export interface PaginationMeta {
+ page: number;
+ limit: number;
+ total: number;
+ totalPages: number;
+ hasNextPage: boolean;
+ hasPrevPage: boolean;
+}
+
+export interface PaginatedResponse {
+ data: T[];
+ pagination: PaginationMeta;
+}
+
+// API response interfaces
+export interface CampaignsResponse extends PaginatedResponse {
+ campaigns: Campaign[];
+}
+
+export interface TasksResponse extends PaginatedResponse {
+ tasks: CampaignTask[];
+}
+
+// Component prop interfaces
+export interface CampaignListProps {
+ campaigns: Campaign[];
+ filters: CampaignFilters;
+ pagination: PaginationMeta;
+ onFilterChange: (filters: CampaignFilters) => void;
+ onPageChange: (page: number) => void;
+ onCreateCampaign: () => void;
+ onSelectCampaign: (campaign: Campaign) => void;
+ onEditCampaign: (campaign: Campaign) => void;
+ onDeleteCampaign: (campaign: Campaign) => void;
+}
+
+export interface CampaignCardProps {
+ campaign: Campaign;
+ onSelect: (campaign: Campaign) => void;
+ onEdit: (campaign: Campaign) => void;
+ onDelete: (campaign: Campaign) => void;
+ className?: string;
+}
+
+export interface CampaignTableProps {
+ campaigns: Campaign[];
+ onSelect: (campaign: Campaign) => void;
+ onEdit: (campaign: Campaign) => void;
+ onDelete: (campaign: Campaign) => void;
+ loading?: boolean;
+}
+
+export interface CampaignFormProps {
+ campaign?: Campaign;
+ onSubmit: (data: CreateCampaignData | UpdateCampaignData) => void;
+ onCancel: () => void;
+ teams: User[];
+ members: User[];
+ loading?: boolean;
+}
+
+export interface CampaignDetailProps {
+ campaign: Campaign;
+ onUpdate: (data: UpdateCampaignData) => void;
+ onDelete: () => void;
+ onAddMember: (data: AddMemberData) => void;
+ onRemoveMember: (memberId: string) => void;
+ onCreateTask: (data: CreateTaskData) => void;
+ onUpdateTask: (taskId: string, data: UpdateTaskData) => void;
+ onDeleteTask: (taskId: string) => void;
+ loading?: boolean;
+}
+
+export interface CampaignFiltersProps {
+ filters: CampaignFilters;
+ onChange: (filters: CampaignFilters) => void;
+ teams: User[];
+ members: User[];
+}
+
+export interface CampaignSearchProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export interface CampaignStatsProps {
+ stats: {
+ total: number;
+ draft: number;
+ planning: number;
+ ready: number;
+ done: number;
+ canceled: number;
+ onTrack: number;
+ atRisk: number;
+ offTrack: number;
+ };
+}
+
+// Task component interfaces
+export interface TaskListProps {
+ tasks: CampaignTask[];
+ onUpdateTask: (taskId: string, data: UpdateTaskData) => void;
+ onDeleteTask: (taskId: string) => void;
+ onCreateSubtask: (parentTaskId: string, data: CreateTaskData) => void;
+ loading?: boolean;
+}
+
+export interface TaskItemProps {
+ task: CampaignTask;
+ onUpdate: (data: UpdateTaskData) => void;
+ onDelete: () => void;
+ onCreateSubtask: (data: CreateTaskData) => void;
+ level?: number;
+}
+
+export interface TaskFormProps {
+ task?: CampaignTask;
+ parentTask?: CampaignTask;
+ onSubmit: (data: CreateTaskData | UpdateTaskData) => void;
+ onCancel: () => void;
+ members: User[];
+ loading?: boolean;
+}
+
+// Label component interfaces
+export interface LabelListProps {
+ labels: CampaignLabel[];
+ onUpdate: (labelId: string, data: UpdateLabelData) => void;
+ onDelete: (labelId: string) => void;
+ editable?: boolean;
+}
+
+export interface LabelItemProps {
+ label: CampaignLabel;
+ onUpdate?: (data: UpdateLabelData) => void;
+ onDelete?: () => void;
+ editable?: boolean;
+ size?: 'sm' | 'md' | 'lg';
+}
+
+export interface LabelFormProps {
+ label?: CampaignLabel;
+ onSubmit: (data: CreateLabelData | UpdateLabelData) => void;
+ onCancel: () => void;
+ loading?: boolean;
+}
+
+// Milestone component interfaces
+export interface MilestoneListProps {
+ milestones: CampaignMilestone[];
+ onUpdate: (milestoneId: string, data: UpdateMilestoneData) => void;
+ onDelete: (milestoneId: string) => void;
+ editable?: boolean;
+}
+
+export interface MilestoneItemProps {
+ milestone: CampaignMilestone;
+ onUpdate?: (data: UpdateMilestoneData) => void;
+ onDelete?: () => void;
+ editable?: boolean;
+}
+
+export interface MilestoneFormProps {
+ milestone?: CampaignMilestone;
+ onSubmit: (data: CreateMilestoneData | UpdateMilestoneData) => void;
+ onCancel: () => void;
+ loading?: boolean;
+}
+
+// Status and priority utilities
+export const CAMPAIGN_STATUSES: CampaignStatus[] = [
+ 'DRAFT',
+ 'PLANNING',
+ 'READY',
+ 'DONE',
+ 'CANCELED',
+];
+
+export const CAMPAIGN_HEALTH_OPTIONS: CampaignHealth[] = ['ON_TRACK', 'AT_RISK', 'OFF_TRACK'];
+
+export const CAMPAIGN_PRIORITIES: CampaignPriority[] = [
+ 'NO_PRIORITY',
+ 'LOW',
+ 'MEDIUM',
+ 'HIGH',
+ 'URGENT',
+];
+
+export const TASK_STATUSES: TaskStatus[] = ['TODO', 'IN_PROGRESS', 'REVIEW', 'DONE', 'CANCELLED'];
+
+export const TASK_PRIORITIES: TaskPriority[] = ['LOW', 'MEDIUM', 'HIGH', 'URGENT'];
+
+export const CAMPAIGN_MEMBER_ROLES: CampaignMemberRole[] = ['OWNER', 'MANAGER', 'MEMBER', 'VIEWER'];
+
+// Utility type guards
+export const isCampaignStatus = (value: string): value is CampaignStatus => {
+ return CAMPAIGN_STATUSES.includes(value as CampaignStatus);
+};
+
+export const isCampaignHealth = (value: string): value is CampaignHealth => {
+ return CAMPAIGN_HEALTH_OPTIONS.includes(value as CampaignHealth);
+};
+
+export const isCampaignPriority = (value: string): value is CampaignPriority => {
+ return CAMPAIGN_PRIORITIES.includes(value as CampaignPriority);
+};
+
+export const isTaskStatus = (value: string): value is TaskStatus => {
+ return TASK_STATUSES.includes(value as TaskStatus);
+};
+
+export const isTaskPriority = (value: string): value is TaskPriority => {
+ return TASK_PRIORITIES.includes(value as TaskPriority);
+};
+
+export const isCampaignMemberRole = (value: string): value is CampaignMemberRole => {
+ return CAMPAIGN_MEMBER_ROLES.includes(value as CampaignMemberRole);
+};
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 00000000..fda2993d
--- /dev/null
+++ b/types/index.ts
@@ -0,0 +1,5 @@
+// Campaign types
+export * from './campaign';
+
+// Re-export common Prisma types
+export type { User, Organization, Membership, OrgRole } from '@prisma/client';
diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts
new file mode 100644
index 00000000..d3074f70
--- /dev/null
+++ b/types/next-auth.d.ts
@@ -0,0 +1,12 @@
+import NextAuth from 'next-auth';
+
+declare module 'next-auth' {
+ interface Session {
+ user: {
+ id: string;
+ name?: string | null;
+ email?: string | null;
+ image?: string | null;
+ };
+ }
+}
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 00000000..1bd50926
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,14 @@
+{
+ "buildCommand": "pnpm run db:generate && pnpm run build",
+ "devCommand": "pnpm run dev",
+ "installCommand": "pnpm install",
+ "framework": "nextjs",
+ "functions": {
+ "app/api/**/*.ts": {
+ "maxDuration": 30
+ }
+ },
+ "env": {
+ "NODE_ENV": "production"
+ }
+}