CognxSafeTrack commited on
Commit ·
cc442ef
0
Parent(s):
Initial commit: Monorepo setup with React+Vite, Fastify, Prisma, and Docker Compose
Browse files- .gitignore +32 -0
- apps/admin/index.html +13 -0
- apps/admin/package.json +30 -0
- apps/admin/postcss.config.js +6 -0
- apps/admin/src/App.tsx +59 -0
- apps/admin/src/index.css +3 -0
- apps/admin/src/main.tsx +10 -0
- apps/admin/tailwind.config.js +11 -0
- apps/admin/tsconfig.json +19 -0
- apps/admin/tsconfig.node.json +12 -0
- apps/admin/vite.config.ts +13 -0
- apps/api/package.json +22 -0
- apps/api/src/index.ts +28 -0
- apps/api/src/routes/whatsapp.ts +46 -0
- apps/api/src/services/prisma.ts +3 -0
- apps/api/src/services/whatsapp.ts +44 -0
- apps/api/tsconfig.json +10 -0
- apps/web/index.html +16 -0
- apps/web/package.json +30 -0
- apps/web/postcss.config.js +6 -0
- apps/web/src/App.tsx +68 -0
- apps/web/src/index.css +3 -0
- apps/web/src/main.tsx +10 -0
- apps/web/tailwind.config.js +11 -0
- apps/web/tsconfig.json +19 -0
- apps/web/tsconfig.node.json +12 -0
- apps/web/vite.config.ts +13 -0
- apps/whatsapp-worker/package.json +21 -0
- apps/whatsapp-worker/src/index.ts +26 -0
- apps/whatsapp-worker/tsconfig.json +10 -0
- docker-compose.yml +23 -0
- package.json +19 -0
- packages/database/index.ts +1 -0
- packages/database/package.json +21 -0
- packages/database/prisma/schema.prisma +118 -0
- packages/database/seed.ts +54 -0
- packages/database/tsconfig.json +10 -0
- packages/shared-types/package.json +18 -0
- packages/shared-types/src/index.ts +31 -0
- packages/shared-types/tsconfig.json +10 -0
- packages/tsconfig/base.json +22 -0
- packages/tsconfig/package.json +10 -0
- packages/tsconfig/vite-react.json +23 -0
- pnpm-workspace.yaml +3 -0
.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules
|
| 3 |
+
.pnpm-store
|
| 4 |
+
|
| 5 |
+
# Next.js
|
| 6 |
+
.next
|
| 7 |
+
out
|
| 8 |
+
|
| 9 |
+
# Production
|
| 10 |
+
build
|
| 11 |
+
dist
|
| 12 |
+
|
| 13 |
+
# Environment variables
|
| 14 |
+
.env
|
| 15 |
+
.env*.local
|
| 16 |
+
|
| 17 |
+
# Logs
|
| 18 |
+
npm-debug.log*
|
| 19 |
+
yarn-debug.log*
|
| 20 |
+
pnpm-debug.log*
|
| 21 |
+
|
| 22 |
+
# IDE settings
|
| 23 |
+
.vscode
|
| 24 |
+
.idea
|
| 25 |
+
*.sublime-workspace
|
| 26 |
+
|
| 27 |
+
# OS files
|
| 28 |
+
.DS_Store
|
| 29 |
+
Thumbs.db
|
| 30 |
+
|
| 31 |
+
# Turbo
|
| 32 |
+
.turbo
|
apps/admin/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>EdTech Admin</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
apps/admin/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "admin",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite --port 5173",
|
| 8 |
+
"build": "tsc && vite build",
|
| 9 |
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^18.2.0",
|
| 14 |
+
"react-dom": "^18.2.0",
|
| 15 |
+
"react-router-dom": "^6.20.0",
|
| 16 |
+
"lucide-react": "^0.300.0",
|
| 17 |
+
"@repo/shared-types": "workspace:*"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/react": "^18.2.43",
|
| 21 |
+
"@types/react-dom": "^18.2.17",
|
| 22 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 23 |
+
"autoprefixer": "^10.4.16",
|
| 24 |
+
"postcss": "^8.4.32",
|
| 25 |
+
"tailwindcss": "^3.4.0",
|
| 26 |
+
"typescript": "^5.2.2",
|
| 27 |
+
"vite": "^5.0.8",
|
| 28 |
+
"@repo/tsconfig": "workspace:*"
|
| 29 |
+
}
|
| 30 |
+
}
|
apps/admin/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
apps/admin/src/App.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
| 2 |
+
|
| 3 |
+
function Dashboard() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="p-8">
|
| 6 |
+
<h1 className="text-3xl font-bold mb-4">Admin Dashboard</h1>
|
| 7 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 8 |
+
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
| 9 |
+
<h2 className="text-xl font-semibold mb-2">Users</h2>
|
| 10 |
+
<p className="text-gray-600">Manage student enrollments</p>
|
| 11 |
+
</div>
|
| 12 |
+
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
| 13 |
+
<h2 className="text-xl font-semibold mb-2">Tracks</h2>
|
| 14 |
+
<p className="text-gray-600">Edit learning content</p>
|
| 15 |
+
</div>
|
| 16 |
+
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
| 17 |
+
<h2 className="text-xl font-semibold mb-2">Analytics</h2>
|
| 18 |
+
<p className="text-gray-600">View platform metrics</p>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function Settings() {
|
| 26 |
+
return (
|
| 27 |
+
<div className="p-8">
|
| 28 |
+
<h1 className="text-3xl font-bold mb-4">Settings</h1>
|
| 29 |
+
<p>System configuration</p>
|
| 30 |
+
</div>
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function App() {
|
| 35 |
+
return (
|
| 36 |
+
<Router>
|
| 37 |
+
<div className="min-h-screen bg-gray-50 flex">
|
| 38 |
+
{/* Sidebar */}
|
| 39 |
+
<aside className="w-64 bg-slate-900 text-white p-6">
|
| 40 |
+
<div className="text-xl font-bold mb-8">EdTech Admin</div>
|
| 41 |
+
<nav className="space-y-4">
|
| 42 |
+
<Link to="/" className="block hover:text-gray-300">Dashboard</Link>
|
| 43 |
+
<Link to="/settings" className="block hover:text-gray-300">Settings</Link>
|
| 44 |
+
</nav>
|
| 45 |
+
</aside>
|
| 46 |
+
|
| 47 |
+
{/* Main Content */}
|
| 48 |
+
<main className="flex-1">
|
| 49 |
+
<Routes>
|
| 50 |
+
<Route path="/" element={<Dashboard />} />
|
| 51 |
+
<Route path="/settings" element={<Settings />} />
|
| 52 |
+
</Routes>
|
| 53 |
+
</main>
|
| 54 |
+
</div>
|
| 55 |
+
</Router>
|
| 56 |
+
)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export default App
|
apps/admin/src/index.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
apps/admin/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App.tsx'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
apps/admin/tailwind.config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {},
|
| 9 |
+
},
|
| 10 |
+
plugins: [],
|
| 11 |
+
}
|
apps/admin/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "@repo/tsconfig/vite-react.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"baseUrl": ".",
|
| 5 |
+
"paths": {
|
| 6 |
+
"@/*": [
|
| 7 |
+
"./src/*"
|
| 8 |
+
]
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"include": [
|
| 12 |
+
"src"
|
| 13 |
+
],
|
| 14 |
+
"references": [
|
| 15 |
+
{
|
| 16 |
+
"path": "./tsconfig.node.json"
|
| 17 |
+
}
|
| 18 |
+
]
|
| 19 |
+
}
|
apps/admin/tsconfig.node.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"skipLibCheck": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "bundler",
|
| 7 |
+
"allowSyntheticDefaultImports": true
|
| 8 |
+
},
|
| 9 |
+
"include": [
|
| 10 |
+
"vite.config.ts"
|
| 11 |
+
]
|
| 12 |
+
}
|
apps/admin/vite.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import path from 'path'
|
| 4 |
+
|
| 5 |
+
// https://vitejs.dev/config/
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [react()],
|
| 8 |
+
resolve: {
|
| 9 |
+
alias: {
|
| 10 |
+
"@": path.resolve(__dirname, "./src"),
|
| 11 |
+
},
|
| 12 |
+
},
|
| 13 |
+
})
|
apps/api/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "api",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "tsx watch src/index.ts",
|
| 7 |
+
"build": "tsc",
|
| 8 |
+
"start": "node dist/index.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"fastify": "^4.0.0",
|
| 12 |
+
"@fastify/cors": "^8.0.0",
|
| 13 |
+
"zod": "^3.0.0",
|
| 14 |
+
"@repo/database": "workspace:*"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"tsx": "^3.0.0",
|
| 18 |
+
"typescript": "^5.0.0",
|
| 19 |
+
"@types/node": "^20.0.0",
|
| 20 |
+
"@repo/tsconfig": "workspace:*"
|
| 21 |
+
}
|
| 22 |
+
}
|
apps/api/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Fastify from 'fastify';
|
| 2 |
+
import cors from '@fastify/cors';
|
| 3 |
+
|
| 4 |
+
const server = Fastify({
|
| 5 |
+
logger: true
|
| 6 |
+
});
|
| 7 |
+
|
| 8 |
+
server.register(cors);
|
| 9 |
+
|
| 10 |
+
// Register Routes
|
| 11 |
+
import { whatsappRoutes } from './routes/whatsapp';
|
| 12 |
+
server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
|
| 13 |
+
|
| 14 |
+
server.get('/health', async (request, reply) => {
|
| 15 |
+
return { status: 'ok', timestamp: new Date().toISOString() };
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
const start = async () => {
|
| 19 |
+
try {
|
| 20 |
+
await server.listen({ port: 3001, host: '0.0.0.0' });
|
| 21 |
+
console.log('Server listening on http://localhost:3001');
|
| 22 |
+
} catch (err) {
|
| 23 |
+
server.log.error(err);
|
| 24 |
+
process.exit(1);
|
| 25 |
+
}
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
start();
|
apps/api/src/routes/whatsapp.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FastifyInstance } from 'fastify';
|
| 2 |
+
import { WhatsAppService } from '../services/whatsapp';
|
| 3 |
+
import { WebhookPayloadSchema } from '@repo/shared-types';
|
| 4 |
+
|
| 5 |
+
export async function whatsappRoutes(fastify: FastifyInstance) {
|
| 6 |
+
// Verification for Meta (Cloud API)
|
| 7 |
+
fastify.get('/webhook', async (request, reply) => {
|
| 8 |
+
const query = request.query as any;
|
| 9 |
+
const mode = query['hub.mode'];
|
| 10 |
+
const token = query['hub.verify_token'];
|
| 11 |
+
const challenge = query['hub.challenge'];
|
| 12 |
+
|
| 13 |
+
if (mode === 'subscribe' && token === process.env.WHATSAPP_VERIFY_TOKEN) {
|
| 14 |
+
return reply.code(200).send(challenge);
|
| 15 |
+
}
|
| 16 |
+
return reply.code(403).send();
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
// Incoming Messages
|
| 20 |
+
fastify.post('/webhook', async (request, reply) => {
|
| 21 |
+
try {
|
| 22 |
+
// Basic runtime validation (can use Zod schema here strictly if needed)
|
| 23 |
+
const body = request.body as any;
|
| 24 |
+
console.log('Webhook body:', JSON.stringify(body, null, 2));
|
| 25 |
+
|
| 26 |
+
const entry = body.entry?.[0];
|
| 27 |
+
const changes = entry?.changes?.[0];
|
| 28 |
+
const value = changes?.value;
|
| 29 |
+
const message = value?.messages?.[0];
|
| 30 |
+
|
| 31 |
+
if (message && message.type === 'text') {
|
| 32 |
+
const phone = message.from; // WhatsApp ID
|
| 33 |
+
const text = message.text?.body;
|
| 34 |
+
|
| 35 |
+
if (phone && text) {
|
| 36 |
+
await WhatsAppService.handleIncomingMessage(phone, text);
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return reply.code(200).send('OK');
|
| 41 |
+
} catch (error) {
|
| 42 |
+
console.error('Webhook Error:', error);
|
| 43 |
+
return reply.code(500).send(error);
|
| 44 |
+
}
|
| 45 |
+
});
|
| 46 |
+
}
|
apps/api/src/services/prisma.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PrismaClient } from '@repo/database';
|
| 2 |
+
|
| 3 |
+
export const prisma = new PrismaClient();
|
apps/api/src/services/whatsapp.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { prisma } from './prisma';
|
| 2 |
+
|
| 3 |
+
export class WhatsAppService {
|
| 4 |
+
static async handleIncomingMessage(phone: string, text: string) {
|
| 5 |
+
const normalizedText = text.trim().toUpperCase();
|
| 6 |
+
console.log(`Received message from ${phone}: ${normalizedText}`);
|
| 7 |
+
|
| 8 |
+
// 1. Find or Create User
|
| 9 |
+
let user = await prisma.user.findUnique({ where: { phone } });
|
| 10 |
+
|
| 11 |
+
if (!user) {
|
| 12 |
+
if (normalizedText === 'INSCRIPTION') {
|
| 13 |
+
user = await prisma.user.create({
|
| 14 |
+
data: { phone }
|
| 15 |
+
});
|
| 16 |
+
// TODO: Send welcome message & ask for name
|
| 17 |
+
console.log('New user created, starting onboarding...');
|
| 18 |
+
return;
|
| 19 |
+
} else {
|
| 20 |
+
// TODO: Send "Type INSCRIPTION to start"
|
| 21 |
+
console.log('User not found, waiting for INSCRIPTION');
|
| 22 |
+
return;
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// 2. Check Active Enrollment
|
| 27 |
+
const activeEnrollment = await prisma.enrollment.findFirst({
|
| 28 |
+
where: { userId: user.id, status: 'ACTIVE' },
|
| 29 |
+
include: { track: true }
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
if (activeEnrollment) {
|
| 33 |
+
// TODO: Handle daily response
|
| 34 |
+
console.log(`User ${user.id} has active enrollment in ${activeEnrollment.track.title}`);
|
| 35 |
+
return;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// 3. Handle Commands
|
| 39 |
+
if (normalizedText === 'INSCRIPTION') {
|
| 40 |
+
console.log('User already exists, checking available tracks...');
|
| 41 |
+
// TODO: List available tracks
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
}
|
apps/api/tsconfig.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "@repo/tsconfig/base.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"outDir": "dist",
|
| 5 |
+
"rootDir": "."
|
| 6 |
+
},
|
| 7 |
+
"include": [
|
| 8 |
+
"src/**/*"
|
| 9 |
+
]
|
| 10 |
+
}
|
apps/web/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 8 |
+
<title>EdTech Learning Portal</title>
|
| 9 |
+
</head>
|
| 10 |
+
|
| 11 |
+
<body>
|
| 12 |
+
<div id="root"></div>
|
| 13 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 14 |
+
</body>
|
| 15 |
+
|
| 16 |
+
</html>
|
apps/web/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "web",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite --port 5174",
|
| 8 |
+
"build": "tsc && vite build",
|
| 9 |
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^18.2.0",
|
| 14 |
+
"react-dom": "^18.2.0",
|
| 15 |
+
"react-router-dom": "^6.20.0",
|
| 16 |
+
"lucide-react": "^0.300.0",
|
| 17 |
+
"@repo/shared-types": "workspace:*"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/react": "^18.2.43",
|
| 21 |
+
"@types/react-dom": "^18.2.17",
|
| 22 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 23 |
+
"autoprefixer": "^10.4.16",
|
| 24 |
+
"postcss": "^8.4.32",
|
| 25 |
+
"tailwindcss": "^3.4.0",
|
| 26 |
+
"typescript": "^5.2.2",
|
| 27 |
+
"vite": "^5.0.8",
|
| 28 |
+
"@repo/tsconfig": "workspace:*"
|
| 29 |
+
}
|
| 30 |
+
}
|
apps/web/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
apps/web/src/App.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
| 2 |
+
|
| 3 |
+
function Home() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="max-w-4xl mx-auto p-8">
|
| 6 |
+
<header className="text-center mb-12">
|
| 7 |
+
<h1 className="text-4xl font-bold text-slate-900 mb-4">EdTech Learning Portal</h1>
|
| 8 |
+
<p className="text-xl text-gray-600">Master business skills via WhatsApp</p>
|
| 9 |
+
</header>
|
| 10 |
+
|
| 11 |
+
<div className="grid md:grid-cols-2 gap-8">
|
| 12 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border hover:shadow-md transition-shadow">
|
| 13 |
+
<h2 className="text-2xl font-semibold mb-4 text-emerald-700">Start Learning</h2>
|
| 14 |
+
<p className="text-gray-600 mb-6">Join our guided learning tracks directly on WhatsApp.</p>
|
| 15 |
+
<button className="bg-emerald-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-emerald-700 w-full">
|
| 16 |
+
Join Now
|
| 17 |
+
</button>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div className="bg-white p-8 rounded-xl shadow-sm border hover:shadow-md transition-shadow">
|
| 21 |
+
<h2 className="text-2xl font-semibold mb-4 text-blue-900">Student Login</h2>
|
| 22 |
+
<p className="text-gray-600 mb-6">Access your progress and download certificates.</p>
|
| 23 |
+
<Link to="/login" className="block text-center bg-blue-900 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-800 w-full">
|
| 24 |
+
Log In
|
| 25 |
+
</Link>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function Login() {
|
| 33 |
+
return (
|
| 34 |
+
<div className="flex items-center justify-center min-h-[60vh]">
|
| 35 |
+
<div className="bg-white p-8 rounded-xl shadow-md border w-full max-w-md">
|
| 36 |
+
<h1 className="text-2xl font-bold mb-6 text-center">Student Login</h1>
|
| 37 |
+
<form className="space-y-4">
|
| 38 |
+
<div>
|
| 39 |
+
<label className="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
|
| 40 |
+
<input type="tel" className="w-full border rounded-lg p-2" placeholder="+221 ..." />
|
| 41 |
+
</div>
|
| 42 |
+
<button type="submit" className="w-full bg-blue-900 text-white py-2 rounded-lg font-medium">
|
| 43 |
+
Send Magic Link
|
| 44 |
+
</button>
|
| 45 |
+
</form>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function App() {
|
| 52 |
+
return (
|
| 53 |
+
<Router>
|
| 54 |
+
<div className="min-h-screen bg-slate-50 text-slate-900">
|
| 55 |
+
<nav className="bg-white border-b px-8 py-4 flex justify-between items-center">
|
| 56 |
+
<Link to="/" className="text-xl font-bold text-emerald-700">EdTech Platform</Link>
|
| 57 |
+
<Link to="/login" className="text-sm font-medium hover:text-emerald-700">Login</Link>
|
| 58 |
+
</nav>
|
| 59 |
+
<Routes>
|
| 60 |
+
<Route path="/" element={<Home />} />
|
| 61 |
+
<Route path="/login" element={<Login />} />
|
| 62 |
+
</Routes>
|
| 63 |
+
</div>
|
| 64 |
+
</Router>
|
| 65 |
+
)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export default App
|
apps/web/src/index.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
apps/web/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App.tsx'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
apps/web/tailwind.config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {},
|
| 9 |
+
},
|
| 10 |
+
plugins: [],
|
| 11 |
+
}
|
apps/web/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "@repo/tsconfig/vite-react.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"baseUrl": ".",
|
| 5 |
+
"paths": {
|
| 6 |
+
"@/*": [
|
| 7 |
+
"./src/*"
|
| 8 |
+
]
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"include": [
|
| 12 |
+
"src"
|
| 13 |
+
],
|
| 14 |
+
"references": [
|
| 15 |
+
{
|
| 16 |
+
"path": "./tsconfig.node.json"
|
| 17 |
+
}
|
| 18 |
+
]
|
| 19 |
+
}
|
apps/web/tsconfig.node.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"skipLibCheck": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "bundler",
|
| 7 |
+
"allowSyntheticDefaultImports": true
|
| 8 |
+
},
|
| 9 |
+
"include": [
|
| 10 |
+
"vite.config.ts"
|
| 11 |
+
]
|
| 12 |
+
}
|
apps/web/vite.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import path from 'path'
|
| 4 |
+
|
| 5 |
+
// https://vitejs.dev/config/
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [react()],
|
| 8 |
+
resolve: {
|
| 9 |
+
alias: {
|
| 10 |
+
"@": path.resolve(__dirname, "./src"),
|
| 11 |
+
},
|
| 12 |
+
},
|
| 13 |
+
})
|
apps/whatsapp-worker/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "whatsapp-worker",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "tsx watch src/index.ts",
|
| 7 |
+
"build": "tsc",
|
| 8 |
+
"start": "node dist/index.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"bullmq": "^4.0.0",
|
| 12 |
+
"dotenv": "^16.0.0",
|
| 13 |
+
"@repo/database": "workspace:*"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"tsx": "^3.0.0",
|
| 17 |
+
"typescript": "^5.0.0",
|
| 18 |
+
"@types/node": "^20.0.0",
|
| 19 |
+
"@repo/tsconfig": "workspace:*"
|
| 20 |
+
}
|
| 21 |
+
}
|
apps/whatsapp-worker/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Worker } from 'bullmq';
|
| 2 |
+
import dotenv from 'dotenv';
|
| 3 |
+
|
| 4 |
+
dotenv.config();
|
| 5 |
+
|
| 6 |
+
const connection = {
|
| 7 |
+
host: process.env.REDIS_HOST || 'localhost',
|
| 8 |
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
const worker = new Worker('whatsapp-queue', async job => {
|
| 12 |
+
console.log('Processing job:', job.id, job.data);
|
| 13 |
+
// Simulating work
|
| 14 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 15 |
+
console.log('Job completed:', job.id);
|
| 16 |
+
}, { connection });
|
| 17 |
+
|
| 18 |
+
console.log('WhatsApp Worker started...');
|
| 19 |
+
|
| 20 |
+
worker.on('completed', job => {
|
| 21 |
+
console.log(`${job.id} has completed!`);
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
worker.on('failed', (job, err) => {
|
| 25 |
+
console.log(`${job?.id} has failed with ${err.message}`);
|
| 26 |
+
});
|
apps/whatsapp-worker/tsconfig.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "@repo/tsconfig/base.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"outDir": "dist",
|
| 5 |
+
"rootDir": "."
|
| 6 |
+
},
|
| 7 |
+
"include": [
|
| 8 |
+
"src/**/*"
|
| 9 |
+
]
|
| 10 |
+
}
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
services:
|
| 3 |
+
postgres:
|
| 4 |
+
image: postgres:15-alpine
|
| 5 |
+
environment:
|
| 6 |
+
POSTGRES_USER: user
|
| 7 |
+
POSTGRES_PASSWORD: password
|
| 8 |
+
POSTGRES_DB: edtech
|
| 9 |
+
ports:
|
| 10 |
+
- "5432:5432"
|
| 11 |
+
volumes:
|
| 12 |
+
- postgres_data:/var/lib/postgresql/data
|
| 13 |
+
|
| 14 |
+
redis:
|
| 15 |
+
image: redis:alpine
|
| 16 |
+
ports:
|
| 17 |
+
- "6379:6379"
|
| 18 |
+
volumes:
|
| 19 |
+
- redis_data:/data
|
| 20 |
+
|
| 21 |
+
volumes:
|
| 22 |
+
postgres_data:
|
| 23 |
+
redis_data:
|
package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "edtech-platform",
|
| 3 |
+
"private": true,
|
| 4 |
+
"scripts": {
|
| 5 |
+
"build": "turbo run build",
|
| 6 |
+
"dev": "turbo run dev",
|
| 7 |
+
"lint": "turbo run lint",
|
| 8 |
+
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
| 9 |
+
},
|
| 10 |
+
"devDependencies": {
|
| 11 |
+
"turbo": "^1.10.0",
|
| 12 |
+
"prettier": "^3.0.0",
|
| 13 |
+
"typescript": "^5.0.0"
|
| 14 |
+
},
|
| 15 |
+
"packageManager": "pnpm@8.0.0",
|
| 16 |
+
"engines": {
|
| 17 |
+
"node": ">=18"
|
| 18 |
+
}
|
| 19 |
+
}
|
packages/database/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export * from '@prisma/client';
|
packages/database/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@repo/database",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"main": "./index.ts",
|
| 5 |
+
"types": "./index.ts",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"db:push": "prisma db push",
|
| 8 |
+
"db:studio": "prisma studio",
|
| 9 |
+
"generate": "prisma generate"
|
| 10 |
+
},
|
| 11 |
+
"prisma": {
|
| 12 |
+
"seed": "tsx seed.ts"
|
| 13 |
+
},
|
| 14 |
+
"dependencies": {
|
| 15 |
+
"@prisma/client": "^5.0.0"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"prisma": "^5.0.0",
|
| 19 |
+
"@repo/tsconfig": "workspace:*"
|
| 20 |
+
}
|
| 21 |
+
}
|
packages/database/prisma/schema.prisma
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generator client {
|
| 2 |
+
provider = "prisma-client-js"
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
datasource db {
|
| 6 |
+
provider = "postgresql"
|
| 7 |
+
url = env("DATABASE_URL")
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
model User {
|
| 11 |
+
id String @id @default(uuid())
|
| 12 |
+
phone String @unique
|
| 13 |
+
name String?
|
| 14 |
+
role Role @default(STUDENT)
|
| 15 |
+
language Language @default(FR)
|
| 16 |
+
city String?
|
| 17 |
+
activity String? // Business activity/sector
|
| 18 |
+
createdAt DateTime @default(now())
|
| 19 |
+
updatedAt DateTime @updatedAt
|
| 20 |
+
|
| 21 |
+
enrollments Enrollment[]
|
| 22 |
+
responses Response[]
|
| 23 |
+
messages Message[]
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
model Track {
|
| 27 |
+
id String @id @default(uuid())
|
| 28 |
+
title String
|
| 29 |
+
description String?
|
| 30 |
+
duration Int // Duration in days
|
| 31 |
+
language Language @default(FR)
|
| 32 |
+
createdAt DateTime @default(now())
|
| 33 |
+
updatedAt DateTime @updatedAt
|
| 34 |
+
|
| 35 |
+
days TrackDay[]
|
| 36 |
+
enrollments Enrollment[]
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
model TrackDay {
|
| 40 |
+
id String @id @default(uuid())
|
| 41 |
+
trackId String
|
| 42 |
+
dayNumber Int
|
| 43 |
+
contentType ContentType // AUDIO, TEXT, IMAGE
|
| 44 |
+
contentUrl String?
|
| 45 |
+
textContent String? // Prompt text or message body
|
| 46 |
+
createdAt DateTime @default(now())
|
| 47 |
+
updatedAt DateTime @updatedAt
|
| 48 |
+
|
| 49 |
+
track Track @relation(fields: [trackId], references: [id])
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
model Enrollment {
|
| 53 |
+
id String @id @default(uuid())
|
| 54 |
+
userId String
|
| 55 |
+
trackId String
|
| 56 |
+
status EnrollmentStatus @default(ACTIVE)
|
| 57 |
+
currentDay Int @default(1)
|
| 58 |
+
startedAt DateTime @default(now())
|
| 59 |
+
completedAt DateTime?
|
| 60 |
+
lastActivityAt DateTime @default(now())
|
| 61 |
+
|
| 62 |
+
user User @relation(fields: [userId], references: [id])
|
| 63 |
+
track Track @relation(fields: [trackId], references: [id])
|
| 64 |
+
responses Response[]
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
model Response {
|
| 68 |
+
id String @id @default(uuid())
|
| 69 |
+
enrollmentId String
|
| 70 |
+
userId String
|
| 71 |
+
dayNumber Int
|
| 72 |
+
content String? // Text response
|
| 73 |
+
mediaUrl String? // Voice/Image response
|
| 74 |
+
createdAt DateTime @default(now())
|
| 75 |
+
|
| 76 |
+
enrollment Enrollment @relation(fields: [enrollmentId], references: [id])
|
| 77 |
+
user User @relation(fields: [userId], references: [id])
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
model Message {
|
| 81 |
+
id String @id @default(uuid())
|
| 82 |
+
userId String
|
| 83 |
+
direction Direction // INBOUND, OUTBOUND
|
| 84 |
+
channel String @default("WHATSAPP")
|
| 85 |
+
payload Json? // Raw payload from provider
|
| 86 |
+
createdAt DateTime @default(now())
|
| 87 |
+
|
| 88 |
+
user User @relation(fields: [userId], references: [id])
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
enum Role {
|
| 92 |
+
STUDENT
|
| 93 |
+
ADMIN
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
enum EnrollmentStatus {
|
| 97 |
+
ACTIVE
|
| 98 |
+
COMPLETED
|
| 99 |
+
DROPPED
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
enum Language {
|
| 103 |
+
FR
|
| 104 |
+
WOLOF
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
enum ContentType {
|
| 108 |
+
TEXT
|
| 109 |
+
AUDIO
|
| 110 |
+
IMAGE
|
| 111 |
+
VIDEO
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
enum Direction {
|
| 115 |
+
INBOUND
|
| 116 |
+
OUTBOUND
|
| 117 |
+
}
|
| 118 |
+
|
packages/database/seed.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PrismaClient } from '@prisma/client';
|
| 2 |
+
|
| 3 |
+
const prisma = new PrismaClient();
|
| 4 |
+
|
| 5 |
+
async function main() {
|
| 6 |
+
// 1. Business Model Track (7 days)
|
| 7 |
+
const businessTrack = await prisma.track.create({
|
| 8 |
+
data: {
|
| 9 |
+
title: "Business Model Express",
|
| 10 |
+
description: "Structurez votre projet en 7 jours",
|
| 11 |
+
duration: 7,
|
| 12 |
+
language: "FR",
|
| 13 |
+
days: {
|
| 14 |
+
create: [
|
| 15 |
+
{ dayNumber: 1, contentType: "TEXT", textContent: "Bienvenue ! Jour 1: Quel est le problème que vous résolvez ? Répondez par un court message." },
|
| 16 |
+
{ dayNumber: 2, contentType: "TEXT", textContent: "Jour 2: Qui est votre client idéal ? (Age, métier, localisation)" },
|
| 17 |
+
{ dayNumber: 3, contentType: "TEXT", textContent: "Jour 3: Quelle est votre solution en une phrase simple ?" },
|
| 18 |
+
{ dayNumber: 4, contentType: "TEXT", textContent: "Jour 4: Comment gagnez-vous de l'argent ? (Prix, abonnement, marge)" },
|
| 19 |
+
{ dayNumber: 5, contentType: "TEXT", textContent: "Jour 5: Quels sont vos coûts principaux ?" },
|
| 20 |
+
{ dayNumber: 6, contentType: "TEXT", textContent: "Jour 6: Avez-vous déjà vendu ? Donnez un chiffre (CA ou nombre de clients)." },
|
| 21 |
+
{ dayNumber: 7, contentType: "TEXT", textContent: "Jour 7: Félicitations ! Envoyez 'PITCH' pour générer votre mini-document." }
|
| 22 |
+
]
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// 2. Pitch Track (7 days)
|
| 28 |
+
const pitchTrack = await prisma.track.create({
|
| 29 |
+
data: {
|
| 30 |
+
title: "Pitch Master",
|
| 31 |
+
description: "Apprenez à convaincre en 30 secondes",
|
| 32 |
+
duration: 7,
|
| 33 |
+
language: "FR",
|
| 34 |
+
days: {
|
| 35 |
+
create: [
|
| 36 |
+
{ dayNumber: 1, contentType: "TEXT", textContent: "Jour 1: L'accroche. Comment capter l'attention en 5 secondes ?" },
|
| 37 |
+
// ... more days
|
| 38 |
+
]
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
console.log({ businessTrack, pitchTrack });
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
main()
|
| 47 |
+
.then(async () => {
|
| 48 |
+
await prisma.$disconnect()
|
| 49 |
+
})
|
| 50 |
+
.catch(async (e) => {
|
| 51 |
+
console.error(e)
|
| 52 |
+
await prisma.$disconnect()
|
| 53 |
+
process.exit(1)
|
| 54 |
+
})
|
packages/database/tsconfig.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "@repo/tsconfig/base.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"outDir": "dist",
|
| 5 |
+
"rootDir": "."
|
| 6 |
+
},
|
| 7 |
+
"include": [
|
| 8 |
+
"."
|
| 9 |
+
]
|
| 10 |
+
}
|
packages/shared-types/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@repo/shared-types",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"main": "./dist/index.js",
|
| 5 |
+
"types": "./dist/index.d.ts",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"build": "tsc",
|
| 8 |
+
"dev": "tsc -w"
|
| 9 |
+
},
|
| 10 |
+
"devDependencies": {
|
| 11 |
+
"typescript": "^5.0.0",
|
| 12 |
+
"zod": "^3.0.0",
|
| 13 |
+
"@repo/tsconfig": "workspace:*"
|
| 14 |
+
},
|
| 15 |
+
"dependencies": {
|
| 16 |
+
"zod": "^3.0.0"
|
| 17 |
+
}
|
| 18 |
+
}
|
packages/shared-types/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { z } from 'zod';
|
| 2 |
+
|
| 3 |
+
export const UserSchema = z.object({
|
| 4 |
+
id: z.string().uuid(),
|
| 5 |
+
phone: z.string(),
|
| 6 |
+
name: z.string().optional(),
|
| 7 |
+
language: z.enum(['FR', 'WOLOF']).default('FR'),
|
| 8 |
+
city: z.string().optional(),
|
| 9 |
+
activity: z.string().optional(),
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
export type User = z.infer<typeof UserSchema>;
|
| 13 |
+
|
| 14 |
+
export const CreateUserSchema = UserSchema.pick({ phone: true, name: true, language: true, city: true, activity: true });
|
| 15 |
+
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
|
| 16 |
+
|
| 17 |
+
export const WebhookPayloadSchema = z.object({
|
| 18 |
+
entry: z.array(z.object({
|
| 19 |
+
changes: z.array(z.object({
|
| 20 |
+
value: z.object({
|
| 21 |
+
messages: z.array(z.object({
|
| 22 |
+
from: z.string(),
|
| 23 |
+
text: z.object({
|
| 24 |
+
body: z.string()
|
| 25 |
+
}).optional(),
|
| 26 |
+
type: z.string()
|
| 27 |
+
})).optional()
|
| 28 |
+
})
|
| 29 |
+
}))
|
| 30 |
+
}))
|
| 31 |
+
});
|
packages/shared-types/tsconfig.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "@repo/tsconfig/base.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"outDir": "dist",
|
| 5 |
+
"rootDir": "src"
|
| 6 |
+
},
|
| 7 |
+
"include": [
|
| 8 |
+
"src/**/*"
|
| 9 |
+
]
|
| 10 |
+
}
|
packages/tsconfig/base.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"lib": [
|
| 5 |
+
"DOM",
|
| 6 |
+
"DOM.Iterable",
|
| 7 |
+
"ESNext"
|
| 8 |
+
],
|
| 9 |
+
"module": "ESNext",
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"resolveJsonModule": true,
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
"jsx": "react-jsx",
|
| 17 |
+
"strict": true,
|
| 18 |
+
"noUnusedLocals": true,
|
| 19 |
+
"noUnusedParameters": true,
|
| 20 |
+
"noFallthroughCasesInSwitch": true
|
| 21 |
+
}
|
| 22 |
+
}
|
packages/tsconfig/package.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@repo/tsconfig",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"files": [
|
| 6 |
+
"base.json",
|
| 7 |
+
"nextjs.json",
|
| 8 |
+
"react.json"
|
| 9 |
+
]
|
| 10 |
+
}
|
packages/tsconfig/vite-react.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": [
|
| 6 |
+
"ES2020",
|
| 7 |
+
"DOM",
|
| 8 |
+
"DOM.Iterable"
|
| 9 |
+
],
|
| 10 |
+
"module": "ESNext",
|
| 11 |
+
"skipLibCheck": true,
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"allowImportingTsExtensions": true,
|
| 14 |
+
"resolveJsonModule": true,
|
| 15 |
+
"isolatedModules": true,
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"noFallthroughCasesInSwitch": true
|
| 22 |
+
}
|
| 23 |
+
}
|
pnpm-workspace.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
packages:
|
| 2 |
+
- "apps/*"
|
| 3 |
+
- "packages/*"
|