CognxSafeTrack commited on
Commit
cc442ef
·
0 Parent(s):

Initial commit: Monorepo setup with React+Vite, Fastify, Prisma, and Docker Compose

Browse files
Files changed (44) hide show
  1. .gitignore +32 -0
  2. apps/admin/index.html +13 -0
  3. apps/admin/package.json +30 -0
  4. apps/admin/postcss.config.js +6 -0
  5. apps/admin/src/App.tsx +59 -0
  6. apps/admin/src/index.css +3 -0
  7. apps/admin/src/main.tsx +10 -0
  8. apps/admin/tailwind.config.js +11 -0
  9. apps/admin/tsconfig.json +19 -0
  10. apps/admin/tsconfig.node.json +12 -0
  11. apps/admin/vite.config.ts +13 -0
  12. apps/api/package.json +22 -0
  13. apps/api/src/index.ts +28 -0
  14. apps/api/src/routes/whatsapp.ts +46 -0
  15. apps/api/src/services/prisma.ts +3 -0
  16. apps/api/src/services/whatsapp.ts +44 -0
  17. apps/api/tsconfig.json +10 -0
  18. apps/web/index.html +16 -0
  19. apps/web/package.json +30 -0
  20. apps/web/postcss.config.js +6 -0
  21. apps/web/src/App.tsx +68 -0
  22. apps/web/src/index.css +3 -0
  23. apps/web/src/main.tsx +10 -0
  24. apps/web/tailwind.config.js +11 -0
  25. apps/web/tsconfig.json +19 -0
  26. apps/web/tsconfig.node.json +12 -0
  27. apps/web/vite.config.ts +13 -0
  28. apps/whatsapp-worker/package.json +21 -0
  29. apps/whatsapp-worker/src/index.ts +26 -0
  30. apps/whatsapp-worker/tsconfig.json +10 -0
  31. docker-compose.yml +23 -0
  32. package.json +19 -0
  33. packages/database/index.ts +1 -0
  34. packages/database/package.json +21 -0
  35. packages/database/prisma/schema.prisma +118 -0
  36. packages/database/seed.ts +54 -0
  37. packages/database/tsconfig.json +10 -0
  38. packages/shared-types/package.json +18 -0
  39. packages/shared-types/src/index.ts +31 -0
  40. packages/shared-types/tsconfig.json +10 -0
  41. packages/tsconfig/base.json +22 -0
  42. packages/tsconfig/package.json +10 -0
  43. packages/tsconfig/vite-react.json +23 -0
  44. 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/*"