rinogeek commited on
Commit
46e985f
·
0 Parent(s):

Deploy Frontend to HF

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +13 -0
  2. .gitignore +4 -0
  3. Dockerfile +35 -0
  4. README.md +46 -0
  5. afridatahub-frontend/components.json +21 -0
  6. afridatahub-frontend/eslint.config.js +33 -0
  7. afridatahub-frontend/index.html +13 -0
  8. afridatahub-frontend/jsconfig.json +8 -0
  9. afridatahub-frontend/package-lock.json +0 -0
  10. afridatahub-frontend/package.json +82 -0
  11. afridatahub-frontend/pnpm-lock.yaml +0 -0
  12. afridatahub-frontend/src/App.css +205 -0
  13. afridatahub-frontend/src/App.jsx +207 -0
  14. afridatahub-frontend/src/assets/africa.json +1 -0
  15. afridatahub-frontend/src/assets/react.svg +1 -0
  16. afridatahub-frontend/src/components/APIDocumentation.jsx +317 -0
  17. afridatahub-frontend/src/components/AfricaMap.jsx +190 -0
  18. afridatahub-frontend/src/components/Alerts.jsx +384 -0
  19. afridatahub-frontend/src/components/Analytics.jsx +311 -0
  20. afridatahub-frontend/src/components/Charts.jsx +242 -0
  21. afridatahub-frontend/src/components/Dashboard.jsx +297 -0
  22. afridatahub-frontend/src/components/DatasetAnalysis.jsx +275 -0
  23. afridatahub-frontend/src/components/DatasetCard.jsx +129 -0
  24. afridatahub-frontend/src/components/Datasets.jsx +443 -0
  25. afridatahub-frontend/src/components/LandingPage.jsx +275 -0
  26. afridatahub-frontend/src/components/Login.jsx +194 -0
  27. afridatahub-frontend/src/components/Navigation.jsx +166 -0
  28. afridatahub-frontend/src/components/Profile.jsx +364 -0
  29. afridatahub-frontend/src/components/Register.jsx +235 -0
  30. afridatahub-frontend/src/components/ui/accordion.jsx +62 -0
  31. afridatahub-frontend/src/components/ui/alert-dialog.jsx +138 -0
  32. afridatahub-frontend/src/components/ui/alert.jsx +63 -0
  33. afridatahub-frontend/src/components/ui/aspect-ratio.jsx +9 -0
  34. afridatahub-frontend/src/components/ui/avatar.jsx +47 -0
  35. afridatahub-frontend/src/components/ui/badge.jsx +44 -0
  36. afridatahub-frontend/src/components/ui/breadcrumb.jsx +112 -0
  37. afridatahub-frontend/src/components/ui/button.jsx +55 -0
  38. afridatahub-frontend/src/components/ui/calendar.jsx +72 -0
  39. afridatahub-frontend/src/components/ui/card.jsx +101 -0
  40. afridatahub-frontend/src/components/ui/carousel.jsx +195 -0
  41. afridatahub-frontend/src/components/ui/chart.jsx +309 -0
  42. afridatahub-frontend/src/components/ui/checkbox.jsx +30 -0
  43. afridatahub-frontend/src/components/ui/collapsible.jsx +21 -0
  44. afridatahub-frontend/src/components/ui/command.jsx +155 -0
  45. afridatahub-frontend/src/components/ui/context-menu.jsx +224 -0
  46. afridatahub-frontend/src/components/ui/dialog.jsx +131 -0
  47. afridatahub-frontend/src/components/ui/drawer.jsx +131 -0
  48. afridatahub-frontend/src/components/ui/dropdown-menu.jsx +223 -0
  49. afridatahub-frontend/src/components/ui/form.jsx +143 -0
  50. afridatahub-frontend/src/components/ui/hover-card.jsx +39 -0
.dockerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ yarn-error.log
4
+ pnpm-debug.log
5
+ .git
6
+ .gitignore
7
+ .env
8
+ .env.local
9
+ dist
10
+ build
11
+ .DS_Store
12
+ *.md
13
+ coverage
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Utiliser une image Node officielle légère
2
+ FROM node:20-alpine
3
+
4
+ # Installer pnpm globalement
5
+ RUN npm install -g pnpm
6
+
7
+ # Définir le répertoire de travail
8
+ WORKDIR /app
9
+
10
+ # Copier les fichiers de dépendances
11
+ # On suppose que le contexte de build est le dossier /frontend
12
+ # et que le projet React est dans le sous-dossier afridatahub-frontend
13
+ COPY afridatahub-frontend/package.json afridatahub-frontend/pnpm-lock.yaml ./
14
+
15
+ # Installer les dépendances
16
+ RUN pnpm install --frozen-lockfile
17
+
18
+ # Copier le reste du code source
19
+ COPY afridatahub-frontend/ .
20
+
21
+ # Argument de build pour l'URL de l'API Backend
22
+ # IMPORTANT: Vous devez définir cette variable (VITE_API_URL) dans les "Secrets" ou "Variables" de votre Space Hugging Face
23
+ # Valeur attendue: https://rinogeek-afridatahubbackend.hf.space/api/
24
+ ARG VITE_API_URL
25
+ ENV VITE_API_URL=$VITE_API_URL
26
+
27
+ # Construire l'application pour la production
28
+ RUN pnpm run build
29
+
30
+ # Exposer le port 7860 (Port par défaut pour HF Spaces)
31
+ EXPOSE 7860
32
+
33
+ # Lancer le serveur de prévisualisation Vite
34
+ # --host permet d'écouter sur toutes les interfaces (0.0.0.0)
35
+ CMD ["pnpm", "run", "preview", "--", "--port", "7860", "--host"]
README.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AfriDataHub Frontend
3
+ emoji: 🌍
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # AfriDataHub Frontend
12
+
13
+ This is the user interface for **AfriDataHub**, an interactive platform for exploring African data through advanced visualizations and AI insights.
14
+
15
+ ## 🎨 Features
16
+
17
+ - **Interactive Maps**: Real-time visualization of data across the African continent.
18
+ - **AI Dashboard**: Predictive analytics and insights generated by Gemini.
19
+ - **Dynamic Charts**: Rich data visualization using Recharts.
20
+ - **Responsive Design**: Modern UI built with Tailwind CSS and Framer Motion.
21
+
22
+ ## 🛠 Technology Stack
23
+
24
+ - **Framework**: React (Vite)
25
+ - **Styling**: Tailwind CSS
26
+ - **State Management**: React Hooks
27
+ - **Maps**: React Simple Maps
28
+ - **Icons**: Lucide React
29
+
30
+ ## 🚀 Deployment
31
+
32
+ This application is containerized using Docker and is designed to run on Hugging Face Spaces.
33
+
34
+ ### Environment Configuration
35
+
36
+ To connect to the backend, ensure you set the `VITE_API_URL` variable in your Space settings:
37
+
38
+ - `VITE_API_URL`: `https://rinogeek-afridatahubbackend.hf.space/api/` (or your specific backend URL).
39
+
40
+ ## 📦 Run Locally
41
+
42
+ ```bash
43
+ cd afridatahub-frontend
44
+ pnpm install
45
+ pnpm run dev
46
+ ```
afridatahub-frontend/components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": false,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/App.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
afridatahub-frontend/eslint.config.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+
6
+ export default [
7
+ { ignores: ['dist'] },
8
+ {
9
+ files: ['**/*.{js,jsx}'],
10
+ languageOptions: {
11
+ ecmaVersion: 2020,
12
+ globals: globals.browser,
13
+ parserOptions: {
14
+ ecmaVersion: 'latest',
15
+ ecmaFeatures: { jsx: true },
16
+ sourceType: 'module',
17
+ },
18
+ },
19
+ plugins: {
20
+ 'react-hooks': reactHooks,
21
+ 'react-refresh': reactRefresh,
22
+ },
23
+ rules: {
24
+ ...js.configs.recommended.rules,
25
+ ...reactHooks.configs.recommended.rules,
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ 'react-refresh/only-export-components': [
28
+ 'warn',
29
+ { allowConstantExport: true },
30
+ ],
31
+ },
32
+ },
33
+ ]
afridatahub-frontend/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/x-icon" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>AfriDataHub - Plateforme de données africaines ouvertes</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
afridatahub-frontend/jsconfig.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": "./",
4
+ "paths": {
5
+ "@/*": ["src/*"]
6
+ }
7
+ }
8
+ }
afridatahub-frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
afridatahub-frontend/package.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "afridatahub-frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@hookform/resolvers": "^5.0.1",
14
+ "@radix-ui/react-accordion": "^1.2.10",
15
+ "@radix-ui/react-alert-dialog": "^1.1.13",
16
+ "@radix-ui/react-aspect-ratio": "^1.1.6",
17
+ "@radix-ui/react-avatar": "^1.1.9",
18
+ "@radix-ui/react-checkbox": "^1.3.1",
19
+ "@radix-ui/react-collapsible": "^1.1.10",
20
+ "@radix-ui/react-context-menu": "^2.2.14",
21
+ "@radix-ui/react-dialog": "^1.1.13",
22
+ "@radix-ui/react-dropdown-menu": "^2.1.14",
23
+ "@radix-ui/react-hover-card": "^1.1.13",
24
+ "@radix-ui/react-label": "^2.1.6",
25
+ "@radix-ui/react-menubar": "^1.1.14",
26
+ "@radix-ui/react-navigation-menu": "^1.2.12",
27
+ "@radix-ui/react-popover": "^1.1.13",
28
+ "@radix-ui/react-progress": "^1.1.6",
29
+ "@radix-ui/react-radio-group": "^1.3.6",
30
+ "@radix-ui/react-scroll-area": "^1.2.8",
31
+ "@radix-ui/react-select": "^2.2.4",
32
+ "@radix-ui/react-separator": "^1.1.6",
33
+ "@radix-ui/react-slider": "^1.3.4",
34
+ "@radix-ui/react-slot": "^1.2.2",
35
+ "@radix-ui/react-switch": "^1.2.4",
36
+ "@radix-ui/react-tabs": "^1.1.11",
37
+ "@radix-ui/react-toggle": "^1.1.8",
38
+ "@radix-ui/react-toggle-group": "^1.1.9",
39
+ "@radix-ui/react-tooltip": "^1.2.6",
40
+ "@tailwindcss/vite": "^4.1.7",
41
+ "class-variance-authority": "^0.7.1",
42
+ "clsx": "^2.1.1",
43
+ "cmdk": "^1.1.1",
44
+ "d3-scale": "^4.0.2",
45
+ "d3-scale-chromatic": "^3.1.0",
46
+ "date-fns": "^4.1.0",
47
+ "embla-carousel-react": "^8.6.0",
48
+ "framer-motion": "^12.15.0",
49
+ "input-otp": "^1.4.2",
50
+ "lucide-react": "^0.510.0",
51
+ "next-themes": "^0.4.6",
52
+ "react": "^19.1.0",
53
+ "react-day-picker": "8.10.1",
54
+ "react-dom": "^19.1.0",
55
+ "react-hook-form": "^7.56.3",
56
+ "react-markdown": "^10.1.0",
57
+ "react-resizable-panels": "^3.0.2",
58
+ "react-router-dom": "^7.6.1",
59
+ "react-simple-maps": "^3.0.0",
60
+ "recharts": "^2.15.3",
61
+ "remark-gfm": "^4.0.1",
62
+ "sonner": "^2.0.3",
63
+ "tailwind-merge": "^3.3.0",
64
+ "tailwindcss": "^4.1.7",
65
+ "vaul": "^1.1.2",
66
+ "zod": "^3.24.4"
67
+ },
68
+ "devDependencies": {
69
+ "@eslint/js": "^9.25.0",
70
+ "@types/react": "^19.1.2",
71
+ "@types/react-dom": "^19.1.2",
72
+ "@vitejs/plugin-react": "^4.4.1",
73
+ "baseline-browser-mapping": "^2.9.11",
74
+ "eslint": "^9.25.0",
75
+ "eslint-plugin-react-hooks": "^5.2.0",
76
+ "eslint-plugin-react-refresh": "^0.4.19",
77
+ "globals": "^16.0.0",
78
+ "tw-animate-css": "^1.2.9",
79
+ "vite": "^6.3.5"
80
+ },
81
+ "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af"
82
+ }
afridatahub-frontend/pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
afridatahub-frontend/src/App.css ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --radius-sm: calc(var(--radius) - 4px);
8
+ --radius-md: calc(var(--radius) - 2px);
9
+ --radius-lg: var(--radius);
10
+ --radius-xl: calc(var(--radius) + 4px);
11
+ --color-background: var(--background);
12
+ --color-foreground: var(--foreground);
13
+ --color-card: var(--card);
14
+ --color-card-foreground: var(--card-foreground);
15
+ --color-popover: var(--popover);
16
+ --color-popover-foreground: var(--popover-foreground);
17
+ --color-primary: var(--primary);
18
+ --color-primary-foreground: var(--primary-foreground);
19
+ --color-secondary: var(--secondary);
20
+ --color-secondary-foreground: var(--secondary-foreground);
21
+ --color-muted: var(--muted);
22
+ --color-muted-foreground: var(--muted-foreground);
23
+ --color-accent: var(--accent);
24
+ --color-accent-foreground: var(--accent-foreground);
25
+ --color-destructive: var(--destructive);
26
+ --color-border: var(--border);
27
+ --color-input: var(--input);
28
+ --color-ring: var(--ring);
29
+ --color-chart-1: var(--chart-1);
30
+ --color-chart-2: var(--chart-2);
31
+ --color-chart-3: var(--chart-3);
32
+ --color-chart-4: var(--chart-4);
33
+ --color-chart-5: var(--chart-5);
34
+ --color-sidebar: var(--sidebar);
35
+ --color-sidebar-foreground: var(--sidebar-foreground);
36
+ --color-sidebar-primary: var(--sidebar-primary);
37
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
38
+ --color-sidebar-accent: var(--sidebar-accent);
39
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
40
+ --color-sidebar-border: var(--sidebar-border);
41
+ --color-sidebar-ring: var(--sidebar-ring);
42
+ }
43
+
44
+ :root {
45
+ --radius: 1rem;
46
+ --background: oklch(0.98 0.01 240);
47
+ --foreground: oklch(0.15 0.02 240);
48
+ --card: oklch(1 0 0);
49
+ --card-foreground: oklch(0.15 0.02 240);
50
+ --popover: oklch(1 0 0);
51
+ --popover-foreground: oklch(0.15 0.02 240);
52
+ --primary: oklch(0.55 0.25 260);
53
+ /* Vibrant Indigo/Violet */
54
+ --primary-foreground: oklch(0.98 0.01 240);
55
+ --secondary: oklch(0.65 0.2 330);
56
+ /* Vibrant Pink/Magenta */
57
+ --secondary-foreground: oklch(0.98 0.01 240);
58
+ --muted: oklch(0.92 0.02 240);
59
+ --muted-foreground: oklch(0.45 0.05 240);
60
+ --accent: oklch(0.85 0.15 190);
61
+ /* Cyan/Teal accent */
62
+ --accent-foreground: oklch(0.15 0.02 240);
63
+ --destructive: oklch(0.6 0.2 25);
64
+ --border: oklch(0.9 0.02 240);
65
+ --input: oklch(0.9 0.02 240);
66
+ --ring: oklch(0.55 0.25 260);
67
+
68
+ --glass-bg: rgba(255, 255, 255, 0.7);
69
+ --glass-border: rgba(255, 255, 255, 0.3);
70
+ --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
71
+ }
72
+
73
+ .dark {
74
+ --background: oklch(0.12 0.02 240);
75
+ --foreground: oklch(0.95 0.01 240);
76
+ --card: oklch(0.16 0.03 240);
77
+ --card-foreground: oklch(0.95 0.01 240);
78
+ --popover: oklch(0.16 0.03 240);
79
+ --popover-foreground: oklch(0.95 0.01 240);
80
+ --primary: oklch(0.65 0.2 260);
81
+ --primary-foreground: oklch(0.12 0.02 240);
82
+ --secondary: oklch(0.7 0.15 330);
83
+ --secondary-foreground: oklch(0.12 0.02 240);
84
+ --muted: oklch(0.2 0.04 240);
85
+ --muted-foreground: oklch(0.7 0.02 240);
86
+ --accent: oklch(0.4 0.15 190);
87
+ --accent-foreground: oklch(0.95 0.01 240);
88
+ --destructive: oklch(0.5 0.2 25);
89
+ --border: oklch(0.25 0.04 240);
90
+ --input: oklch(0.25 0.04 240);
91
+ --ring: oklch(0.65 0.2 260);
92
+
93
+ --glass-bg: rgba(20, 20, 30, 0.6);
94
+ --glass-border: rgba(255, 255, 255, 0.1);
95
+ --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
96
+ }
97
+
98
+ @layer utilities {
99
+ .glass {
100
+ background: var(--glass-bg);
101
+ backdrop-filter: blur(12px);
102
+ -webkit-backdrop-filter: blur(12px);
103
+ border: 1px solid var(--glass-border);
104
+ box-shadow: var(--glass-shadow);
105
+ }
106
+
107
+ .text-gradient {
108
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
109
+ -webkit-background-clip: text;
110
+ -webkit-text-fill-color: transparent;
111
+ background-clip: text;
112
+ }
113
+
114
+ .bg-mesh {
115
+ background-color: var(--background);
116
+ background-image:
117
+ radial-gradient(at 0% 0%, oklch(0.55 0.25 260 / 0.15) 0px, transparent 50%),
118
+ radial-gradient(at 100% 0%, oklch(0.65 0.2 330 / 0.15) 0px, transparent 50%),
119
+ radial-gradient(at 100% 100%, oklch(0.85 0.15 190 / 0.1) 0px, transparent 50%),
120
+ radial-gradient(at 0% 100%, oklch(0.55 0.25 260 / 0.1) 0px, transparent 50%);
121
+ }
122
+
123
+ .animate-float {
124
+ animation: float 6s ease-in-out infinite;
125
+ }
126
+
127
+ .shadow-premium {
128
+ box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.1), 0 10px 20px -10px rgba(0, 0, 0, 0.05);
129
+ }
130
+
131
+ .hover-lift {
132
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
133
+ }
134
+
135
+ .hover-lift:hover {
136
+ transform: translateY(-8px) scale(1.02);
137
+ box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.15);
138
+ }
139
+
140
+ @keyframes float {
141
+ 0% {
142
+ transform: translateY(0px);
143
+ }
144
+
145
+ 50% {
146
+ transform: translateY(-10px);
147
+ }
148
+
149
+ 100% {
150
+ transform: translateY(0px);
151
+ }
152
+ }
153
+ }
154
+
155
+ @layer base {
156
+ * {
157
+ @apply border-border outline-ring/50;
158
+ }
159
+
160
+ body {
161
+ @apply bg-background text-foreground;
162
+ }
163
+ }
164
+
165
+ /* Prose styles for Markdown parsing */
166
+ .prose {
167
+ max-width: none;
168
+ color: inherit;
169
+ }
170
+
171
+ .prose p {
172
+ margin-bottom: 1.25em;
173
+ }
174
+
175
+ .prose strong {
176
+ font-weight: 700;
177
+ color: inherit;
178
+ }
179
+
180
+ .prose ul {
181
+ list-style-type: disc;
182
+ padding-left: 1.625em;
183
+ margin-bottom: 1.25em;
184
+ }
185
+
186
+ .prose li {
187
+ margin-bottom: 0.5em;
188
+ }
189
+
190
+ .prose h1,
191
+ .prose h2,
192
+ .prose h3 {
193
+ font-weight: 700;
194
+ margin-top: 1.5em;
195
+ margin-bottom: 0.75em;
196
+ }
197
+
198
+ .prose-lg {
199
+ font-size: 1.125rem;
200
+ line-height: 1.75;
201
+ }
202
+
203
+ .prose-indigo {
204
+ --tw-prose-bullets: var(--color-indigo-500);
205
+ }
afridatahub-frontend/src/App.jsx ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Application principale AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { useState, useEffect } from 'react'
7
+ import Navigation from './components/Navigation'
8
+ import Dashboard from './components/Dashboard'
9
+ import Datasets from './components/Datasets'
10
+ import Analytics from './components/Analytics'
11
+ import Alerts from './components/Alerts'
12
+ import Profile from './components/Profile'
13
+ import Login from './components/Login'
14
+ import Register from './components/Register'
15
+ import DatasetAnalysis from './components/DatasetAnalysis'
16
+ import APIDocumentation from './components/APIDocumentation'
17
+ import LandingPage from './components/LandingPage'
18
+ import { motion } from 'framer-motion'
19
+ import { getCurrentUser, logout } from './services/auth'
20
+ import './App.css'
21
+
22
+ function App() {
23
+ const [currentPage, setCurrentPage] = useState('landing')
24
+ const [user, setUser] = useState(null)
25
+ const [loading, setLoading] = useState(true)
26
+
27
+ useEffect(() => {
28
+ const currentUser = getCurrentUser()
29
+ if (currentUser) {
30
+ setUser(currentUser)
31
+ if (currentPage === 'landing') {
32
+ setCurrentPage('dashboard')
33
+ }
34
+ }
35
+ setLoading(false)
36
+ }, [])
37
+
38
+ const [selectedDataset, setSelectedDataset] = useState(null)
39
+ const [analysisData, setAnalysisData] = useState(null)
40
+
41
+ const handleLoginSuccess = (userData) => {
42
+ setUser(userData)
43
+ setCurrentPage('dashboard')
44
+ }
45
+
46
+ const handleLogout = () => {
47
+ logout()
48
+ setUser(null)
49
+ setCurrentPage('landing')
50
+ }
51
+
52
+ const handleStartAnalysis = (dataset, data) => {
53
+ setSelectedDataset(dataset)
54
+ setAnalysisData(data)
55
+ setCurrentPage('dataset-analysis')
56
+ }
57
+
58
+ const renderCurrentPage = () => {
59
+ if (!user) {
60
+ if (currentPage === 'register') {
61
+ return <Register
62
+ onRegisterSuccess={handleLoginSuccess}
63
+ onNavigateToLogin={() => setCurrentPage('login')}
64
+ onNavigateToLanding={() => setCurrentPage('landing')}
65
+ />
66
+ }
67
+ if (currentPage === 'login') {
68
+ return <Login
69
+ onLoginSuccess={handleLoginSuccess}
70
+ onNavigateToRegister={() => setCurrentPage('register')}
71
+ onNavigateToLanding={() => setCurrentPage('landing')}
72
+ />
73
+ }
74
+ return <LandingPage
75
+ onGetStarted={() => setCurrentPage('register')}
76
+ onLogin={() => setCurrentPage('login')}
77
+ />
78
+ }
79
+
80
+ switch (currentPage) {
81
+ case 'dashboard':
82
+ return <Dashboard />
83
+ case 'datasets':
84
+ return <Datasets onStartAnalysis={handleStartAnalysis} />
85
+ case 'analytics':
86
+ return <Analytics />
87
+ case 'alerts':
88
+ return <Alerts />
89
+ case 'profile':
90
+ return <Profile user={user} onUpdateUser={setUser} />
91
+ case 'api-docs':
92
+ return <APIDocumentation />
93
+ case 'dataset-analysis':
94
+ return (
95
+ <DatasetAnalysis
96
+ dataset={selectedDataset}
97
+ analysisData={analysisData}
98
+ onBack={() => setCurrentPage('datasets')}
99
+ />
100
+ )
101
+ default:
102
+ return <Dashboard />
103
+ }
104
+ }
105
+
106
+ if (loading) {
107
+ return (
108
+ <div className="min-h-screen flex flex-col items-center justify-center bg-background">
109
+ <div className="relative w-20 h-20 mb-6">
110
+ <div className="absolute inset-0 border-4 border-primary/20 rounded-full"></div>
111
+ <div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
112
+ </div>
113
+ <p className="text-xl font-black text-foreground animate-pulse">Initialisation d'AfriDataHub...</p>
114
+ </div>
115
+ )
116
+ }
117
+
118
+ if (!user) {
119
+ return (
120
+ <div className="min-h-screen bg-mesh relative overflow-hidden">
121
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-secondary/5"></div>
122
+ <div className="relative z-10">
123
+ {renderCurrentPage()}
124
+ </div>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ const isAnalysisPage = currentPage === 'dataset-analysis'
130
+
131
+ return (
132
+ <div className="min-h-screen bg-background relative selection:bg-primary/30">
133
+ {/* Global Background Elements */}
134
+ <div className="fixed inset-0 bg-mesh opacity-40 pointer-events-none"></div>
135
+ <div className="fixed top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/20 to-transparent"></div>
136
+
137
+ <Navigation
138
+ currentPage={isAnalysisPage ? 'datasets' : currentPage}
139
+ onPageChange={setCurrentPage}
140
+ user={user}
141
+ onLogout={handleLogout}
142
+ />
143
+
144
+ <main className={`${isAnalysisPage ? '' : 'max-w-7xl mx-auto px-6 lg:px-8'} pt-12 relative z-10`}>
145
+ <motion.div
146
+ key={currentPage}
147
+ initial={{ opacity: 0, y: 10 }}
148
+ animate={{ opacity: 1, y: 0 }}
149
+ transition={{ duration: 0.4, ease: "easeOut" }}
150
+ >
151
+ {renderCurrentPage()}
152
+ </motion.div>
153
+ </main>
154
+
155
+ {/* Footer Premium */}
156
+ <footer className="relative z-10 mt-20 border-t border-white/10 bg-white/5 backdrop-blur-xl">
157
+ <div className="max-w-7xl mx-auto px-6 lg:px-8 py-16">
158
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
159
+ <div className="col-span-1 md:col-span-2">
160
+ <div className="flex items-center space-x-3 mb-6">
161
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center shadow-lg shadow-primary/20">
162
+ <span className="text-white font-black text-xl">A</span>
163
+ </div>
164
+ <span className="text-2xl font-black text-foreground tracking-tighter">AfriDataHub</span>
165
+ </div>
166
+ <p className="text-muted-foreground font-medium text-lg max-w-md leading-relaxed">
167
+ La plateforme d'intelligence de données leader pour le continent africain.
168
+ Propulsé par l'IA pour des décisions basées sur la vérité.
169
+ </p>
170
+ </div>
171
+ <div>
172
+ <h4 className="text-foreground font-black uppercase tracking-widest text-xs mb-6">Plateforme</h4>
173
+ <ul className="space-y-4">
174
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">Datasets</a></li>
175
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">Analyses</a></li>
176
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">Alertes IA</a></li>
177
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">API Cloud</a></li>
178
+ </ul>
179
+ </div>
180
+ <div>
181
+ <h4 className="text-foreground font-black uppercase tracking-widest text-xs mb-6">Ressources</h4>
182
+ <ul className="space-y-4">
183
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">Documentation</a></li>
184
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">Guide Scientifique</a></li>
185
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">Support</a></li>
186
+ <li><a href="#" className="text-muted-foreground hover:text-primary font-bold transition-colors">Contact</a></li>
187
+ </ul>
188
+ </div>
189
+ </div>
190
+
191
+ <div className="pt-8 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-6">
192
+ <p className="text-muted-foreground font-bold text-sm">
193
+ © 2024 AfriDataHub. Créé avec passion par <span className="text-foreground">Marino ATOHOUN</span>.
194
+ </p>
195
+ <div className="flex items-center space-x-8">
196
+ <a href="#" className="text-muted-foreground hover:text-primary transition-colors"><span className="sr-only">Twitter</span>🐦</a>
197
+ <a href="#" className="text-muted-foreground hover:text-primary transition-colors"><span className="sr-only">GitHub</span>🐙</a>
198
+ <a href="#" className="text-muted-foreground hover:text-primary transition-colors"><span className="sr-only">LinkedIn</span>🔗</a>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </footer>
203
+ </div>
204
+ )
205
+ }
206
+
207
+ export default App
afridatahub-frontend/src/assets/africa.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"type":"Topology","objects":{"default":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0]],[[1]],[[2,3]],[[4,5,6,7,8,9,10,11,12,13]]],"id":"UG","properties":{"hc-group":"admin0","hc-key":"ug","hc-a2":"UG","name":"Uganda","labelrank":"3","country-abbrev":"Uga.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"UGA","iso-a2":"UG","woe-id":"23424974","continent":"Africa","hc-middle-lon":32.752,"hc-middle-lat":1.666}},{"type":"MultiPolygon","arcs":[[[14]],[[15,16,17,18,19]]],"id":"NG","properties":{"hc-group":"admin0","hc-key":"ng","hc-a2":"NG","name":"Nigeria","labelrank":"2","country-abbrev":"Nigeria","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"NGA","iso-a2":"NG","woe-id":"23424908","continent":"Africa","hc-middle-lon":8.67,"hc-middle-lat":9.569}},{"type":"Polygon","arcs":[[20]],"id":"ST","properties":{"hc-group":"admin0","hc-key":"st","hc-a2":"ST","name":"Sao Tome and Principe","labelrank":"6","country-abbrev":"S.T.P.","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"STP","iso-a2":"ST","woe-id":"23424966","continent":"Africa","hc-middle-lon":6.63,"hc-middle-lat":0.245}},{"type":"MultiPolygon","arcs":[[[21]],[[22]],[[23]],[[24]],[[25,26,27,28,29,30,31,32,33,34,35,36,37,38,-12,39,-4,40,41,42,43]]],"id":"TZ","properties":{"hc-group":"admin0","hc-key":"tz","hc-a2":"TZ","name":"United Republic of Tanzania","labelrank":"3","country-abbrev":"Tanz.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"TZA","iso-a2":"TZ","woe-id":"23424973","continent":"Africa","hc-middle-lon":34.683,"hc-middle-lat":-6.28}},{"type":"MultiPolygon","arcs":[[[44]],[[45,46,47]]],"id":"SL","properties":{"hc-group":"admin0","hc-key":"sl","hc-a2":"SL","name":"Sierra Leone","labelrank":"4","country-abbrev":"S.L.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"SLE","iso-a2":"SL","woe-id":"23424946","continent":"Africa","hc-middle-lon":-12.32,"hc-middle-lat":8.106}},{"type":"MultiPolygon","arcs":[[[48]],[[49,50,51]]],"id":"GW","properties":{"hc-group":"admin0","hc-key":"gw","hc-a2":"GW","name":"Guinea Bissau","labelrank":"6","country-abbrev":"GnB.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"GNB","iso-a2":"GW","woe-id":"23424929","continent":"Africa","hc-middle-lon":-15.078,"hc-middle-lat":11.834}},{"type":"MultiPolygon","arcs":[[[52]],[[53]],[[54]],[[55]],[[56]],[[57]]],"id":"CV","properties":{"hc-group":"admin0","hc-key":"cv","hc-a2":"CV","name":"Cape Verde","labelrank":"4","country-abbrev":"C.Vd.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"CPV","iso-a2":"CV","woe-id":"23424794","continent":"Africa","hc-middle-lon":-23.667,"hc-middle-lat":15.075}},{"type":"Polygon","arcs":[[58]],"id":"SC","properties":{"hc-group":"admin0","hc-key":"sc","hc-a2":"SC","name":"Seychelles","labelrank":"6","country-abbrev":"Syc.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"SYC","iso-a2":"SC","woe-id":"23424941","continent":"Seven seas (open ocean)","hc-middle-lon":55.5,"hc-middle-lat":-4.702}},{"type":"MultiPolygon","arcs":[[[59]],[[60,61,62]]],"id":"TN","properties":{"hc-group":"admin0","hc-key":"tn","hc-a2":"TN","name":"Tunisia","labelrank":"3","country-abbrev":"Tun.","subregion":"Northern Africa","region-wb":"Middle East & North Africa","iso-a3":"TUN","iso-a2":"TN","woe-id":"23424967","continent":"Africa","hc-middle-lon":9.523,"hc-middle-lat":33.217}},{"type":"MultiPolygon","arcs":[[[63]],[[64]]],"id":"MG","properties":{"hc-group":"admin0","hc-key":"mg","hc-a2":"MG","name":"Madagascar","labelrank":"3","country-abbrev":"Mad.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"MDG","iso-a2":"MG","woe-id":"23424883","continent":"Africa","hc-middle-lon":46.348,"hc-middle-lat":-19.041}},{"type":"Polygon","arcs":[[65,-10,66,67,68,69,-42]],"id":"KE","properties":{"hc-group":"admin0","hc-key":"ke","hc-a2":"KE","name":"Kenya","labelrank":"2","country-abbrev":"Ken.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"KEN","iso-a2":"KE","woe-id":"23424863","continent":"Africa","hc-middle-lon":37.715,"hc-middle-lat":0.866}},{"type":"MultiPolygon","arcs":[[[70]],[[71,-6,72,-14,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,-8]]],"id":"CD","properties":{"hc-group":"admin0","hc-key":"cd","hc-a2":"CD","name":"Democratic Republic of the Congo","labelrank":"2","country-abbrev":"D.R.C.","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"COD","iso-a2":"CD","woe-id":"23424780","continent":"Africa","hc-middle-lon":24.998,"hc-middle-lat":-7.296}},{"type":"MultiPolygon","arcs":[[[89]],[[90]]],"id":"FR","properties":{"hc-group":"admin0","hc-key":"fr","hc-a2":"FR","name":"France","labelrank":"2","country-abbrev":"Fr.","subregion":"Western Europe","region-wb":"Europe & Central Asia","iso-a3":"FRA","iso-a2":"FR","woe-id":"-90","continent":"Europe","hc-middle-lon":55.536,"hc-middle-lat":-21.156}},{"type":"MultiPolygon","arcs":[[[91,92,93,94,95]],[[96,96,96]],[[96,96,96]]],"id":"MR","properties":{"hc-group":"admin0","hc-key":"mr","hc-a2":"MR","name":"Mauritania","labelrank":"3","country-abbrev":"Mrt.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"MRT","iso-a2":"MR","woe-id":"23424896","continent":"Africa","hc-middle-lon":-10.854,"hc-middle-lat":20.424}},{"type":"Polygon","arcs":[[97,98,99,-62,100,101,102,-92]],"id":"DZ","properties":{"hc-group":"admin0","hc-key":"dz","hc-a2":"DZ","name":"Algeria","labelrank":"3","country-abbrev":"Alg.","subregion":"Northern Africa","region-wb":"Middle East & North Africa","iso-a3":"DZA","iso-a2":"DZ","woe-id":"23424740","continent":"Africa","hc-middle-lon":3.724,"hc-middle-lat":28.458}},{"type":"MultiPolygon","arcs":[[[103]],[[104,105,106,107]]],"id":"ER","properties":{"hc-group":"admin0","hc-key":"er","hc-a2":"ER","name":"Eritrea","labelrank":"4","country-abbrev":"Erit.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"ERI","iso-a2":"ER","woe-id":"23424806","continent":"Africa","hc-middle-lon":38.553,"hc-middle-lat":15.811}},{"type":"MultiPolygon","arcs":[[[108]],[[109,110,111]]],"id":"GQ","properties":{"hc-group":"admin0","hc-key":"gq","hc-a2":"GQ","name":"Equatorial Guinea","labelrank":"4","country-abbrev":"Eq. G.","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"GNQ","iso-a2":"GQ","woe-id":"23424804","continent":"Africa","hc-middle-lon":9.669,"hc-middle-lat":1.689}},{"type":"Polygon","arcs":[[112]],"id":"MU","properties":{"hc-group":"admin0","hc-key":"mu","hc-a2":"MU","name":"Mauritius","labelrank":"5","country-abbrev":"Mus.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"MUS","iso-a2":"MU","woe-id":"23424894","continent":"Seven seas (open ocean)","hc-middle-lon":57.584,"hc-middle-lat":-20.304}},{"type":"Polygon","arcs":[[113,-94,114,115,-51,116,117]],"id":"SN","properties":{"hc-group":"admin0","hc-key":"sn","hc-a2":"SN","name":"Senegal","labelrank":"3","country-abbrev":"Sen.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"SEN","iso-a2":"SN","woe-id":"23424943","continent":"Africa","hc-middle-lon":-14.75,"hc-middle-lat":14.871}},{"type":"MultiPolygon","arcs":[[[118]],[[119]]],"id":"KM","properties":{"hc-group":"admin0","hc-key":"km","hc-a2":"KM","name":"Comoros","labelrank":"6","country-abbrev":"Com.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"COM","iso-a2":"KM","woe-id":"23424786","continent":"Africa","hc-middle-lon":43.334,"hc-middle-lat":-11.66}},{"type":"Polygon","arcs":[[120,121,122,123,-68,124,125,-106,126]],"id":"ET","properties":{"hc-group":"admin0","hc-key":"et","hc-a2":"ET","name":"Ethiopia","labelrank":"2","country-abbrev":"Eth.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"ETH","iso-a2":"ET","woe-id":"23424808","continent":"Africa","hc-middle-lon":39.915,"hc-middle-lat":8.052}},{"type":"MultiPolygon","arcs":[[[127,128]],[[129,130,131,132,133,134]]],"id":"CI","properties":{"hc-group":"admin0","hc-key":"ci","hc-a2":"CI","name":"Ivory Coast","labelrank":"3","country-abbrev":"I.C.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"CIV","iso-a2":"CI","woe-id":"23424854","continent":"Africa","hc-middle-lon":-5.563,"hc-middle-lat":7.403}},{"type":"Polygon","arcs":[[-129,135,-133,136,137,138]],"id":"GH","properties":{"hc-group":"admin0","hc-key":"gh","hc-a2":"GH","name":"Ghana","labelrank":"3","country-abbrev":"Ghana","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"GHA","iso-a2":"GH","woe-id":"23424824","continent":"Africa","hc-middle-lon":-1.278,"hc-middle-lat":8.72}},{"type":"Polygon","arcs":[[139,140,141,142,-81,143,-79,144,-36,145,146,147]],"id":"ZM","properties":{"hc-group":"admin0","hc-key":"zm","hc-a2":"ZM","name":"Zambia","labelrank":"3","country-abbrev":"Zambia","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"ZMB","iso-a2":"ZM","woe-id":"23425003","continent":"Africa","hc-middle-lon":26.195,"hc-middle-lat":-14.668}},{"type":"Polygon","arcs":[[-142,148,149,150,151]],"id":"NA","properties":{"hc-group":"admin0","hc-key":"na","hc-a2":"NA","name":"Namibia","labelrank":"3","country-abbrev":"Nam.","subregion":"Southern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"NAM","iso-a2":"NA","woe-id":"23424987","continent":"Africa","hc-middle-lon":17.523,"hc-middle-lat":-21.881}},{"type":"Polygon","arcs":[[-39,152,-76,153,-74,-13]],"id":"RW","properties":{"hc-group":"admin0","hc-key":"rw","hc-a2":"RW","name":"Rwanda","labelrank":"3","country-abbrev":"Rwa.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"RWA","iso-a2":"RW","woe-id":"23424937","continent":"Africa","hc-middle-lon":30.498,"hc-middle-lat":-1.746}},{"type":"Polygon","arcs":[[154,-123,155,156]],"id":"SX","properties":{"hc-group":"admin0","hc-key":"sx","hc-a2":"SX","name":"Somaliland","labelrank":"5","country-abbrev":"Solnd.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"-99","iso-a2":"SX","woe-id":"-99","continent":"Africa","hc-middle-lon":47.194,"hc-middle-lat":9.692}},{"type":"Polygon","arcs":[[-155,157,-69,-124]],"id":"SO","properties":{"hc-group":"admin0","hc-key":"so","hc-a2":"SO","name":"Somalia","labelrank":"6","country-abbrev":"Som.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"SOM","iso-a2":"SO","woe-id":"-90","continent":"Africa","hc-middle-lon":44.007,"hc-middle-lat":3.259}},{"type":"MultiPolygon","arcs":[[[158,159,160,161,162,-112,163,-19,164]],[[165,166]]],"id":"CM","properties":{"hc-group":"admin0","hc-key":"cm","hc-a2":"CM","name":"Cameroon","labelrank":"3","country-abbrev":"Cam.","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"CMR","iso-a2":"CM","woe-id":"23424785","continent":"Africa","hc-middle-lon":12.435,"hc-middle-lat":4.505}},{"type":"Polygon","arcs":[[-162,167,-87,168,169,170]],"id":"CG","properties":{"hc-group":"admin0","hc-key":"cg","hc-a2":"CG","name":"Republic of Congo","labelrank":"4","country-abbrev":"Rep. Congo","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"COG","iso-a2":"CG","woe-id":"23424779","continent":"Africa","hc-middle-lon":15.924,"hc-middle-lat":0.479}},{"type":"Polygon","arcs":[[171,-98,-96,172]],"id":"EH","properties":{"hc-group":"admin0","hc-key":"eh","hc-a2":"EH","name":"Western Sahara","labelrank":"7","country-abbrev":"W. Sah.","subregion":"Northern Africa","region-wb":"Middle East & North Africa","iso-a3":"ESH","iso-a2":"EH","woe-id":"23424990","continent":"Africa","hc-middle-lon":-10.357,"hc-middle-lat":26.176}},{"type":"Polygon","arcs":[[173,174,175,176,-16]],"id":"BJ","properties":{"hc-group":"admin0","hc-key":"bj","hc-a2":"BJ","name":"Benin","labelrank":"5","country-abbrev":"Benin","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"BEN","iso-a2":"BJ","woe-id":"23424764","continent":"Africa","hc-middle-lon":2.245,"hc-middle-lat":10.744}},{"type":"Polygon","arcs":[[-176,177,-137,-132,178,179]],"id":"BF","properties":{"hc-group":"admin0","hc-key":"bf","hc-a2":"BF","name":"Burkina Faso","labelrank":"3","country-abbrev":"B.F.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"BFA","iso-a2":"BF","woe-id":"23424978","continent":"Africa","hc-middle-lon":-1.229,"hc-middle-lat":12.754}},{"type":"Polygon","arcs":[[180,-138,-178,-175]],"id":"TG","properties":{"hc-group":"admin0","hc-key":"tg","hc-a2":"TG","name":"Togo","labelrank":"6","country-abbrev":"Togo","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"TGO","iso-a2":"TG","woe-id":"23424965","continent":"Africa","hc-middle-lon":1.284,"hc-middle-lat":6.552}},{"type":"Polygon","arcs":[[-177,-180,181,-102,182,183,-17]],"id":"NE","properties":{"hc-group":"admin0","hc-key":"ne","hc-a2":"NE","name":"Niger","labelrank":"3","country-abbrev":"Niger","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"NER","iso-a2":"NE","woe-id":"23424906","continent":"Africa","hc-middle-lon":10.115,"hc-middle-lat":18.047}},{"type":"Polygon","arcs":[[-183,-101,-61,184,185,186,187]],"id":"LY","properties":{"hc-group":"admin0","hc-key":"ly","hc-a2":"LY","name":"Libya","labelrank":"3","country-abbrev":"Libya","subregion":"Northern Africa","region-wb":"Middle East & North Africa","iso-a3":"LBY","iso-a2":"LY","woe-id":"23424882","continent":"Africa","hc-middle-lon":17.366,"hc-middle-lat":26.958}},{"type":"Polygon","arcs":[[-135,188,-46,189]],"id":"LR","properties":{"hc-group":"admin0","hc-key":"lr","hc-a2":"LR","name":"Liberia","labelrank":"4","country-abbrev":"Liberia","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"LBR","iso-a2":"LR","woe-id":"23424876","continent":"Africa","hc-middle-lon":-9.965,"hc-middle-lat":6.289}},{"type":"MultiPolygon","arcs":[[[26,-27]],[[28,-29]],[[30,-31,190]],[[191,-33]],[[-35,192,193,-146]]],"id":"MW","properties":{"hc-group":"admin0","hc-key":"mw","hc-a2":"MW","name":"Malawi","labelrank":"6","country-abbrev":"Mal.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"MWI","iso-a2":"MW","woe-id":"23424889","continent":"Africa","hc-middle-lon":33.498,"hc-middle-lat":-11.982}},{"type":"Polygon","arcs":[[194,-118]],"id":"GM","properties":{"hc-group":"admin0","hc-key":"gm","hc-a2":"GM","name":"Gambia","labelrank":"6","country-abbrev":"Gambia","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"GMB","iso-a2":"GM","woe-id":"23424821","continent":"Africa","hc-middle-lon":-16.051,"hc-middle-lat":13.422}},{"type":"Polygon","arcs":[[195,-167,196,-165,-18,-184,-188,197,198,-160]],"id":"TD","properties":{"hc-group":"admin0","hc-key":"td","hc-a2":"TD","name":"Chad","labelrank":"3","country-abbrev":"Chad","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"TCD","iso-a2":"TD","woe-id":"23424777","continent":"Africa","hc-middle-lon":18.722,"hc-middle-lat":15.616}},{"type":"Polygon","arcs":[[-110,-163,-171,199]],"id":"GA","properties":{"hc-group":"admin0","hc-key":"ga","hc-a2":"GA","name":"Gabon","labelrank":"4","country-abbrev":"Gabon","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"GAB","iso-a2":"GA","woe-id":"23424822","continent":"Africa","hc-middle-lon":12.002,"hc-middle-lat":-0.634}},{"type":"Polygon","arcs":[[-122,200,-127,-105,201,-156]],"id":"DJ","properties":{"hc-group":"admin0","hc-key":"dj","hc-a2":"DJ","name":"Djibouti","labelrank":"5","country-abbrev":"Dji.","subregion":"Eastern Africa","region-wb":"Middle East & North Africa","iso-a3":"DJI","iso-a2":"DJ","woe-id":"23424797","continent":"Africa","hc-middle-lon":43.049,"hc-middle-lat":12.336}},{"type":"Polygon","arcs":[[202,-77,-153,-38]],"id":"BI","properties":{"hc-group":"admin0","hc-key":"bi","hc-a2":"BI","name":"Burundi","labelrank":"6","country-abbrev":"Bur.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"BDI","iso-a2":"BI","woe-id":"23424774","continent":"Africa","hc-middle-lon":29.379,"hc-middle-lat":-3.579}},{"type":"MultiPolygon","arcs":[[[203,-169,-86]],[[204,-82,-143,-152,205,-84]]],"id":"AO","properties":{"hc-group":"admin0","hc-key":"ao","hc-a2":"AO","name":"Angola","labelrank":"3","country-abbrev":"Ang.","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"AGO","iso-a2":"AO","woe-id":"23424745","continent":"Africa","hc-middle-lon":17.916,"hc-middle-lat":-13.06}},{"type":"Polygon","arcs":[[206,-52,-116,207,-130,-190,-48]],"id":"GN","properties":{"hc-group":"admin0","hc-key":"gn","hc-a2":"GN","name":"Guinea","labelrank":"3","country-abbrev":"Gin.","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"GIN","iso-a2":"GN","woe-id":"23424835","continent":"Africa","hc-middle-lon":-12.22,"hc-middle-lat":11.146}},{"type":"Polygon","arcs":[[-141,208,-148,209,210,211]],"id":"ZW","properties":{"hc-group":"admin0","hc-key":"zw","hc-a2":"ZW","name":"Zimbabwe","labelrank":"3","country-abbrev":"Zimb.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"ZWE","iso-a2":"ZW","woe-id":"23425004","continent":"Africa","hc-middle-lon":30.147,"hc-middle-lat":-19.176}},{"type":"Polygon","arcs":[[212,213,-150,214,-211,215,216],[217]],"id":"ZA","properties":{"hc-group":"admin0","hc-key":"za","hc-a2":"ZA","name":"South Africa","labelrank":"2","country-abbrev":"S.Af.","subregion":"Southern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"ZAF","iso-a2":"ZA","woe-id":"23424942","continent":"Africa","hc-middle-lon":23.711,"hc-middle-lat":-29.796}},{"type":"Polygon","arcs":[[-213,218,-216,-210,-147,-194,219,-44,220]],"id":"MZ","properties":{"hc-group":"admin0","hc-key":"mz","hc-a2":"MZ","name":"Mozambique","labelrank":"3","country-abbrev":"Moz.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"MOZ","iso-a2":"MZ","woe-id":"23424902","continent":"Africa","hc-middle-lon":37.969,"hc-middle-lat":-14.541}},{"type":"Polygon","arcs":[[-219,-217]],"id":"SZ","properties":{"hc-group":"admin0","hc-key":"sz","hc-a2":"SZ","name":"Eswatini","labelrank":"4","country-abbrev":"Swz.","subregion":"Southern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"SWZ","iso-a2":"SZ","woe-id":"23424993","continent":"Africa","hc-middle-lon":31.474,"hc-middle-lat":-26.5}},{"type":"Polygon","arcs":[[-103,-182,-179,-131,-208,-115,-93]],"id":"ML","properties":{"hc-group":"admin0","hc-key":"ml","hc-a2":"ML","name":"Mali","labelrank":"3","country-abbrev":"Mali","subregion":"Western Africa","region-wb":"Sub-Saharan Africa","iso-a3":"MLI","iso-a2":"ML","woe-id":"23424891","continent":"Africa","hc-middle-lon":-2.324,"hc-middle-lat":18.881}},{"type":"Polygon","arcs":[[-149,-212,-215]],"id":"BW","properties":{"hc-group":"admin0","hc-key":"bw","hc-a2":"BW","name":"Botswana","labelrank":"4","country-abbrev":"Bwa.","subregion":"Southern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"BWA","iso-a2":"BW","woe-id":"23424755","continent":"Africa","hc-middle-lon":24.194,"hc-middle-lat":-22.412}},{"type":"Polygon","arcs":[[221,-107,-126,222,223,-198,-187,224]],"id":"SD","properties":{"hc-group":"admin0","hc-key":"sd","hc-a2":"SD","name":"Sudan","labelrank":"3","country-abbrev":"Sudan","subregion":"Northern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"SDN","iso-a2":"SD","woe-id":"-90","continent":"Africa","hc-middle-lon":29.942,"hc-middle-lat":16.13}},{"type":"Polygon","arcs":[[-172,225,-99]],"id":"MA","properties":{"hc-group":"admin0","hc-key":"ma","hc-a2":"MA","name":"Morocco","labelrank":"3","country-abbrev":"Mor.","subregion":"Northern Africa","region-wb":"Middle East & North Africa","iso-a3":"MAR","iso-a2":"MA","woe-id":"23424893","continent":"Africa","hc-middle-lon":-4.273,"hc-middle-lat":33.737}},{"type":"Polygon","arcs":[[-225,-186,226]],"id":"EG","properties":{"hc-group":"admin0","hc-key":"eg","hc-a2":"EG","name":"Egypt","labelrank":"2","country-abbrev":"Egypt","subregion":"Northern Africa","region-wb":"Middle East & North Africa","iso-a3":"EGY","iso-a2":"EG","woe-id":"23424802","continent":"Africa","hc-middle-lon":29.311,"hc-middle-lat":26.247}},{"type":"Polygon","arcs":[[-218]],"id":"LS","properties":{"hc-group":"admin0","hc-key":"ls","hc-a2":"LS","name":"Lesotho","labelrank":"6","country-abbrev":"Les.","subregion":"Southern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"LSO","iso-a2":"LS","woe-id":"23424880","continent":"Africa","hc-middle-lon":28.225,"hc-middle-lat":-29.578}},{"type":"Polygon","arcs":[[-9,-89,227,-223,-125,-67]],"id":"SS","properties":{"hc-group":"admin0","hc-key":"ss","hc-a2":"SS","name":"South Sudan","labelrank":"3","country-abbrev":"S. Sud.","subregion":"Eastern Africa","region-wb":"Sub-Saharan Africa","iso-a3":"SSD","iso-a2":"SS","woe-id":"-99","continent":"Africa","hc-middle-lon":30.135,"hc-middle-lat":7.962}},{"type":"Polygon","arcs":[[-228,-88,-168,-161,-199,-224]],"id":"CF","properties":{"hc-group":"admin0","hc-key":"cf","hc-a2":"CF","name":"Central African Republic","labelrank":"4","country-abbrev":"C.A.R.","subregion":"Middle Africa","region-wb":"Sub-Saharan Africa","iso-a3":"CAF","iso-a2":"CF","woe-id":"23424792","continent":"Africa","hc-middle-lon":21.052,"hc-middle-lat":7.077}}],"hc-recommended-transform":{"default":{"crs":"+proj=lcc +lat_1=20 +lat_2=-23 +lat_0=0 +lon_0=25 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs","scale":0.0000812423194296,"jsonres":15.5,"jsonmarginX":-999,"jsonmarginY":9851,"xoffset":-5257539.5112,"yoffset":4185728.66873}},"hc-recommended-mapview":{"projection":{"name":"LambertConformalConic","parallels":[20,-23],"rotation":[-25]}}}},"arcs":[[[694632,477479],[-1291,-433],[-1203,1171],[1661,221],[833,-959]],[[704575,484577],[639,2141],[1568,7],[-451,-3102],[-1756,954]],[[688196,468685],[0,214],[185,-213]],[[688381,468686],[-185,-1]],[[661874,475875],[3129,4713],[-2493,635]],[[662510,481223],[3029,8239],[-193,3954],[6627,6323]],[[671973,499739],[1206,-2988],[4410,6850],[5428,4823],[-1486,4265]],[[681531,512689],[-6830,4033],[1829,5979],[-1295,1915],[1091,6291]],[[676326,530907],[3674,4166],[4334,-1796],[3310,2142],[4801,-4029],[2387,2885],[7730,2056],[6176,-1463],[5411,6200]],[[714149,541068],[2234,-6270],[3321,-1368],[-722,-3945],[2327,-6064],[4172,-6484],[690,-11204],[-2188,-6099],[-3497,-1835],[-6136,-11786]],[[714350,486013],[-5164,-880],[-3607,2763],[-3124,-3756],[-4984,407],[-8108,-3881],[1206,-2348],[-3857,-6858],[1023,-2777]],[[687735,468683],[-11539,-39],[-4336,-869]],[[671860,467775],[-6913,-5581],[-3787,1060]],[[661160,463254],[-289,6523],[1003,6098]],[[393344,544587],[2761,-239],[-5254,-801],[1023,1479],[1470,-439]],[[337506,570880],[819,9093],[-1271,11951],[1250,16204],[3868,900],[405,4197],[4644,5822],[697,6068],[3252,4356],[-1407,7118],[-2903,3892],[1322,4151]],[[348182,644632],[653,11631],[5285,6469],[516,6612],[4519,3047],[9266,830],[3405,1615],[9783,-3399],[6445,-8537],[5879,1393],[4423,2958],[3850,-242],[7207,-5339],[11639,-1340],[3421,4423],[9007,3239],[9168,-171],[7500,-3465],[4948,-757],[1113,2475],[9568,6549],[3025,-122]],[[468802,672501],[5461,-8711]],[[474263,663790],[1424,-9542],[5858,-3044],[-865,-9254],[-5156,-3619],[-2200,534],[-4995,-8321],[-1595,-7209],[-2301,-1033],[-587,-7441],[-4053,-2283],[-681,-7563],[-6924,-6164],[-398,-5795],[-5569,-9780],[1828,-2233],[-3880,-3145],[-2885,-6118],[-8689,8638],[-1302,-2529],[-4497,1623],[-15187,-15867],[-707,-9546],[-2517,-4760]],[[408385,549339],[-4316,1633],[515,-5426],[-6632,-471],[-4963,1323],[-3519,-2837],[-10050,-1709],[-7144,4967],[-3052,9808],[-5448,9183],[-5847,4940],[-6432,919],[-13991,-789]],[[384604,483645],[-1830,1943],[2141,2651],[1464,-1725],[-1775,-2869]],[[783580,371685],[-1487,838],[1101,1710],[2217,868],[-1831,-3416]],[[777295,401338],[1099,1929],[2374,-10323],[-4004,4232],[531,4162]],[[782443,407032],[416,7688],[1579,-1055],[486,-3946],[-2481,-2687]],[[701623,455691],[2033,-419],[748,-2777],[-3885,1801],[1104,1395]],[[725991,322106],[-4,1053],[-561,1156]],[[725426,324315],[-187,209],[-93,105]],[[725146,324629],[-1117,837],[-745,839]],[[723284,326305],[-94,211],[-94,315]],[[723096,326831],[-2852,17830],[-2422,3268]],[[717822,347929],[-1118,1476]],[[716704,349405],[-372,210],[-838,949]],[[715494,350564],[-93,106],[-92,-1]],[[715309,350669],[-1574,-537],[-362,-2331]],[[713373,347801],[-6123,1560],[-5848,2728]],[[701402,352089],[-6499,3893],[-4821,830],[-5295,5071],[-4543,1576]],[[680244,363459],[-6983,15064],[-202,7555],[-5383,7014],[-4911,2864],[2767,4591],[-2418,6713],[1009,4272],[-2781,3088],[728,6195]],[[662070,420815],[4156,2468],[5616,10595],[3966,3325],[360,4064],[-4901,1803],[1650,6206]],[[672917,449276],[3323,759],[-24,10374],[-4356,7366]],[[687735,468683],[-90,-749],[551,751]],[[688381,468686],[375,-2031],[-3106,-12417],[3709,-5227],[-1098,-3853],[4786,6220],[1763,-2560],[10164,-1133],[7468,4098],[-3151,3194],[899,7063],[5056,5906]],[[715246,467946],[43068,-27658],[-615,-6418],[19225,-16207]],[[776924,417663],[-614,-5876],[-4368,-12835],[858,-4364],[7445,-7080],[947,-3612],[-3294,-6621],[2163,-5724],[-2106,-4159],[960,-5618],[3467,-6653],[1909,-9092],[-1104,-1384],[8841,-5532],[-81,-1797]],[[791947,337316],[-5164,-4581],[-8689,-4814],[-4730,-141],[-4897,-3408],[-2793,1877],[-4635,-560],[-1095,-3377],[-4256,-2239],[-7339,2057],[-3051,-2229],[-4455,394],[-4472,4080],[-2957,-2649],[-7423,380]],[[153212,588026],[875,-2869],[-995,1076],[-4211,1248],[4331,545]],[[181064,600136],[-986,-4617],[-3062,-1946],[43,-3377],[-9140,-8697],[-1240,-3043]],[[166679,578456],[-4368,3154],[-8043,3332],[1229,2193],[-6634,5096],[-202,4750],[-2224,-1114],[-1742,12038]],[[144695,607905],[3321,773],[3927,4438],[2300,6144],[7193,1849],[7715,-47],[7209,-9476],[2353,-8773],[-3012,-4892],[5363,2215]],[[110102,638070],[3022,-483],[524,-1577],[-4043,-224],[497,2284]],[[123998,634535],[-4722,3128],[-757,4186],[4889,1458],[-1890,2227],[-5199,-3533],[3594,6376],[-7211,-3173],[-9310,8313]],[[103392,653517],[5992,1633],[6695,-145],[5835,3298],[17612,-57]],[[139526,658246],[277,-5609],[-3145,-1598],[2993,-2140],[-64,-4056],[-6618,-592],[-4900,-2189],[-4071,-7527]],[[11797,688134],[-2265,1289],[1500,1602],[1252,-1468],[-487,-1423]],[[18992,694918],[3578,-4291],[-576,-1319],[-3356,1726],[354,3884]],[[25773,694950],[783,-2067],[-841,-799],[-899,843],[957,2023]],[[29685,707207],[2004,-560],[-1425,-2512],[-1731,451],[1152,2621]],[[12317,713241],[722,-527],[-1134,-1699],[-1139,2469],[1551,-243]],[[2681,720895],[1439,-1253],[-3899,-2535],[-221,2540],[2681,1248]],[[972866,418428],[842,-733],[-350,-1608],[-667,1591],[175,750]],[[436554,952392],[1533,-1316],[-2010,-1995],[-1883,3152],[2360,159]],[[443475,942465],[-684,-7487],[1437,-2399],[-8385,-4563],[-9153,-10032],[1951,-6848],[-4873,-7837],[-4194,-1695]],[[419574,901604],[-5717,25496],[-8755,6811],[-2755,7432],[-4072,1525],[-3277,9558],[4234,7172],[4835,3246],[1112,13877],[-976,2474],[1997,11307],[2299,4092]],[[408499,994594],[7370,4124],[7798,1281],[4964,-2115],[-1188,-1411],[3135,-4793],[7462,4894],[976,-2933],[-4027,-5772],[-3364,-1581],[1039,-6693],[5415,-3351],[1233,-5217],[-6777,-9589],[-5646,-3077],[-942,-4768],[3420,-3857],[4628,57],[-21,-3059],[3191,2166],[2410,-5971],[3900,-464]],[[886897,298853],[22,-2302],[-1398,506],[-110,1778],[1486,18]],[[852837,128028],[-4296,-140],[-3673,3380],[-5615,1216],[-4047,3583],[-3897,8211],[-834,10551],[1472,2588],[-4829,8694],[-1652,8262],[3441,12761],[3625,1241],[1079,4744],[6970,13414],[48,5380],[-3033,6483],[343,3471],[-2463,5288],[-1318,13417],[5868,10458],[221,7129],[4839,-571],[4985,4191],[4191,-784],[998,2807],[6312,1002],[9045,7053],[-237,-4569],[3335,1283],[-2074,3090],[4854,7648],[241,-5822],[3281,6697],[-495,3224],[4050,3907],[-2083,4152],[1079,3671],[4400,-3605],[1172,3569],[3886,1512],[2517,8306],[-870,3451],[4555,8567],[3621,-9302],[4515,-5812],[2429,-10325],[1068,-13230],[3078,-9831],[-3082,-7296],[-2248,1524],[-1737,6094],[-3142,-1701],[2562,-13936],[-5010,-10164],[799,-5231],[-1659,-10575],[-3445,-8924],[-6213,-20873],[-4302,-16954],[-4158,-13135],[-3485,-15316],[-5359,-15017],[-5663,-3488],[-5042,-151],[-8918,-5267]],[[715246,467946],[1165,9523],[8118,791],[-1304,3094],[-5804,-2489],[-3057,3085],[-14,4063]],[[714149,541068],[4860,5555],[12405,5700],[2418,-5726],[3683,21]],[[737515,546618],[1484,-2437],[9669,-153],[15161,-11394],[4697,141],[12545,-2994],[3739,6420],[11016,5733],[4264,-4434],[9301,190]],[[809391,537690],[-6559,-10818],[-4391,-4943],[56,-51492],[6527,-9675],[103,-1710]],[[805127,459052],[-3022,-3770],[-4798,-684],[480,-2776],[-3666,-4630],[-5340,-2932],[574,-3203],[-3017,-5263],[-2265,-7499],[-4565,-9758],[-2584,-874]],[[655170,455860],[100,-4063],[-923,-537],[-3,1497],[826,3103]],[[681531,512689],[-9381,-9425],[-177,-3525]],[[662510,481223],[-4604,-5571],[744,-2672],[3224,2895]],[[661160,463254],[-2675,-1611],[-1658,-2357]],[[656827,459286],[-1941,1279],[-2389,-7705],[192,-4705],[189,-2352]],[[652878,445803],[1479,-959]],[[654357,444844],[2687,-4485],[-363,-4063]],[[656681,436296],[-1462,-9728],[1398,-6620],[-1282,-7690],[3343,-7462],[-2306,-4272],[3814,-10861],[8257,-9239],[939,-6377],[3717,-5515]],[[673099,368532],[-19908,-3448],[-278,-107]],[[652913,364977],[-6658,-8287],[-177,-6251],[2314,2123]],[[648392,352562],[1862,-5926],[-448,-9937],[-3413,-12874],[1863,-5890],[6321,-6924],[5663,-1037],[-376,2835],[3988,1165],[-58,-17510],[-2698,2714],[-4916,-2837],[-9954,13604],[-8448,2192],[-5484,10088],[-5840,-6213],[-8444,1466],[-7795,3679],[-835,6109],[-11966,-2527],[1114,3688],[-5007,3377]],[[593521,331804],[-1578,-2109],[-4080,847],[-5752,-1893],[-5749,956],[-3620,-2947],[-922,5485],[1863,6649],[-1848,6134],[-3702,4345],[948,16643],[-2123,5526],[291,10101],[-15184,31],[1121,5106],[-3890,-1161],[-9257,25],[-108,-6703],[-1947,-1270],[79,-5953],[-14263,47],[-7597,-1139]],[[516203,370524],[-9383,19081]],[[506820,389605],[-2000,10772],[-3419,976],[-19431,-327],[-18318,322]],[[463652,401348],[-8803,-2714],[-2847,4071]],[[452002,402705],[3518,510],[-693,9287],[7520,5823]],[[462347,418325],[3410,-2264],[4261,1575],[7977,5613],[333,-8438],[5183,1038],[4465,6597],[4454,4144],[3795,1263],[4099,9064],[-626,5670],[684,10476],[3522,3726],[3814,8218],[5549,3399],[5375,6929],[-356,4492],[2795,9290],[-993,7485],[2511,7468],[-69,8434],[6579,13416],[105,4795]],[[529214,530715],[294,7028],[-1370,4475],[2213,1056],[4437,7322],[3874,3069],[3774,95],[7446,-5014],[3121,-5112],[3316,631],[4048,-2243],[6170,94],[8469,-2248],[5718,9565],[5889,-3301],[13168,7113],[3682,-2444],[6259,1488],[644,3824],[5615,-1592],[3589,958],[4052,-2759],[4879,-314],[3035,2234],[3868,-1801]],[[635404,552839],[3965,-6584],[7648,-4455],[4230,3838],[4977,-2861],[3399,4368],[9044,-9871],[3870,-1160],[193,-3514],[3596,-1693]],[[973309,193254],[4341,-3962],[-435,-2320],[-5603,718],[-1625,3095],[3322,2469]],[[850256,305870],[-1106,-956],[-264,-1784],[-1514,3443],[2884,-703]],[[200368,860797],[38074,-25816],[8398,-5947]],[[246840,829034],[-21335,-61],[7722,-76190],[3944,-41166],[3258,-3001],[-1885,-11294],[-46260,69],[-5797,-1736],[-10891,713],[-2175,-4464],[-5940,6968],[-3769,-848],[-1145,-8886],[-5342,-1743]],[[157225,687395],[-6242,5147],[-2212,4956],[-3272,1598],[-2995,7406],[-3945,-430],[-6385,7049],[-7048,145],[-9012,-2250],[-7877,-137],[-2542,-9217]],[[105695,701662],[1027,11587],[3445,8546],[1726,9762],[-2131,14110],[-3830,5782],[3859,11371],[-3983,7220],[-1571,-956],[-3075,6784],[-1653,-5497]],[[99509,770371],[1148,7873],[47502,26],[-919,16102],[-854,3528],[1797,3762],[4713,3505],[7259,3065],[26,34692],[39823,-23],[364,17896]],[[246840,829034],[0,0]],[[200368,860797],[-10,5235]],[[200358,866032],[-58,13927],[12800,10015],[5756,1653],[16756,1399],[2621,-1228],[3164,5072],[10849,8576],[8766,2867],[1044,3326],[-3112,5080],[1880,4546],[10020,2014],[-1316,3495],[5102,1181],[15250,-722],[2438,6096],[-4593,3062],[-2998,6905],[-25,12051],[-2360,6783],[1296,1968],[-5467,4880]],[[278171,968978],[5539,534],[5940,3611],[939,2465],[10122,4733],[2698,-1729],[3037,3701],[11038,6047],[19136,1658],[3655,2819],[6336,-479],[4368,1887],[11591,-271],[5402,-3318],[11538,3426],[1743,2327],[6861,-2396],[5730,2632],[6311,-3310],[8344,1279]],[[419574,901604],[-2830,-1616],[4587,-6966],[1954,-6709],[-630,-11907],[1964,-5589],[-2604,-7918],[2233,-6209],[-852,-4703],[-4296,-2139],[-934,-3303],[6857,-9930],[679,-7551],[4342,-5210],[4019,1027],[10190,-3914],[4773,-10457]],[[449026,808510],[-54018,-36669],[-19802,-19281],[-19331,-4698]],[[355875,747862],[-11085,-2181],[-2538,2330],[1856,2978],[-492,5989],[-9838,3593],[-2619,3110],[-3480,-654],[-2975,4002],[-5727,2940],[-221,5140],[-71916,53925]],[[787001,702764],[4424,-4355],[-3301,587],[-2301,1718],[1178,2050]],[[824242,658656],[-5285,-4821],[-3680,1522]],[[815277,655357],[-5177,5654],[-2787,5149],[-6270,5111],[-4354,6767],[-8739,5073],[-7157,1382],[-3655,-1774],[-1574,2762],[-7864,-3036],[-6449,6322],[-3885,-10508],[-3324,4710],[-2645,-2382],[-6598,-349]],[[744799,680238],[-1247,11798],[6333,15766],[-843,3861],[1518,7519],[4854,-577],[1260,4058],[8404,2786],[4720,6692]],[[769798,732141],[3984,-8450],[2809,-10622],[867,-7217],[2705,-8144],[3136,-3556],[1812,3287],[4999,-8151],[2924,1151],[2868,-4182],[4678,-783],[6299,-9718],[7370,-4988],[5849,-9872],[4144,-2240]],[[410497,534531],[2293,-1511],[-3265,-6151],[-2574,875],[3546,6787]],[[441231,512532],[188,-16127],[-18445,37]],[[422974,496442],[-5518,2504],[5502,9993],[-54,6084]],[[422904,515023],[2289,-2473],[16038,-18]],[[999999,203991],[-483,-5371],[-3536,-361],[1863,6709],[2156,-977]],[[105435,670867],[-4581,10714],[-2862,4072],[-4282,1430],[4807,2356],[7178,12223]],[[157225,687395],[466,-5351],[3106,-6649],[-1603,-2857],[5887,-4761],[2753,-5950],[-111,-7358]],[[167723,654469],[-8256,57],[-3416,-1386],[-8671,2872],[26,1659],[-7880,575]],[[103392,653517],[-491,3436],[208,6729]],[[103109,663682],[1032,1323],[10087,-117],[130,2272],[7178,1500],[1047,2250],[8998,-4838],[6533,1312],[-1687,3444],[-4338,-1769],[-2888,2847],[-6107,2192],[-5098,-3297],[-12561,66]],[[840621,315008],[404,-4093],[-3737,3113],[2044,-295],[1289,1275]],[[828355,317197],[-2424,1554],[232,6109],[1396,-619],[796,-7044]],[[808144,638962],[-1276,-1367],[1294,-1345]],[[808162,636250],[2034,-2069],[7972,2056],[3680,-1217]],[[821848,635020],[-3268,-5045],[2247,-5942],[6971,-11047],[7304,-5915],[35591,-13720],[12042,40]],[[882735,593391],[-18664,-20833],[-17944,-21950],[-11697,620],[-10187,-4247],[-4015,-5039],[-7819,-1138],[-3018,-3114]],[[737515,546618],[-1666,2010],[335,7546],[-5893,497],[-4077,6875],[-2988,11005],[-5261,4628],[-7023,9477],[-8006,2075],[1535,8335],[9557,571],[1736,2535],[-502,11456]],[[715262,613628],[3080,11232],[-566,4182],[3655,4302],[4407,-81],[1142,12412],[2011,1779],[4914,9374],[5771,1692],[1607,9220],[1821,2903],[1695,9595]],[[815277,655357],[-7525,-12855],[392,-3540]],[[267292,553164],[-1654,237],[1749,-26]],[[267387,553375],[-95,-211]],[[202672,587292],[3125,-162],[2498,7976],[-2696,4579],[6647,2199],[-3184,2372],[1128,8704],[-2483,-166],[-1275,7576],[2245,2999]],[[208677,623369],[3910,3909],[8494,-3496],[3150,2145],[332,4283],[3013,-1306],[2141,2265],[15,-5962],[2737,-1407],[2519,3099],[3399,153]],[[238387,627052],[3656,-1419],[3051,-5810],[3452,-3096],[12261,3688],[5505,-503],[5337,-7214],[842,1352]],[[272491,614050],[-987,-5973],[1818,-1814],[1358,-9918],[-4014,-5424],[-2416,-10317],[-2648,-6308],[3522,-13833],[2295,-566],[-712,-5935]],[[270707,553962],[-5712,-444],[-8176,1507],[-22402,-2718],[-12772,-5205],[-7514,-4230]],[[214131,542872],[-515,9792],[2394,7073],[-778,3833],[-4301,1773],[-1325,4157],[-8515,3013],[3269,3647],[532,5280],[-2220,5852]],[[267387,553375],[1472,-23],[1848,610]],[[272491,614050],[-2743,17114],[2061,3625],[24696,171],[6446,1895]],[[302951,636855],[2193,-968],[-1255,-5937],[5827,-4889],[-2027,-9721],[3650,-2462],[-1933,-9436],[3983,-6054],[-1574,-1139],[891,-12149],[-1540,-6853],[2979,-5756],[5017,-4410]],[[319162,567081],[-2243,-3576],[-8107,-848],[-8415,-3919],[-5102,-3858],[-9327,-2736],[-6211,-3949],[-3194,2598],[-9271,2371]],[[651327,253401],[-3906,-214],[-5202,-4238],[-2790,99],[-5479,-8037],[-3622,-7394]],[[630328,233617],[-3721,-1541],[-6143,2558],[-2698,-1539],[-3537,2561],[-5118,204]],[[609111,235860],[-3629,3388],[-8838,1029],[-10142,-2253]],[[586502,238024],[-7530,6689],[-7803,8463],[-1481,5272],[-38,43835],[24322,79],[-1669,2935],[2044,5560],[-831,10619],[1208,3159],[-1203,7169]],[[648392,352562],[5922,5416],[1660,4353],[-3061,2646]],[[673099,368532],[-1195,-3932],[3892,-731],[2787,-3280],[1661,2870]],[[701402,352089],[5399,-6752],[3648,-9390],[-4254,-3397],[851,-4742],[-1936,-3486],[313,-10099],[1410,-5245],[-5002,-2854],[485,-6390],[-2298,-7123],[2338,-4794],[2413,428]],[[704769,288245],[-36007,-13357],[2251,-9129]],[[671013,265759],[-6786,293],[-5110,-1360],[-6592,-4571],[-1198,-6720]],[[609111,235860],[-3629,103],[-5397,-3793],[-3909,411],[-7169,-6136],[-3346,6451],[-4001,-99],[-18059,-3865],[-6050,-296],[98,-50518],[-12122,-573],[9,-38064]],[[545536,139481],[72,-50479],[-5056,-1986],[-5817,-5868],[-4019,1622],[-7021,-831],[-9161,2687],[-725,6664],[-5417,2024],[-1138,-4001],[-3753,-2839]],[[503501,86474],[-1683,197],[-8010,8423],[-4731,8745],[-3959,13627],[-1458,7973],[-780,13296],[-3693,8968],[737,18022],[-1736,7985],[-5282,6833],[-1376,4623],[-5363,8786],[-2841,9213],[-10417,20198],[-5263,7704],[-1629,8217],[582,4108]],[[446599,243392],[3170,1419],[6319,-1174],[7369,3970],[9365,-6334],[3164,291],[51073,-24],[3801,-5046],[15348,-1993],[3630,504],[5672,-2371],[7166,293],[23826,5097]],[[672917,449276],[-7485,1155],[-360,-4599],[-2490,-2252],[-8225,1264]],[[652878,445803],[-189,2459],[5901,6109],[-1763,4915]],[[894335,638436],[-31,-24906],[-11569,-20139]],[[821848,635020],[3805,6815]],[[825653,641835],[7140,-9528],[5363,-4754],[8269,-645],[10138,6382],[7733,-2525],[11048,6707],[9642,-405],[5941,2577],[3408,-1208]],[[894335,638436],[7679,3018],[8334,1777],[6365,5498],[5987,-2102],[-2564,-8995],[1103,-8653],[2676,-1217],[-5671,-2067],[-1066,-12070],[-4966,-8265],[-1601,-5179],[-5441,-7240],[-245,-3064],[-5418,-9897],[-4019,-12229],[-4695,-9288],[-8428,-14133],[-15940,-19895],[-7210,-8083],[-17784,-12152],[-12996,-13061],[-16738,-20583],[-6570,-11504]],[[477289,663877],[-562,-2689],[2849,1227]],[[479576,662415],[3832,-4683],[980,-6855],[1740,-841],[1070,-7600],[-1397,-4892],[528,-5431],[2912,-6603],[4574,-4421],[-11484,-988],[-6609,771],[-2868,-4702],[4839,-6537],[10068,-9523],[3625,-13307]],[[491386,586803],[-3142,-3788],[-6034,-13724],[-3878,-3264],[2202,-1391],[1581,-18061],[3486,-2998],[1991,-8951],[11100,-12418],[1259,-8647]],[[499951,513561],[-394,-6937],[-11139,4113],[-4150,-404],[-3952,2797],[-3598,-727],[-11706,72]],[[465012,512475],[-637,1605],[-22764,370],[-380,-1918]],[[422904,515023],[1905,10224],[-3738,6318],[-332,5860],[-3687,-395],[-4129,2378],[-419,6280],[-5067,-380],[948,4031]],[[474263,663790],[3026,87]],[[478023,663872],[91,-207],[-366,209]],[[477748,663874],[275,-2]],[[499951,513561],[3439,8305],[1043,8736],[10789,3362],[4598,-2362],[7557,1250],[1837,-2137]],[[462347,418325],[-2666,3221],[-5562,-2846],[-4561,-5734]],[[449558,412966],[-2283,5568],[-8546,9463]],[[438729,427997],[4470,5952],[2020,-2366],[3540,5212],[-3132,1840],[-1509,11344],[6092,-899],[4716,1144],[-431,5992],[4616,-245],[2002,-6216],[5999,-1001],[3347,4685],[4039,-5585],[4583,13234],[-800,7278],[1223,5447],[-4502,4517],[-3501,1411],[487,5557],[7146,9897],[-3384,6537],[-4700,669],[-6560,-2950],[-1265,5455],[1787,7569]],[[146278,866312],[54080,-280]],[[99509,770371],[-530,1001],[1060,8067],[549,5003],[3139,6892],[2201,540],[2795,8137],[6354,13250],[10330,11439],[1465,10582],[4485,11303],[9489,5954],[5432,13773]],[[337506,570880],[-9856,-1363],[-3228,-807]],[[324422,568710],[1940,823],[-2615,5541],[778,4434],[-372,28478],[-2081,3284],[-1099,9875],[-6837,5114],[1647,8654]],[[315783,634913],[2578,802],[3913,5894],[6876,-710],[4559,6608]],[[333709,647507],[-326,4369],[5803,2527],[8996,-9771]],[[315783,634913],[-5048,170],[-7784,1772]],[[238387,627052],[203,8775],[3459,5054],[-754,5740],[8679,4332],[2082,5263],[-1160,2610],[3319,1399],[-1692,4167],[4553,4892],[6185,-4228],[2496,1720],[-206,5890],[4482,-892],[705,5454],[4530,3331],[5303,-1208],[686,4002],[3210,160],[7838,4410],[3520,3541],[6229,-84],[5476,-2122]],[[307530,689258],[-786,-5019],[5652,-12017],[3928,-1392],[296,-7135],[6822,-5782],[6886,745],[1615,-3857],[-2126,-1635],[3892,-5659]],[[324422,568710],[-2305,-501],[-2955,-1128]],[[307530,689258],[9082,1115],[4346,4042],[26210,1021],[4350,4960],[3749,9742],[619,8016],[-11,29708]],[[449026,808510],[18223,-4668],[9089,-7739],[8968,5212]],[[485306,801315],[2313,-13874],[153,-6775],[5175,-7714],[-831,-2071],[4910,-5866],[-2584,-6544],[-3184,-41382],[-13254,-16211],[-6458,-10089],[-845,-4201],[-3779,-4703],[1880,-9384]],[[443475,942465],[10137,-4814],[4627,-467],[7453,1458],[9973,-2521],[3527,-2759],[8708,-1813],[3586,-10079],[6700,-5406],[8175,-661],[7717,-1918],[9709,-4180],[9524,-7002],[4546,260],[4916,2890],[3465,4332],[1285,5492],[-2710,6908],[1832,6414],[5915,5309],[12629,5099],[6446,255],[11343,-4160],[87,-4517],[11985,-4365],[10717,-537],[2090,-4285]],[[607857,921398],[-3544,-3852],[1636,-8281],[-3728,-8882],[3546,-13347],[-1,-99593]],[[605766,787443],[0,-27644],[-12064,-96],[-3,-6873]],[[593699,752830],[-96231,54737],[-12162,-6252]],[[214131,542872],[-8706,3134],[-12230,7877],[-13511,14261],[-11486,7319],[-1519,2993]],[[181064,600136],[5703,314],[3737,-2281],[1791,-9949],[-1417,-2614],[4092,-3352],[3234,1104],[2104,5877],[2364,-1943]],[[717822,347929],[0,0]],[[715494,350564],[-185,105]],[[713373,347801],[570,-3911],[3732,-6113],[-732,-2538],[1262,-14752],[-3407,-6851],[3663,-11419],[-165,-5341],[3731,-4268],[-1285,-3871],[2153,-4058],[1935,3766],[4575,-5504],[-2269,9276],[-2427,3018]],[[724709,295235],[2704,-2599],[9254,-13404],[-803,-7284],[144,-11301],[-5012,-1889],[-2952,-5493],[1777,-2261],[119,-5873],[-2512,90],[-948,4219],[-7475,8739],[-1783,4130],[4056,8523],[-875,9666],[-1868,2596],[-7519,-1391],[-923,-1566],[-5324,8108]],[[103109,663682],[-852,3742],[3178,3443]],[[479576,662415],[3215,1328],[-2832,2497],[-1936,-2368]],[[477748,663874],[-914,729],[455,-726]],[[593699,752830],[68,-52304],[-10715,-298],[-2017,-1939],[454,-4705],[-3580,-7480],[-3667,-1946],[2010,-5865],[-5506,-4836],[2377,-6094],[-4039,-3820],[-8,-5800],[3304,1446],[4852,-9243],[-923,-5512],[4584,-3963],[-648,-6571]],[[580245,633900],[-4864,1154],[-8819,-5105],[272,-3033],[-8830,-9305],[-2304,-4507],[-4692,-3875],[-16083,-1742],[-2761,-2094],[3026,-2641],[-6364,-8617],[-10941,-909],[-11051,-5969],[-2651,4339],[-2124,-2734],[-6907,-2923],[-3766,864]],[[438729,427997],[-5600,6669],[-173,1926],[-8820,8516],[-7595,11399],[1062,7371],[-3966,676],[-3997,9874],[4142,-1747],[2703,4147],[526,10153],[2288,-2906],[3876,74],[-3860,2384],[-2184,4722],[3872,-353],[-892,5030],[2863,510]],[[808162,636250],[1094,1158],[-1112,1554]],[[824242,658656],[2499,-2983],[601,-6745],[-7493,-4129],[-169,-1979],[5973,-985]],[[662070,420815],[-3807,9607],[-1582,5874]],[[452002,402705],[204,3839],[-2648,6422]],[[506820,389605],[3859,-8852],[5524,-10229]],[[446599,243392],[711,10715],[-878,9418],[2896,2571],[3410,12027],[699,9061],[2060,3327],[123,5748],[5232,6352],[-82,2307],[5121,3114],[3271,4705],[1886,6612],[599,9159],[-1083,6442],[-6706,13044],[-2461,8490],[4656,4742],[-157,5945],[-6226,14709],[-253,5004],[-2487,2359],[-4214,8553],[5649,921],[5287,2631]],[[144695,607905],[-3515,10153],[-10463,6191],[-1823,6525],[-4896,3761]],[[167723,654469],[-1419,-2880],[3816,-2464],[3433,2532],[2322,-4306],[5389,4576],[6568,-2720],[4927,4481],[3861,553],[2385,-6589],[-692,-3526],[5727,-3957],[-2891,-3286],[3647,-1839],[188,-6377],[3693,-5298]],[[630328,233617],[8084,10486],[4276,1653],[833,3298],[7254,528],[3436,2689],[-2884,1130]],[[671013,265759],[11,-4972],[10413,-382],[7826,-5556],[4557,-397],[8194,-3167],[-1756,-3513],[2345,-5857],[-999,-7192],[1139,-6346],[-1748,-6240],[-2325,-622],[2061,-3967],[-1194,-4788],[3181,-4456],[-267,-3449],[-4354,-7301],[-1584,-107],[-1272,-9981],[-13473,-15375]],[[681768,172091],[-5413,1581],[-6526,-717],[-6534,2781],[-4941,-711]],[[658354,175025],[-3360,1893],[-380,3504],[-12124,2987],[-4296,6829],[270,8069],[-5124,94],[-657,5361],[-5871,2426],[-7175,6085],[-2331,8042],[-8567,13699],[372,1846]],[[691662,110453],[2899,-279],[6542,217]],[[701103,110391],[-4236,-18629],[-1947,-5055],[-7560,-5719],[-6808,-8238],[-9596,-17812],[-4576,-6136],[-7482,-6207],[-6265,-7641],[-11704,-10425],[-9091,-6087],[-7596,-3800],[-6755,628],[-3283,-1174],[282,-3152],[-9008,539],[-1220,-2786],[-14732,2791],[-2723,-1797],[-10133,1628],[-10424,-5462],[-9479,374],[-4695,-1245],[-6864,-4986],[-3754,544],[-4398,5103],[-5630,553],[572,2958],[-6475,-71],[2077,4041],[-7737,14522],[5165,2356],[861,5395],[-2129,8192],[-3359,4446],[-7352,13792],[-5739,17609],[-3809,7034]],[[545536,139481],[4660,-3932],[2978,-5482],[2784,-9741],[-2718,-4067],[829,-6478],[12059,461],[12444,12088],[1502,7010],[1963,2342],[5418,386],[5601,-4786],[10556,-2730],[9435,2825],[3451,12130],[6442,1574],[5317,5316],[1860,8394],[9045,5857],[7077,8864],[6899,1807],[1580,3199],[3636,507]],[[681768,172091],[2921,-10543],[-176,-3572],[4035,-7405],[1607,-6598],[-495,-21225]],[[689660,122748],[-6268,2996],[-3730,-2347],[-3997,-7774],[199,-4736],[4315,-5481],[9728,-1508],[258,7033],[1497,-478]],[[644715,63002],[11322,4893],[87,3383],[3267,4531],[-9659,10685],[-2992,-765],[-7667,-3902],[-3923,-6813],[-4864,-3114],[3565,-7040],[5158,-6443],[4029,-833],[1677,5418]],[[691662,110453],[-866,7736],[-1136,4559]],[[724709,295235],[-1239,7950],[823,3150],[-1515,7235],[2127,2008],[1086,6528]],[[791947,337316],[2165,-5049],[-2248,-11085],[1881,-18259],[-589,-9425],[3604,-11450],[-62,-4686],[-2661,-5012],[-251,-4362],[-5166,-6053],[-3118,-6850],[-9354,-6365],[-14658,-6794],[-9918,-7048],[-7302,-9871],[-9832,-7707],[-6484,-7864],[-4646,-2560],[-900,-7497],[3931,-4222],[394,-4836],[2924,-7824],[514,-10604],[2032,4407],[518,-11268],[-1527,-12995],[1231,-3745],[-4730,-6717],[-5496,-2972],[-11096,-4067],[-10251,-6100],[-4278,-5566],[3847,-4743],[1760,4472],[-1078,-12208]],[[749113,787478],[25,-4808],[2767,-5199],[1331,-9870],[-624,-2978],[1915,-16522],[2594,-6193],[1552,411],[4786,-5390],[6339,-4788]],[[715262,613628],[-2021,-10],[-37,10170],[-8662,9058],[-1228,10320],[1531,8639],[-5689,80],[102,-2909],[-7982,-135],[3223,-3941],[762,-9169],[-5675,-5452],[-3289,-6184],[-5219,-5781],[-5876,-858],[-8833,7414],[-4769,-2944],[-1646,-4092],[-5691,-2216],[-1923,-3783],[-9646,88],[-1841,3671],[-9829,92],[-6245,-1161],[-7903,9325],[-736,3139],[-9090,-1781],[-3582,-6809],[-2757,-12707],[-4687,-2733]],[[596024,602959],[-8270,1373],[-88,5362],[1838,1469],[-87,8709],[-4036,7438],[-5136,6590]],[[605766,787443],[75508,20],[67839,15]],[[146278,866312],[2424,3217],[10965,2407],[6882,3220],[5289,6106],[5694,3130],[5194,6068],[5887,9674],[518,3898],[-3301,3857],[438,9990],[5865,8577],[1216,7705],[8873,9558],[12428,5243],[8071,5461],[6440,11670],[4638,12676],[6100,1786],[1833,-4701],[5731,-4774],[4787,-1262],[8459,1861],[3065,-1237],[5373,1645],[1056,-2325],[7968,-784]],[[607857,921398],[2998,-2148],[5087,1524],[18264,-3392],[6728,-3496],[6089,-441],[7458,-3593],[5722,1816],[10341,7662],[2184,-1608],[8170,2620],[3639,-1870],[4812,1090],[-1807,-3145],[3826,-3044],[2899,2617],[3282,-2866],[6079,2451],[6549,-1683],[6622,2723],[7551,-21792],[-2305,-13415],[-2792,-6486],[292,-4456],[-4176,-2532],[-10145,10632],[-1207,6452],[-5025,6041],[-1843,6754],[-2802,-4940],[3104,-3470],[383,-5236],[2931,-5433],[7768,-8594],[-165,-4380],[6054,-11028],[104,-3294],[6429,-12272],[8286,-18404],[5863,-5651],[-2461,-206],[-334,-6671],[2500,-7752],[6042,-3960],[8262,-9044]],[[635404,552839],[-3782,9656],[-7825,4658],[-2302,3919],[1102,3706],[-4233,4759],[-8002,4648],[-1012,4542],[-5332,7067],[-8455,2426],[461,4739]]],"bbox":[-25.316849789525808,-34.81133086771159,57.71386577803557,37.32879358878588],"transform":{"scale":[0.00008303079859835996,0.00007214019659669405],"translate":[-25.316849789525808,-34.81133086771159]},"title":"Africa","version":"2.3.2","copyright":"Copyright (c) 2025 Highsoft AS, Based on data from Natural Earth","copyrightShort":"Natural Earth","copyrightUrl":"http://www.naturalearthdata.com"}
afridatahub-frontend/src/assets/react.svg ADDED
afridatahub-frontend/src/components/APIDocumentation.jsx ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composant APIDocumentation pour AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { useState, useEffect } from 'react'
7
+ import { motion, AnimatePresence } from 'framer-motion'
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ Code,
11
+ Copy,
12
+ RefreshCw,
13
+ Terminal,
14
+ Globe,
15
+ Lock,
16
+ Check,
17
+ ExternalLink,
18
+ BookOpen,
19
+ Cpu,
20
+ Database
21
+ } from 'lucide-react'
22
+ import { API_URL } from '../config'
23
+
24
+ const APIDocumentation = () => {
25
+ const [token, setToken] = useState('')
26
+ const [loading, setLoading] = useState(false)
27
+ const [copied, setCopied] = useState(false)
28
+ const [activeTab, setActiveTab] = useState('python')
29
+
30
+ useEffect(() => {
31
+ fetchToken()
32
+ }, [])
33
+
34
+ const fetchToken = async () => {
35
+ try {
36
+ const storedToken = localStorage.getItem('token')
37
+ const response = await fetch(`${API_URL}auth/token/`, {
38
+ headers: {
39
+ 'Authorization': `Token ${storedToken}`
40
+ }
41
+ })
42
+ const data = await response.json()
43
+ if (response.ok) {
44
+ setToken(data.token)
45
+ }
46
+ } catch (error) {
47
+ console.error('Erreur lors de la récupération du token:', error)
48
+ }
49
+ }
50
+
51
+ const regenerateToken = async () => {
52
+ if (!confirm('Êtes-vous sûr de vouloir régénérer votre token ? L\'ancien token ne fonctionnera plus.')) return
53
+
54
+ try {
55
+ setLoading(true)
56
+ const storedToken = localStorage.getItem('token')
57
+ const response = await fetch(`${API_URL}auth/token/`, {
58
+ method: 'POST',
59
+ headers: {
60
+ 'Authorization': `Token ${storedToken}`
61
+ }
62
+ })
63
+ const data = await response.json()
64
+ if (response.ok) {
65
+ setToken(data.token)
66
+ }
67
+ } catch (error) {
68
+ console.error('Erreur lors de la régénération du token:', error)
69
+ } finally {
70
+ setLoading(false)
71
+ }
72
+ }
73
+
74
+ const copyToClipboard = (text) => {
75
+ navigator.clipboard.writeText(text)
76
+ setCopied(true)
77
+ setTimeout(() => setCopied(false), 2000)
78
+ }
79
+
80
+ const endpoints = [
81
+ {
82
+ method: 'GET',
83
+ path: '/api/datasets/',
84
+ description: 'Liste tous les datasets publics disponibles.',
85
+ params: [
86
+ { name: 'domain', type: 'string', desc: 'Filtrer par domaine (agriculture, health, etc.)' },
87
+ { name: 'search', type: 'string', desc: 'Recherche textuelle dans le titre ou la description' }
88
+ ]
89
+ },
90
+ {
91
+ method: 'GET',
92
+ path: '/api/datasets/{slug}/data/',
93
+ description: 'Récupère les points de données pour un dataset spécifique.',
94
+ params: [
95
+ { name: 'country', type: 'string', desc: 'Code pays ISO (ex: BJ, NG)' },
96
+ { name: 'limit', type: 'number', desc: 'Nombre maximum de résultats (défaut: 100)' }
97
+ ]
98
+ },
99
+ {
100
+ method: 'GET',
101
+ path: '/api/analytics/comprehensive/',
102
+ description: 'Récupère des analyses consolidées et des statistiques.',
103
+ params: [
104
+ { name: 'dataset_id', type: 'string', desc: 'ID ou Slug du dataset pour filtrer l\'analyse' }
105
+ ]
106
+ }
107
+ ]
108
+
109
+ const codeExamples = {
110
+ python: `import requests
111
+
112
+ url = "http://localhost:8000/api/datasets/"
113
+ headers = {
114
+ "Authorization": "Token ${token || 'VOTRE_TOKEN_ICI'}"
115
+ }
116
+
117
+ response = requests.get(url, headers=headers)
118
+ data = response.json()
119
+
120
+ print(f"Nombre de datasets: {len(data['results'])}")`,
121
+ javascript: `const fetchDatasets = async () => {
122
+ const response = await fetch('http://localhost:8000/api/datasets/', {
123
+ headers: {
124
+ 'Authorization': 'Token ${token || 'VOTRE_TOKEN_ICI'}'
125
+ }
126
+ });
127
+ const data = await response.json();
128
+ console.log(data);
129
+ };`,
130
+ curl: `curl -X GET http://localhost:8000/api/datasets/ \\
131
+ -H "Authorization: Token ${token || 'VOTRE_TOKEN_ICI'}"`
132
+ }
133
+
134
+ return (
135
+ <div className="space-y-10 pb-20">
136
+ {/* En-tête */}
137
+ <motion.div
138
+ initial={{ opacity: 0, y: -20 }}
139
+ animate={{ opacity: 1, y: 0 }}
140
+ className="flex flex-col md:flex-row md:items-end md:justify-between gap-6"
141
+ >
142
+ <div>
143
+ <h1 className="text-4xl font-black text-foreground tracking-tight">API <span className="text-gradient">Developer Hub</span></h1>
144
+ <p className="text-muted-foreground font-medium mt-2 text-lg">
145
+ Intégrez les données d'AfriDataHub directement dans vos applications et flux de travail.
146
+ </p>
147
+ </div>
148
+ <div className="flex items-center space-x-3">
149
+ <Button variant="outline" className="rounded-2xl border-primary/20 hover:bg-primary/5 font-bold">
150
+ <BookOpen className="h-4 w-4 mr-2" />
151
+ Documentation Complète
152
+ </Button>
153
+ </div>
154
+ </motion.div>
155
+
156
+ {/* Section Token */}
157
+ <motion.div
158
+ initial={{ opacity: 0, scale: 0.95 }}
159
+ animate={{ opacity: 1, scale: 1 }}
160
+ className="glass rounded-[3rem] p-10 relative overflow-hidden"
161
+ >
162
+ <div className="absolute top-0 right-0 p-8 opacity-5 pointer-events-none">
163
+ <Lock className="w-48 h-48 text-primary" />
164
+ </div>
165
+
166
+ <div className="relative z-10">
167
+ <div className="flex items-center mb-8">
168
+ <div className="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center mr-4">
169
+ <Cpu className="h-6 w-6 text-primary" />
170
+ </div>
171
+ <div>
172
+ <h2 className="text-2xl font-black text-foreground">Votre Clé API</h2>
173
+ <p className="text-muted-foreground font-medium">Utilisez ce token pour authentifier vos requêtes.</p>
174
+ </div>
175
+ </div>
176
+
177
+ <div className="flex flex-col md:flex-row items-center gap-4">
178
+ <div className="flex-1 w-full relative group">
179
+ <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
180
+ <Terminal className="h-5 w-5 text-muted-foreground" />
181
+ </div>
182
+ <input
183
+ type="text"
184
+ readOnly
185
+ value={token || 'Chargement...'}
186
+ className="w-full pl-12 pr-12 py-5 bg-white/50 border border-white/20 rounded-2xl font-mono text-sm focus:ring-4 focus:ring-primary/10 outline-none transition-all"
187
+ />
188
+ <button
189
+ onClick={() => copyToClipboard(token)}
190
+ className="absolute inset-y-0 right-4 flex items-center text-muted-foreground hover:text-primary transition-colors"
191
+ >
192
+ {copied ? <Check className="h-5 w-5 text-green-500" /> : <Copy className="h-5 w-5" />}
193
+ </button>
194
+ </div>
195
+ <Button
196
+ onClick={regenerateToken}
197
+ disabled={loading}
198
+ className="w-full md:w-auto px-8 py-7 rounded-2xl bg-primary text-white font-bold shadow-lg hover:shadow-primary/20 transition-all hover:scale-105"
199
+ >
200
+ <RefreshCw className={`h-5 w-5 mr-2 ${loading ? 'animate-spin' : ''}`} />
201
+ Régénérer le Token
202
+ </Button>
203
+ </div>
204
+
205
+ <div className="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-2xl flex items-start">
206
+ <div className="p-2 bg-yellow-500/20 rounded-lg mr-4 mt-1">
207
+ <Lock className="h-4 w-4 text-yellow-600" />
208
+ </div>
209
+ <p className="text-sm text-yellow-800 font-medium">
210
+ Gardez votre token secret. Ne le partagez jamais dans des environnements publics ou côté client (frontend).
211
+ </p>
212
+ </div>
213
+ </div>
214
+ </motion.div>
215
+
216
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
217
+ {/* Documentation des Endpoints */}
218
+ <motion.div
219
+ initial={{ opacity: 0, x: -20 }}
220
+ animate={{ opacity: 1, x: 0 }}
221
+ className="space-y-6"
222
+ >
223
+ <div className="flex items-center mb-4">
224
+ <Globe className="h-6 w-6 text-primary mr-3" />
225
+ <h2 className="text-2xl font-black text-foreground">Endpoints Publics</h2>
226
+ </div>
227
+
228
+ {endpoints.map((ep, i) => (
229
+ <div key={i} className="glass rounded-3xl p-6 border border-white/20 hover:border-primary/30 transition-all group">
230
+ <div className="flex items-center justify-between mb-4">
231
+ <div className="flex items-center space-x-3">
232
+ <span className="px-3 py-1 bg-primary text-white text-xs font-black rounded-lg uppercase tracking-widest">
233
+ {ep.method}
234
+ </span>
235
+ <code className="text-sm font-bold text-foreground/80">{ep.path}</code>
236
+ </div>
237
+ <ExternalLink className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
238
+ </div>
239
+ <p className="text-muted-foreground text-sm font-medium mb-4">{ep.description}</p>
240
+
241
+ <div className="space-y-2">
242
+ <p className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Paramètres</p>
243
+ {ep.params.map((param, pi) => (
244
+ <div key={pi} className="flex items-start text-xs">
245
+ <span className="font-bold text-primary mr-2">{param.name}</span>
246
+ <span className="text-muted-foreground/60 mr-2">({param.type})</span>
247
+ <span className="text-foreground/70">{param.desc}</span>
248
+ </div>
249
+ ))}
250
+ </div>
251
+ </div>
252
+ ))}
253
+ </motion.div>
254
+
255
+ {/* Exemples de Code */}
256
+ <motion.div
257
+ initial={{ opacity: 0, x: 20 }}
258
+ animate={{ opacity: 1, x: 0 }}
259
+ className="space-y-6"
260
+ >
261
+ <div className="flex items-center mb-4">
262
+ <Code className="h-6 w-6 text-secondary mr-3" />
263
+ <h2 className="text-2xl font-black text-foreground">Exemples d'Intégration</h2>
264
+ </div>
265
+
266
+ <div className="glass rounded-[2.5rem] overflow-hidden border border-white/20 shadow-2xl">
267
+ <div className="flex border-b border-white/10 bg-white/5">
268
+ {['python', 'javascript', 'curl'].map((tab) => (
269
+ <button
270
+ key={tab}
271
+ onClick={() => setActiveTab(tab)}
272
+ className={`px-8 py-4 text-xs font-black uppercase tracking-widest transition-all ${activeTab === tab
273
+ ? 'bg-primary text-white'
274
+ : 'text-muted-foreground hover:bg-white/10'
275
+ }`}
276
+ >
277
+ {tab}
278
+ </button>
279
+ ))}
280
+ </div>
281
+
282
+ <div className="p-8 bg-black/40 relative group">
283
+ <button
284
+ onClick={() => copyToClipboard(codeExamples[activeTab])}
285
+ className="absolute top-6 right-6 p-2 bg-white/10 hover:bg-white/20 rounded-xl text-white transition-all opacity-0 group-hover:opacity-100"
286
+ >
287
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
288
+ </button>
289
+ <pre className="font-mono text-sm text-blue-100 overflow-x-auto">
290
+ <code>{codeExamples[activeTab]}</code>
291
+ </pre>
292
+ </div>
293
+
294
+ <div className="p-8 bg-white/5 border-t border-white/10">
295
+ <div className="flex items-center justify-between">
296
+ <div className="flex items-center">
297
+ <div className="w-10 h-10 rounded-xl bg-secondary/10 flex items-center justify-center mr-4">
298
+ <Database className="h-5 w-5 text-secondary" />
299
+ </div>
300
+ <div>
301
+ <p className="text-sm font-bold text-foreground">SDK AfriDataHub</p>
302
+ <p className="text-xs text-muted-foreground">Bientôt disponible sur PyPI et NPM.</p>
303
+ </div>
304
+ </div>
305
+ <Button variant="ghost" className="text-xs font-bold text-secondary hover:bg-secondary/10">
306
+ S'inscrire à la Beta
307
+ </Button>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ </motion.div>
312
+ </div>
313
+ </div>
314
+ )
315
+ }
316
+
317
+ export default APIDocumentation
afridatahub-frontend/src/components/AfricaMap.jsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'
3
+ import { scaleLinear } from 'd3-scale'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import africaTopo from '../assets/africa.json'
6
+ import { API_URL } from '../config'
7
+
8
+ const AfricaMap = ({ onCountryClick, data: externalData }) => {
9
+ const [internalData, setInternalData] = useState({})
10
+ const [loading, setLoading] = useState(true)
11
+ const [tooltipContent, setTooltipContent] = useState('')
12
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
13
+
14
+ const data = externalData || internalData
15
+
16
+ useEffect(() => {
17
+ if (!externalData) {
18
+ fetchMapData()
19
+ } else {
20
+ setLoading(false)
21
+ }
22
+ }, [externalData])
23
+
24
+ const fetchMapData = async () => {
25
+ try {
26
+ const response = await fetch(`${API_URL}datasets/map_data/`)
27
+ const result = await response.json()
28
+ setInternalData(result)
29
+ } catch (error) {
30
+ console.error('Erreur lors du chargement des données de la carte:', error)
31
+ } finally {
32
+ setLoading(false)
33
+ }
34
+ }
35
+
36
+ const colorScale = useMemo(() => {
37
+ const values = Object.values(data).map(d => d.value).filter(v => v !== undefined)
38
+ const min = Math.min(...values, 0)
39
+ const max = Math.max(...values, 100)
40
+
41
+ return scaleLinear()
42
+ .domain([min, max])
43
+ .range(["#ede9fe", "#7c3aed"])
44
+ }, [data])
45
+
46
+ const handleMouseEnter = (geo, evt) => {
47
+ const countryCode = geo.properties['iso-a2']
48
+ const countryName = geo.properties.name
49
+ const countryData = data[countryCode]
50
+
51
+ setTooltipContent({
52
+ name: countryName,
53
+ value: countryData ? `${countryData.value.toLocaleString()} ${countryData.unit}` : 'Pas de données',
54
+ source: countryData?.source,
55
+ date: countryData?.date
56
+ })
57
+ setMousePosition({ x: evt.clientX, y: evt.clientY })
58
+ }
59
+
60
+ const handleMouseMove = (evt) => {
61
+ setMousePosition({ x: evt.clientX, y: evt.clientY })
62
+ }
63
+
64
+ const handleMouseLeave = () => {
65
+ setTooltipContent('')
66
+ }
67
+
68
+ return (
69
+ <div className="bg-white rounded-lg border border-gray-200 p-6 relative overflow-hidden">
70
+ <div className="flex items-center justify-between mb-4">
71
+ <h3 className="text-lg font-semibold text-gray-900">
72
+ Carte interactive de l'Afrique
73
+ </h3>
74
+ {loading && <span className="text-sm text-gray-500">Chargement...</span>}
75
+ </div>
76
+
77
+ <div className="h-[600px] w-full bg-slate-50 rounded-xl overflow-hidden relative">
78
+ <ComposableMap
79
+ projection="geoMercator"
80
+ projectionConfig={{
81
+ scale: 400,
82
+ center: [20, 0]
83
+ }}
84
+ className="w-full h-full"
85
+ >
86
+ <ZoomableGroup>
87
+ <Geographies geography={africaTopo}>
88
+ {({ geographies }) =>
89
+ geographies.map((geo) => {
90
+ const countryCode = geo.properties['iso-a2']
91
+ const countryData = data[countryCode]
92
+
93
+ return (
94
+ <Geography
95
+ key={geo.rsmKey}
96
+ geography={geo}
97
+ onMouseEnter={(evt) => handleMouseEnter(geo, evt)}
98
+ onMouseMove={handleMouseMove}
99
+ onMouseLeave={handleMouseLeave}
100
+ onClick={() => {
101
+ if (countryData && onCountryClick) {
102
+ onCountryClick(countryCode, countryData)
103
+ }
104
+ }}
105
+ style={{
106
+ default: {
107
+ fill: countryData ? colorScale(countryData.value) : "#F5F4F6",
108
+ stroke: "#D6D6DA",
109
+ strokeWidth: 0.5,
110
+ outline: "none",
111
+ transition: "all 250ms"
112
+ },
113
+ hover: {
114
+ fill: "#a78bfa",
115
+ stroke: "#7c3aed",
116
+ strokeWidth: 1,
117
+ outline: "none",
118
+ cursor: "pointer"
119
+ },
120
+ pressed: {
121
+ fill: "#7c3aed",
122
+ outline: "none"
123
+ }
124
+ }}
125
+ />
126
+ )
127
+ })
128
+ }
129
+ </Geographies>
130
+ </ZoomableGroup>
131
+ </ComposableMap>
132
+
133
+ {/* Tooltip personnalisé */}
134
+ <AnimatePresence>
135
+ {tooltipContent && (
136
+ <motion.div
137
+ initial={{ opacity: 0, scale: 0.9 }}
138
+ animate={{ opacity: 1, scale: 1 }}
139
+ exit={{ opacity: 0, scale: 0.9 }}
140
+ style={{
141
+ position: 'fixed',
142
+ left: mousePosition.x + 20,
143
+ top: mousePosition.y - 20,
144
+ pointerEvents: 'none',
145
+ zIndex: 50
146
+ }}
147
+ className="bg-white p-4 rounded-lg shadow-xl border border-purple-100 min-w-[200px]"
148
+ >
149
+ <h4 className="font-bold text-gray-900 mb-1">{tooltipContent.name}</h4>
150
+ <div className="text-sm space-y-1">
151
+ <p className="text-purple-600 font-medium">{tooltipContent.value}</p>
152
+ {tooltipContent.source && (
153
+ <p className="text-xs text-gray-500">Source: {tooltipContent.source}</p>
154
+ )}
155
+ {tooltipContent.date && (
156
+ <p className="text-xs text-gray-400">
157
+ {new Date(tooltipContent.date).toLocaleDateString()}
158
+ </p>
159
+ )}
160
+ </div>
161
+ </motion.div>
162
+ )}
163
+ </AnimatePresence>
164
+ </div>
165
+
166
+ <div className="mt-4 flex items-center justify-between text-xs text-gray-500">
167
+ <div className="flex items-center space-x-4">
168
+ <div className="flex items-center">
169
+ <div className="w-3 h-3 rounded-full bg-[#F5F4F6] border border-gray-200 mr-1"></div>
170
+ <span>Pas de données</span>
171
+ </div>
172
+ <div className="flex items-center">
173
+ <div className="w-3 h-3 rounded-full bg-[#ede9fe] mr-1"></div>
174
+ <span>Faible</span>
175
+ </div>
176
+ <div className="flex items-center">
177
+ <div className="w-3 h-3 rounded-full bg-[#7c3aed] mr-1"></div>
178
+ <span>Élevé</span>
179
+ </div>
180
+ </div>
181
+ <div className="text-right">
182
+ <p>Utilisez la molette pour zoomer • Glissez pour vous déplacer</p>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ )
187
+ }
188
+
189
+ export default AfricaMap
190
+
afridatahub-frontend/src/components/Alerts.jsx ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composant Alerts pour AfriDataHub avec analyse prédictive
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { useState, useEffect } from 'react'
7
+ import { motion, AnimatePresence } from 'framer-motion'
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ AlertTriangle,
11
+ TrendingUp,
12
+ TrendingDown,
13
+ Activity,
14
+ Filter,
15
+ RefreshCw,
16
+ Settings,
17
+ BarChart3,
18
+ Brain,
19
+ Zap
20
+ } from 'lucide-react'
21
+ import { API_URL } from '../config'
22
+
23
+ const Alerts = () => {
24
+ const [alerts, setAlerts] = useState([])
25
+ const [analyticsData, setAnalyticsData] = useState(null)
26
+ const [loading, setLoading] = useState(true)
27
+ const [selectedSeverity, setSelectedSeverity] = useState('')
28
+ const [selectedType, setSelectedType] = useState('')
29
+
30
+ useEffect(() => {
31
+ fetchAlertsAndAnalytics()
32
+ }, [])
33
+
34
+ const fetchAlertsAndAnalytics = async () => {
35
+ try {
36
+ setLoading(true)
37
+
38
+ // Récupérer les alertes et données d'analyse
39
+ const [alertsResponse, analyticsResponse] = await Promise.all([
40
+ fetch(`${API_URL}alerts/`),
41
+ fetch(`${API_URL}analytics/dashboard/`)
42
+ ])
43
+
44
+ const alertsData = await alertsResponse.json()
45
+ const analytics = await analyticsResponse.json()
46
+
47
+ setAlerts(alertsData.results || [])
48
+ setAnalyticsData(analytics)
49
+ } catch (error) {
50
+ console.error('Erreur lors du chargement des alertes:', error)
51
+ } finally {
52
+ setLoading(false)
53
+ }
54
+ }
55
+
56
+ const triggerAnalysis = async () => {
57
+ try {
58
+ setLoading(true)
59
+ const response = await fetch(`${API_URL}analytics/trigger/`, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ },
64
+ body: JSON.stringify({})
65
+ })
66
+
67
+ if (response.ok) {
68
+ await fetchAlertsAndAnalytics()
69
+ }
70
+ } catch (error) {
71
+ console.error('Erreur lors du déclenchement de l\'analyse:', error)
72
+ } finally {
73
+ setLoading(false)
74
+ }
75
+ }
76
+
77
+ const getSeverityColor = (severity) => {
78
+ const colors = {
79
+ 'critical': 'bg-red-100 text-red-800 border-red-200',
80
+ 'high': 'bg-orange-100 text-orange-800 border-orange-200',
81
+ 'medium': 'bg-yellow-100 text-yellow-800 border-yellow-200',
82
+ 'low': 'bg-green-100 text-green-800 border-green-200'
83
+ }
84
+ return colors[severity] || 'bg-gray-100 text-gray-800 border-gray-200'
85
+ }
86
+
87
+ const getSeverityIcon = (severity) => {
88
+ switch (severity) {
89
+ case 'critical':
90
+ case 'high':
91
+ return <AlertTriangle className="h-5 w-5" />
92
+ case 'medium':
93
+ return <Activity className="h-5 w-5" />
94
+ case 'low':
95
+ return <TrendingUp className="h-5 w-5" />
96
+ default:
97
+ return <AlertTriangle className="h-5 w-5" />
98
+ }
99
+ }
100
+
101
+ const getAlertTypeIcon = (alertType) => {
102
+ switch (alertType) {
103
+ case 'trend_up':
104
+ return <TrendingUp className="h-4 w-4 text-green-600" />
105
+ case 'trend_down':
106
+ return <TrendingDown className="h-4 w-4 text-red-600" />
107
+ case 'anomaly':
108
+ return <Activity className="h-4 w-4 text-orange-600" />
109
+ default:
110
+ return <AlertTriangle className="h-4 w-4 text-gray-600" />
111
+ }
112
+ }
113
+
114
+ const filteredAlerts = alerts.filter(alert => {
115
+ if (selectedSeverity && alert.severity !== selectedSeverity) return false
116
+ if (selectedType && alert.alert_type !== selectedType) return false
117
+ return true
118
+ })
119
+
120
+ const severityCounts = analyticsData?.alerts_by_severity || {}
121
+
122
+ if (loading) {
123
+ return (
124
+ <div className="flex items-center justify-center h-64">
125
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600"></div>
126
+ </div>
127
+ )
128
+ }
129
+
130
+ return (
131
+ <div className="space-y-10 pb-20">
132
+ {/* En-tête */}
133
+ <motion.div
134
+ initial={{ opacity: 0, y: -20 }}
135
+ animate={{ opacity: 1, y: 0 }}
136
+ className="flex flex-col md:flex-row md:items-end md:justify-between gap-6"
137
+ >
138
+ <div>
139
+ <h1 className="text-4xl font-black text-foreground tracking-tight">Système d'<span className="text-gradient">Alerte</span></h1>
140
+ <p className="text-muted-foreground font-medium mt-2 text-lg">
141
+ Surveillance proactive et détection d'anomalies par Intelligence Artificielle.
142
+ </p>
143
+ </div>
144
+ <div className="flex items-center space-x-3">
145
+ <Button
146
+ variant="ghost"
147
+ onClick={triggerAnalysis}
148
+ disabled={loading}
149
+ className="rounded-2xl hover:bg-primary/10 text-primary font-bold"
150
+ >
151
+ <Brain className={`h-4 w-4 mr-2 ${loading ? 'animate-pulse' : ''}`} />
152
+ Lancer l'Analyse IA
153
+ </Button>
154
+ <Button
155
+ onClick={fetchAlertsAndAnalytics}
156
+ disabled={loading}
157
+ className="rounded-2xl bg-gradient-to-r from-primary to-secondary text-white font-bold shadow-lg hover:shadow-primary/20 transition-all hover:scale-105"
158
+ >
159
+ <RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
160
+ Actualiser
161
+ </Button>
162
+ <Button variant="ghost" className="rounded-2xl hover:bg-muted font-bold">
163
+ <Settings className="h-4 w-4 mr-2" />
164
+ Configuration
165
+ </Button>
166
+ </div>
167
+ </motion.div>
168
+
169
+ {/* Statistiques des alertes Glassmorphism */}
170
+ <motion.div
171
+ initial={{ opacity: 0, y: 20 }}
172
+ animate={{ opacity: 1, y: 0 }}
173
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"
174
+ >
175
+ {[
176
+ { label: 'Critique', count: severityCounts.critical || 0, color: 'from-red-500 to-rose-600', icon: AlertTriangle, bg: 'bg-red-500/10' },
177
+ { label: 'Élevée', count: severityCounts.high || 0, color: 'from-orange-500 to-amber-600', icon: AlertTriangle, bg: 'bg-orange-500/10' },
178
+ { label: 'Moyenne', count: severityCounts.medium || 0, color: 'from-yellow-500 to-orange-400', icon: Activity, bg: 'bg-yellow-500/10' },
179
+ { label: 'Faible', count: severityCounts.low || 0, color: 'from-emerald-500 to-teal-600', icon: TrendingUp, bg: 'bg-emerald-500/10' }
180
+ ].map((stat, i) => (
181
+ <motion.div
182
+ key={stat.label}
183
+ whileHover={{ y: -5 }}
184
+ className="glass rounded-[2rem] p-6 relative overflow-hidden group cursor-pointer"
185
+ >
186
+ <div className={`absolute top-0 right-0 w-24 h-24 ${stat.bg} rounded-full -mr-12 -mt-12 blur-2xl group-hover:scale-150 transition-transform duration-500`}></div>
187
+ <div className="relative z-10 flex items-center">
188
+ <div className={`p-4 rounded-2xl bg-gradient-to-br ${stat.color} shadow-lg mr-4 group-hover:rotate-12 transition-transform`}>
189
+ <stat.icon className="h-6 w-6 text-white" />
190
+ </div>
191
+ <div>
192
+ <p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">{stat.label}</p>
193
+ <p className="text-3xl font-black text-foreground">{stat.count}</p>
194
+ </div>
195
+ </div>
196
+ </motion.div>
197
+ ))}
198
+ </motion.div>
199
+
200
+ {/* Filtres */}
201
+ <motion.div
202
+ initial={{ opacity: 0, y: 20 }}
203
+ animate={{ opacity: 1, y: 0 }}
204
+ className="glass rounded-[2rem] p-6 shadow-xl"
205
+ >
206
+ <div className="flex flex-col md:flex-row gap-6">
207
+ <div className="flex-1">
208
+ <div className="relative group">
209
+ <Filter className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
210
+ <select
211
+ value={selectedSeverity}
212
+ onChange={(e) => setSelectedSeverity(e.target.value)}
213
+ className="w-full pl-12 pr-10 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-bold text-foreground appearance-none cursor-pointer"
214
+ >
215
+ <option value="">Toutes les sévérités</option>
216
+ <option value="critical">🔴 Critique</option>
217
+ <option value="high">🟠 Élevée</option>
218
+ <option value="medium">🟡 Moyenne</option>
219
+ <option value="low">🟢 Faible</option>
220
+ </select>
221
+ </div>
222
+ </div>
223
+
224
+ <div className="flex-1">
225
+ <select
226
+ value={selectedType}
227
+ onChange={(e) => setSelectedType(e.target.value)}
228
+ className="w-full px-6 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-bold text-foreground appearance-none cursor-pointer"
229
+ >
230
+ <option value="">Tous les types d'analyse</option>
231
+ <option value="trend_up">📈 Tendance à la hausse</option>
232
+ <option value="trend_down">📉 Tendance à la baisse</option>
233
+ <option value="anomaly">⚠️ Anomalie détectée</option>
234
+ <option value="threshold">🔔 Seuil dépassé</option>
235
+ <option value="missing_data">❓ Données manquantes</option>
236
+ </select>
237
+ </div>
238
+ </div>
239
+ </motion.div>
240
+
241
+ {/* Liste des alertes */}
242
+ <div className="space-y-6">
243
+ <div className="flex items-center justify-between px-2">
244
+ <div className="flex items-center space-x-2">
245
+ <div className="w-2 h-2 rounded-full bg-primary animate-pulse"></div>
246
+ <h2 className="text-muted-foreground font-bold uppercase tracking-widest text-xs">
247
+ {filteredAlerts.length} alerte{filteredAlerts.length > 1 ? 's' : ''} active{filteredAlerts.length > 1 ? 's' : ''}
248
+ </h2>
249
+ </div>
250
+ </div>
251
+
252
+ {filteredAlerts.length === 0 ? (
253
+ <motion.div
254
+ initial={{ opacity: 0, scale: 0.9 }}
255
+ animate={{ opacity: 1, scale: 1 }}
256
+ className="text-center py-24 glass rounded-[3rem]"
257
+ >
258
+ <div className="w-24 h-24 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-6">
259
+ <Zap className="h-10 w-10 text-primary" />
260
+ </div>
261
+ <h3 className="text-2xl font-black text-foreground mb-2">Horizon dégagé</h3>
262
+ <p className="text-muted-foreground font-medium max-w-md mx-auto">
263
+ Aucune alerte critique n'a été détectée pour le moment. Votre écosystème de données est stable.
264
+ </p>
265
+ </motion.div>
266
+ ) : (
267
+ <div className="grid grid-cols-1 gap-6">
268
+ {filteredAlerts.map((alert, index) => (
269
+ <motion.div
270
+ key={alert.id}
271
+ initial={{ opacity: 0, x: -20 }}
272
+ animate={{ opacity: 1, x: 0 }}
273
+ transition={{ delay: index * 0.05 }}
274
+ className="glass rounded-[2.5rem] p-8 relative overflow-hidden group hover:shadow-2xl transition-all duration-500"
275
+ >
276
+ <div className={`absolute left-0 top-0 bottom-0 w-2 bg-gradient-to-b ${alert.severity === 'critical' ? 'from-red-500 to-rose-600' :
277
+ alert.severity === 'high' ? 'from-orange-500 to-amber-600' :
278
+ alert.severity === 'medium' ? 'from-yellow-500 to-orange-400' :
279
+ 'from-emerald-500 to-teal-600'
280
+ }`}></div>
281
+
282
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-8">
283
+ <div className="flex items-start space-x-6">
284
+ <div className={`p-5 rounded-[1.5rem] bg-white/50 shadow-inner group-hover:scale-110 transition-transform duration-500`}>
285
+ {getSeverityIcon(alert.severity)}
286
+ </div>
287
+ <div>
288
+ <div className="flex flex-wrap items-center gap-3 mb-3">
289
+ <div className="flex items-center space-x-2">
290
+ {getAlertTypeIcon(alert.alert_type)}
291
+ <h3 className="text-xl font-black text-foreground group-hover:text-primary transition-colors">
292
+ {alert.title}
293
+ </h3>
294
+ </div>
295
+ <span className={`px-4 py-1 text-xs font-black uppercase tracking-widest rounded-full bg-white/50 border border-white/20`}>
296
+ {alert.severity === 'critical' ? 'Critique' :
297
+ alert.severity === 'high' ? 'Élevée' :
298
+ alert.severity === 'medium' ? 'Moyenne' : 'Faible'}
299
+ </span>
300
+ </div>
301
+ <p className="text-muted-foreground font-medium text-lg leading-relaxed mb-6 max-w-3xl">{alert.message}</p>
302
+ <div className="flex flex-wrap items-center gap-6">
303
+ <div className="flex items-center space-x-2 px-4 py-2 bg-white/30 rounded-xl border border-white/20">
304
+ <span className="text-lg">📍</span>
305
+ <span className="text-sm font-bold text-foreground/70">{alert.country_name || alert.country}</span>
306
+ </div>
307
+ <div className="flex items-center space-x-2 px-4 py-2 bg-white/30 rounded-xl border border-white/20">
308
+ <BarChart3 className="h-4 w-4 text-primary" />
309
+ <span className="text-sm font-bold text-foreground/70">{alert.dataset_title}</span>
310
+ </div>
311
+ <div className="flex items-center space-x-2 px-4 py-2 bg-white/30 rounded-xl border border-white/20">
312
+ <span className="text-sm font-bold text-foreground/70">🕒 {new Date(alert.created_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' })}</span>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ <div className="flex lg:flex-col gap-3">
318
+ <Button className="rounded-2xl bg-primary text-white font-bold px-8 py-6 shadow-lg hover:shadow-primary/20 transition-all hover:scale-105">
319
+ <BarChart3 className="h-4 w-4 mr-2" />
320
+ Analyser
321
+ </Button>
322
+ <Button variant="ghost" className="rounded-2xl hover:bg-primary/10 text-primary font-bold px-8 py-6">
323
+ Détails
324
+ </Button>
325
+ </div>
326
+ </div>
327
+ </motion.div>
328
+ ))}
329
+ </div>
330
+ )}
331
+ </div>
332
+
333
+ {/* Insights d'analyse prédictive Premium */}
334
+ {analyticsData?.global_trends && (
335
+ <motion.div
336
+ initial={{ opacity: 0, y: 40 }}
337
+ animate={{ opacity: 1, y: 0 }}
338
+ className="relative overflow-hidden rounded-[3rem] p-12 text-white shadow-2xl"
339
+ >
340
+ <div className="absolute inset-0 bg-gradient-to-br from-primary to-secondary opacity-90"></div>
341
+ <div className="absolute inset-0 bg-mesh opacity-20"></div>
342
+
343
+ <div className="relative z-10">
344
+ <h3 className="text-3xl font-black mb-10 flex items-center">
345
+ <Brain className="h-8 w-8 mr-4 text-yellow-300" />
346
+ Intelligence Prédictive
347
+ </h3>
348
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
349
+ {Object.entries(analyticsData.global_trends).map(([datasetTitle, trend]) => (
350
+ <motion.div
351
+ key={datasetTitle}
352
+ whileHover={{ y: -5 }}
353
+ className="bg-white/10 backdrop-blur-lg rounded-3xl p-8 border border-white/20 group cursor-pointer"
354
+ >
355
+ <h4 className="font-black text-yellow-300 mb-4 text-xl">{datasetTitle}</h4>
356
+ <div className="space-y-3">
357
+ <div className="flex items-center justify-between">
358
+ <span className="text-white/70 font-bold text-sm uppercase tracking-widest">Tendances</span>
359
+ <span className="text-white font-black">{trend.trends_count}</span>
360
+ </div>
361
+ <div className="flex items-center justify-between">
362
+ <span className="text-white/70 font-bold text-sm uppercase tracking-widest">Variation</span>
363
+ <span className="text-white font-black">{trend.avg_change.toFixed(1)}%</span>
364
+ </div>
365
+ <div className="mt-4 h-1.5 w-full bg-white/10 rounded-full overflow-hidden">
366
+ <motion.div
367
+ initial={{ width: 0 }}
368
+ animate={{ width: `${Math.min(Math.abs(trend.avg_change) * 5, 100)}%` }}
369
+ className="h-full bg-yellow-300"
370
+ />
371
+ </div>
372
+ </div>
373
+ </motion.div>
374
+ ))}
375
+ </div>
376
+ </div>
377
+ </motion.div>
378
+ )}
379
+ </div>
380
+ )
381
+ }
382
+
383
+ export default Alerts
384
+
afridatahub-frontend/src/components/Analytics.jsx ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composant Analytics pour AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { useState, useEffect } from 'react'
7
+ import { motion, AnimatePresence } from 'framer-motion'
8
+ import { Button } from '@/components/ui/button'
9
+ import AfricaMap from './AfricaMap'
10
+ import {
11
+ CountryBarChart,
12
+ TrendLineChart,
13
+ DomainPieChart,
14
+ ComparisonBarChart,
15
+ MetricsChart
16
+ } from './Charts'
17
+ import {
18
+ BarChart3,
19
+ TrendingUp,
20
+ Globe,
21
+ Filter,
22
+ Download,
23
+ RefreshCw
24
+ } from 'lucide-react'
25
+ import { API_URL } from '../config'
26
+
27
+ const Analytics = () => {
28
+ const [loading, setLoading] = useState(true)
29
+ const [selectedDataset, setSelectedDataset] = useState('')
30
+ const [selectedCountry, setSelectedCountry] = useState('')
31
+ const [datasets, setDatasets] = useState([])
32
+ const [analyticsData, setAnalyticsData] = useState(null)
33
+
34
+ useEffect(() => {
35
+ fetchDatasets()
36
+ fetchAnalyticsData()
37
+ }, [])
38
+
39
+ useEffect(() => {
40
+ if (selectedDataset) {
41
+ fetchAnalyticsData(selectedDataset)
42
+ }
43
+ }, [selectedDataset])
44
+
45
+ const fetchDatasets = async () => {
46
+ try {
47
+ const response = await fetch(`${API_URL}datasets/`)
48
+ const data = await response.json()
49
+ setDatasets(data.results || [])
50
+ } catch (error) {
51
+ console.error('Erreur lors du chargement des datasets:', error)
52
+ }
53
+ }
54
+
55
+ const fetchAnalyticsData = async (datasetRef = null) => {
56
+ try {
57
+ setLoading(true)
58
+
59
+ let url = `${API_URL}analytics/comprehensive/`
60
+ if (datasetRef) {
61
+ url += `?dataset_id=${datasetRef}`
62
+ }
63
+
64
+ const response = await fetch(url)
65
+ const data = await response.json()
66
+
67
+ setAnalyticsData(data)
68
+ } catch (error) {
69
+ console.error('Erreur lors du chargement des données d\'analyse:', error)
70
+ } finally {
71
+ setLoading(false)
72
+ }
73
+ }
74
+
75
+ const handleCountryClick = (countryCode, countryInfo) => {
76
+ setSelectedCountry(countryCode)
77
+ }
78
+
79
+ const handleExportData = () => {
80
+ // Logique d'export des données
81
+ console.log('Export des données d\'analyse')
82
+ }
83
+
84
+ if (loading) {
85
+ return (
86
+ <div className="flex items-center justify-center h-64">
87
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600"></div>
88
+ </div>
89
+ )
90
+ }
91
+
92
+ return (
93
+ <div className="space-y-10 pb-20">
94
+ {/* En-tête */}
95
+ <motion.div
96
+ initial={{ opacity: 0, y: -20 }}
97
+ animate={{ opacity: 1, y: 0 }}
98
+ className="flex flex-col md:flex-row md:items-end md:justify-between gap-6"
99
+ >
100
+ <div>
101
+ <h1 className="text-4xl font-black text-foreground tracking-tight">Intelligence <span className="text-gradient">Analytique</span></h1>
102
+ <p className="text-muted-foreground font-medium mt-2 text-lg">
103
+ Visualisations avancées et corrélations stratégiques en temps réel.
104
+ </p>
105
+ </div>
106
+ <div className="flex items-center space-x-3">
107
+ <Button
108
+ variant="ghost"
109
+ onClick={fetchAnalyticsData}
110
+ disabled={loading}
111
+ className="rounded-2xl hover:bg-primary/10 text-primary font-bold"
112
+ >
113
+ <RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
114
+ Actualiser
115
+ </Button>
116
+ <Button
117
+ onClick={handleExportData}
118
+ className="rounded-2xl bg-gradient-to-r from-primary to-secondary text-white font-bold shadow-lg hover:shadow-primary/20 transition-all hover:scale-105"
119
+ >
120
+ <Download className="h-4 w-4 mr-2" />
121
+ Exporter le rapport
122
+ </Button>
123
+ </div>
124
+ </motion.div>
125
+
126
+ {/* Filtres Glassmorphism */}
127
+ <motion.div
128
+ initial={{ opacity: 0, y: 20 }}
129
+ animate={{ opacity: 1, y: 0 }}
130
+ className="glass rounded-[2rem] p-6 shadow-xl"
131
+ >
132
+ <div className="flex flex-col md:flex-row gap-6">
133
+ <div className="flex-1">
134
+ <div className="relative group">
135
+ <Filter className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
136
+ <select
137
+ value={selectedDataset}
138
+ onChange={(e) => setSelectedDataset(e.target.value)}
139
+ className="w-full pl-12 pr-10 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-bold text-foreground appearance-none cursor-pointer"
140
+ >
141
+ <option value="">Tous les datasets (Vue globale)</option>
142
+ {datasets.map(dataset => (
143
+ <option key={dataset.slug} value={dataset.slug}>
144
+ {dataset.title}
145
+ </option>
146
+ ))}
147
+ </select>
148
+ </div>
149
+ </div>
150
+
151
+ <div className="md:w-64">
152
+ <select
153
+ className="w-full px-6 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-bold text-foreground appearance-none cursor-pointer"
154
+ >
155
+ <option value="">Période: Historique Complet</option>
156
+ <option value="2024">Année 2024</option>
157
+ <option value="2023">Année 2023</option>
158
+ <option value="last5">5 dernières années</option>
159
+ </select>
160
+ </div>
161
+ </div>
162
+ </motion.div>
163
+
164
+ {/* Carte interactive et métriques */}
165
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
166
+ <motion.div
167
+ initial={{ opacity: 0, scale: 0.95 }}
168
+ animate={{ opacity: 1, scale: 1 }}
169
+ className="lg:col-span-2 glass rounded-[3rem] p-10 relative overflow-hidden"
170
+ >
171
+ <div className="absolute top-0 right-0 p-8 opacity-5 pointer-events-none">
172
+ <Globe className="w-48 h-48 text-primary" />
173
+ </div>
174
+ <div className="relative z-10">
175
+ <div className="mb-8">
176
+ <h3 className="text-2xl font-black text-foreground mb-1">Distribution Géographique</h3>
177
+ <p className="text-muted-foreground font-medium">Cliquez sur un pays pour isoler ses performances.</p>
178
+ </div>
179
+ <AfricaMap
180
+ data={analyticsData?.mapData || {}}
181
+ onCountryClick={handleCountryClick}
182
+ selectedCountry={selectedCountry}
183
+ />
184
+ </div>
185
+ </motion.div>
186
+
187
+ <motion.div
188
+ initial={{ opacity: 0, x: 20 }}
189
+ animate={{ opacity: 1, x: 0 }}
190
+ className="glass rounded-[3rem] p-10"
191
+ >
192
+ <MetricsChart
193
+ data={analyticsData?.metricsData || []}
194
+ title="Indicateurs de Performance"
195
+ metric="value"
196
+ />
197
+ </motion.div>
198
+ </div>
199
+
200
+ {/* Graphiques de données */}
201
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
202
+ <motion.div
203
+ initial={{ opacity: 0, y: 40 }}
204
+ animate={{ opacity: 1, y: 0 }}
205
+ className="glass rounded-[3rem] p-10"
206
+ >
207
+ <CountryBarChart
208
+ data={analyticsData?.countryData || []}
209
+ title="Top 10 des Leaders par Valeur"
210
+ dataKey="value"
211
+ />
212
+ </motion.div>
213
+
214
+ <motion.div
215
+ initial={{ opacity: 0, y: 40 }}
216
+ animate={{ opacity: 1, y: 0 }}
217
+ className="glass rounded-[3rem] p-10"
218
+ >
219
+ <DomainPieChart
220
+ data={analyticsData?.domainData || []}
221
+ title="Structure du Portefeuille de Données"
222
+ />
223
+ </motion.div>
224
+ </div>
225
+
226
+ {/* Tendances et comparaisons */}
227
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
228
+ <motion.div
229
+ initial={{ opacity: 0, y: 40 }}
230
+ animate={{ opacity: 1, y: 0 }}
231
+ className="glass rounded-[3rem] p-10"
232
+ >
233
+ <TrendLineChart
234
+ data={analyticsData?.trendData || []}
235
+ title="Trajectoire Temporelle (5 ans)"
236
+ lines={[
237
+ { dataKey: 'agriculture', name: 'Agriculture' },
238
+ { dataKey: 'health', name: 'Santé' },
239
+ { dataKey: 'economy', name: 'Économie' }
240
+ ]}
241
+ />
242
+ </motion.div>
243
+
244
+ <motion.div
245
+ initial={{ opacity: 0, y: 40 }}
246
+ animate={{ opacity: 1, y: 0 }}
247
+ className="glass rounded-[3rem] p-10"
248
+ >
249
+ <ComparisonBarChart
250
+ data={analyticsData?.comparisonData || []}
251
+ title="Analyse Comparative Régionale"
252
+ bars={[
253
+ { dataKey: 'agriculture', name: 'Agriculture' },
254
+ { dataKey: 'health', name: 'Santé' },
255
+ { dataKey: 'economy', name: 'Économie' }
256
+ ]}
257
+ />
258
+ </motion.div>
259
+ </div>
260
+
261
+ {/* Insights automatiques */}
262
+ <motion.div
263
+ initial={{ opacity: 0, y: 40 }}
264
+ animate={{ opacity: 1, y: 0 }}
265
+ className="relative overflow-hidden rounded-[3rem] p-12 text-white shadow-2xl"
266
+ >
267
+ <div className="absolute inset-0 bg-gradient-to-br from-primary to-secondary opacity-90"></div>
268
+ <div className="absolute inset-0 bg-mesh opacity-20"></div>
269
+
270
+ <div className="relative z-10">
271
+ <div className="flex items-center justify-between mb-10">
272
+ <h3 className="text-3xl font-black flex items-center">
273
+ <TrendingUp className="h-8 w-8 mr-4 text-yellow-300" />
274
+ Insights Stratégiques IA
275
+ </h3>
276
+ <div className="px-4 py-2 bg-white/20 backdrop-blur-md rounded-full text-xs font-bold uppercase tracking-widest border border-white/30">
277
+ Généré par Gemini 2.5
278
+ </div>
279
+ </div>
280
+
281
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
282
+ {analyticsData?.global_trends && Object.entries(analyticsData.global_trends).length > 0 ? (
283
+ Object.entries(analyticsData.global_trends).map(([title, trend]) => (
284
+ <motion.div
285
+ key={title}
286
+ whileHover={{ y: -5 }}
287
+ className="bg-white/10 backdrop-blur-lg rounded-3xl p-6 border border-white/20 group cursor-pointer"
288
+ >
289
+ <h4 className="font-black text-yellow-300 mb-3 text-lg group-hover:scale-105 transition-transform origin-left">{title}</h4>
290
+ <p className="text-white/90 font-medium leading-relaxed">
291
+ {trend.trends_count} tendances analysées avec une variation moyenne de <span className="text-white font-black">{trend.avg_change.toFixed(1)}%</span>
292
+ </p>
293
+ </motion.div>
294
+ ))
295
+ ) : (
296
+ <div className="col-span-full py-12 text-center">
297
+ <div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4 animate-pulse">
298
+ <RefreshCw className="h-8 w-8 text-white animate-spin" />
299
+ </div>
300
+ <p className="text-xl font-bold text-white/70">Analyse du flux de données en cours...</p>
301
+ </div>
302
+ )}
303
+ </div>
304
+ </div>
305
+ </motion.div>
306
+ </div>
307
+ )
308
+ }
309
+
310
+ export default Analytics
311
+
afridatahub-frontend/src/components/Charts.jsx ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composants de graphiques pour AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import {
7
+ BarChart,
8
+ Bar,
9
+ LineChart,
10
+ Line,
11
+ PieChart,
12
+ Pie,
13
+ Cell,
14
+ XAxis,
15
+ YAxis,
16
+ CartesianGrid,
17
+ Tooltip,
18
+ Legend,
19
+ ResponsiveContainer
20
+ } from 'recharts'
21
+
22
+ // Palette de couleurs AfriDataHub
23
+ const COLORS = {
24
+ primary: '#7c3aed', // violet
25
+ secondary: '#ec4899', // rose
26
+ success: '#10b981', // vert
27
+ warning: '#f59e0b', // orange
28
+ info: '#3b82f6', // bleu
29
+ danger: '#ef4444', // rouge
30
+ gray: '#6b7280' // gris
31
+ }
32
+
33
+ const CHART_COLORS = [
34
+ COLORS.primary,
35
+ COLORS.secondary,
36
+ COLORS.success,
37
+ COLORS.warning,
38
+ COLORS.info,
39
+ COLORS.danger,
40
+ '#8b5cf6',
41
+ '#f472b6',
42
+ '#34d399',
43
+ '#fbbf24'
44
+ ]
45
+
46
+ // Composant Tooltip personnalisé
47
+ const CustomTooltip = ({ active, payload, label }) => {
48
+ if (active && payload && payload.length) {
49
+ return (
50
+ <div className="bg-white p-3 border border-gray-200 rounded-lg shadow-lg">
51
+ <p className="font-medium text-gray-900">{label}</p>
52
+ {payload.map((entry, index) => (
53
+ <p key={index} style={{ color: entry.color }} className="text-sm">
54
+ {entry.name}: {entry.value?.toLocaleString()}
55
+ </p>
56
+ ))}
57
+ </div>
58
+ )
59
+ }
60
+ return null
61
+ }
62
+
63
+ // Graphique en barres pour les données par pays
64
+ export const CountryBarChart = ({ data, title, dataKey, xAxisKey = 'country' }) => {
65
+ return (
66
+ <div className="bg-white rounded-lg border border-gray-200 p-6">
67
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
68
+ <ResponsiveContainer width="100%" height={300}>
69
+ <BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
70
+ <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
71
+ <XAxis
72
+ dataKey={xAxisKey}
73
+ tick={{ fontSize: 12, fill: '#6b7280' }}
74
+ axisLine={{ stroke: '#e5e7eb' }}
75
+ />
76
+ <YAxis
77
+ tick={{ fontSize: 12, fill: '#6b7280' }}
78
+ axisLine={{ stroke: '#e5e7eb' }}
79
+ />
80
+ <Tooltip content={<CustomTooltip />} />
81
+ <Bar
82
+ dataKey={dataKey}
83
+ fill={COLORS.primary}
84
+ radius={[4, 4, 0, 0]}
85
+ />
86
+ </BarChart>
87
+ </ResponsiveContainer>
88
+ </div>
89
+ )
90
+ }
91
+
92
+ // Graphique en ligne pour les tendances temporelles
93
+ export const TrendLineChart = ({ data, title, lines = [] }) => {
94
+ return (
95
+ <div className="bg-white rounded-lg border border-gray-200 p-6">
96
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
97
+ <ResponsiveContainer width="100%" height={300}>
98
+ <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
99
+ <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
100
+ <XAxis
101
+ dataKey="date"
102
+ tick={{ fontSize: 12, fill: '#6b7280' }}
103
+ axisLine={{ stroke: '#e5e7eb' }}
104
+ />
105
+ <YAxis
106
+ tick={{ fontSize: 12, fill: '#6b7280' }}
107
+ axisLine={{ stroke: '#e5e7eb' }}
108
+ />
109
+ <Tooltip content={<CustomTooltip />} />
110
+ <Legend />
111
+ {lines.map((line, index) => (
112
+ <Line
113
+ key={line.dataKey}
114
+ type="monotone"
115
+ dataKey={line.dataKey}
116
+ stroke={CHART_COLORS[index % CHART_COLORS.length]}
117
+ strokeWidth={2}
118
+ dot={{ r: 4 }}
119
+ name={line.name}
120
+ />
121
+ ))}
122
+ </LineChart>
123
+ </ResponsiveContainer>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ // Graphique en secteurs pour la répartition
129
+ export const DomainPieChart = ({ data, title }) => {
130
+ return (
131
+ <div className="bg-white rounded-lg border border-gray-200 p-6">
132
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
133
+ <ResponsiveContainer width="100%" height={300}>
134
+ <PieChart>
135
+ <Pie
136
+ data={data}
137
+ cx="50%"
138
+ cy="50%"
139
+ labelLine={false}
140
+ label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
141
+ outerRadius={80}
142
+ fill="#8884d8"
143
+ dataKey="value"
144
+ >
145
+ {data.map((entry, index) => (
146
+ <Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
147
+ ))}
148
+ </Pie>
149
+ <Tooltip content={<CustomTooltip />} />
150
+ </PieChart>
151
+ </ResponsiveContainer>
152
+ </div>
153
+ )
154
+ }
155
+
156
+ // Graphique de comparaison multi-barres
157
+ export const ComparisonBarChart = ({ data, title, bars = [] }) => {
158
+ return (
159
+ <div className="bg-white rounded-lg border border-gray-200 p-6">
160
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
161
+ <ResponsiveContainer width="100%" height={300}>
162
+ <BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
163
+ <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
164
+ <XAxis
165
+ dataKey="name"
166
+ tick={{ fontSize: 12, fill: '#6b7280' }}
167
+ axisLine={{ stroke: '#e5e7eb' }}
168
+ />
169
+ <YAxis
170
+ tick={{ fontSize: 12, fill: '#6b7280' }}
171
+ axisLine={{ stroke: '#e5e7eb' }}
172
+ />
173
+ <Tooltip content={<CustomTooltip />} />
174
+ <Legend />
175
+ {bars.map((bar, index) => (
176
+ <Bar
177
+ key={bar.dataKey}
178
+ dataKey={bar.dataKey}
179
+ fill={CHART_COLORS[index % CHART_COLORS.length]}
180
+ name={bar.name}
181
+ radius={[2, 2, 0, 0]}
182
+ />
183
+ ))}
184
+ </BarChart>
185
+ </ResponsiveContainer>
186
+ </div>
187
+ )
188
+ }
189
+
190
+ // Graphique de métriques avec indicateurs
191
+ export const MetricsChart = ({ data, title, metric }) => {
192
+ const formatValue = (value) => {
193
+ if (value >= 1000000) {
194
+ return `${(value / 1000000).toFixed(1)}M`
195
+ } else if (value >= 1000) {
196
+ return `${(value / 1000).toFixed(1)}K`
197
+ }
198
+ return value.toString()
199
+ }
200
+
201
+ return (
202
+ <div className="bg-white rounded-lg border border-gray-200 p-6">
203
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
204
+ <div className="space-y-4">
205
+ {data.map((item, index) => (
206
+ <div key={index} className="flex items-center justify-between">
207
+ <div className="flex items-center">
208
+ <div
209
+ className="w-4 h-4 rounded mr-3"
210
+ style={{ backgroundColor: CHART_COLORS[index % CHART_COLORS.length] }}
211
+ />
212
+ <span className="text-sm font-medium text-gray-700">{item.name}</span>
213
+ </div>
214
+ <div className="flex items-center">
215
+ <span className="text-lg font-bold text-gray-900 mr-2">
216
+ {formatValue(item[metric])}
217
+ </span>
218
+ {item.change && (
219
+ <span className={`text-xs px-2 py-1 rounded-full ${
220
+ item.change > 0
221
+ ? 'bg-green-100 text-green-800'
222
+ : 'bg-red-100 text-red-800'
223
+ }`}>
224
+ {item.change > 0 ? '+' : ''}{item.change}%
225
+ </span>
226
+ )}
227
+ </div>
228
+ </div>
229
+ ))}
230
+ </div>
231
+ </div>
232
+ )
233
+ }
234
+
235
+ export default {
236
+ CountryBarChart,
237
+ TrendLineChart,
238
+ DomainPieChart,
239
+ ComparisonBarChart,
240
+ MetricsChart
241
+ }
242
+
afridatahub-frontend/src/components/Dashboard.jsx ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composant Dashboard pour AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { useState, useEffect } from 'react'
7
+ import { motion } from 'framer-motion'
8
+ import AfricaMap from './AfricaMap'
9
+ import {
10
+ BarChart3,
11
+ Database,
12
+ AlertTriangle,
13
+ TrendingUp,
14
+ Globe,
15
+ Activity,
16
+ Users,
17
+ Zap
18
+ } from 'lucide-react'
19
+ import { API_URL } from '../config'
20
+
21
+ const Dashboard = () => {
22
+ const [stats, setStats] = useState(null)
23
+ const [loading, setLoading] = useState(true)
24
+
25
+ useEffect(() => {
26
+ fetchStats()
27
+ }, [])
28
+
29
+ const fetchStats = async () => {
30
+ try {
31
+ const response = await fetch(`${API_URL}datasets/stats/`)
32
+ const data = await response.json()
33
+ setStats(data)
34
+ } catch (error) {
35
+ console.error('Erreur lors du chargement des statistiques:', error)
36
+ } finally {
37
+ setLoading(false)
38
+ }
39
+ }
40
+
41
+ const StatCard = ({ title, value, icon: Icon, color, description }) => (
42
+ <motion.div
43
+ initial={{ opacity: 0, y: 20 }}
44
+ animate={{ opacity: 1, y: 0 }}
45
+ whileHover={{ y: -5, scale: 1.02 }}
46
+ className="glass rounded-3xl p-6 transition-all duration-300 group cursor-pointer"
47
+ >
48
+ <div className="flex items-center justify-between mb-4">
49
+ <div className={`p-4 rounded-2xl ${color} shadow-lg group-hover:rotate-12 transition-transform duration-300`}>
50
+ <Icon className="h-6 w-6 text-white" />
51
+ </div>
52
+ <div className="h-12 w-12 rounded-full bg-primary/5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
53
+ <TrendingUp className="h-5 w-5 text-primary" />
54
+ </div>
55
+ </div>
56
+ <div>
57
+ <p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">{title}</p>
58
+ <p className="text-3xl font-black text-foreground mt-1">{value}</p>
59
+ {description && (
60
+ <div className="flex items-center mt-3 text-xs font-medium text-success">
61
+ <Activity className="h-3 w-3 mr-1" />
62
+ <span>{description}</span>
63
+ </div>
64
+ )}
65
+ </div>
66
+ </motion.div>
67
+ )
68
+
69
+ const DomainCard = ({ domain, count }) => {
70
+ const getDomainInfo = (domain) => {
71
+ const info = {
72
+ agriculture: { label: 'Agriculture', color: 'bg-green-500', icon: '🌾' },
73
+ health: { label: 'Santé', color: 'bg-red-500', icon: '🏥' },
74
+ economy: { label: 'Économie', color: 'bg-blue-500', icon: '💰' },
75
+ weather: { label: 'Météo', color: 'bg-cyan-500', icon: '🌤️' },
76
+ energy: { label: 'Énergie', color: 'bg-yellow-500', icon: '⚡' },
77
+ education: { label: 'Éducation', color: 'bg-purple-500', icon: '📚' },
78
+ population: { label: 'Population', color: 'bg-pink-500', icon: '👥' },
79
+ environment: { label: 'Environnement', color: 'bg-emerald-500', icon: '🌍' },
80
+ transport: { label: 'Transport', color: 'bg-orange-500', icon: '🚗' },
81
+ other: { label: 'Autre', color: 'bg-gray-500', icon: '📊' },
82
+ }
83
+ return info[domain] || info.other
84
+ }
85
+
86
+ const domainInfo = getDomainInfo(domain)
87
+
88
+ return (
89
+ <motion.div
90
+ whileHover={{ x: 5 }}
91
+ className="flex items-center justify-between p-4 bg-white/50 hover:bg-white rounded-2xl transition-colors border border-transparent hover:border-primary/20"
92
+ >
93
+ <div className="flex items-center">
94
+ <div className={`w-10 h-10 rounded-xl ${domainInfo.color} flex items-center justify-center text-xl shadow-sm mr-4`}>
95
+ {domainInfo.icon}
96
+ </div>
97
+ <span className="font-bold text-foreground/80">{domainInfo.label}</span>
98
+ </div>
99
+ <div className="px-3 py-1 bg-primary/10 rounded-full">
100
+ <span className="text-sm font-black text-primary">{count}</span>
101
+ </div>
102
+ </motion.div>
103
+ )
104
+ }
105
+
106
+ if (loading) {
107
+ return (
108
+ <div className="flex flex-col items-center justify-center h-[60vh]">
109
+ <div className="relative w-20 h-20">
110
+ <div className="absolute inset-0 border-4 border-primary/20 rounded-full"></div>
111
+ <div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
112
+ </div>
113
+ <p className="mt-4 font-bold text-primary animate-pulse">Chargement de l'univers AfriDataHub...</p>
114
+ </div>
115
+ )
116
+ }
117
+
118
+ return (
119
+ <div className="space-y-10 pb-20">
120
+ {/* En-tête Hero */}
121
+ <motion.div
122
+ initial={{ opacity: 0, scale: 0.95 }}
123
+ animate={{ opacity: 1, scale: 1 }}
124
+ className="relative overflow-hidden rounded-[3rem] p-12 text-white shadow-2xl"
125
+ >
126
+ <div className="absolute inset-0 bg-gradient-to-br from-primary via-secondary to-accent opacity-90"></div>
127
+ <div className="absolute inset-0 bg-mesh opacity-30 animate-pulse"></div>
128
+
129
+ <div className="relative z-10 max-w-3xl">
130
+ <motion.div
131
+ initial={{ opacity: 0, x: -20 }}
132
+ animate={{ opacity: 1, x: 0 }}
133
+ transition={{ delay: 0.2 }}
134
+ className="inline-flex items-center px-4 py-2 bg-white/20 backdrop-blur-md rounded-full mb-6 border border-white/30"
135
+ >
136
+ <Zap className="h-4 w-4 mr-2 text-yellow-300" />
137
+ <span className="text-xs font-bold uppercase tracking-widest">Intelligence Artificielle Active</span>
138
+ </motion.div>
139
+
140
+ <h1 className="text-5xl md:text-7xl font-black mb-6 leading-tight">
141
+ L'Afrique par les <span className="text-yellow-300">Données</span>
142
+ </h1>
143
+ <p className="text-xl text-white/80 font-medium leading-relaxed mb-10">
144
+ Explorez, analysez et visualisez le futur du continent à travers une plateforme intelligente, ouverte et interactive.
145
+ </p>
146
+
147
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
148
+ {[
149
+ { icon: Database, label: 'Données Réelles', color: 'bg-blue-400' },
150
+ { icon: BarChart3, label: 'Analyses IA', color: 'bg-purple-400' },
151
+ { icon: AlertTriangle, label: 'Alertes Prédictives', color: 'bg-pink-400' }
152
+ ].map((item, i) => (
153
+ <motion.div
154
+ key={i}
155
+ whileHover={{ y: -5 }}
156
+ className="flex items-center p-4 bg-white/10 backdrop-blur-lg rounded-2xl border border-white/20"
157
+ >
158
+ <div className={`p-2 rounded-lg ${item.color} mr-3 shadow-lg`}>
159
+ <item.icon className="h-5 w-5 text-white" />
160
+ </div>
161
+ <span className="font-bold text-sm">{item.label}</span>
162
+ </motion.div>
163
+ ))}
164
+ </div>
165
+ </div>
166
+
167
+ {/* Élément décoratif flottant */}
168
+ <div className="absolute right-[-10%] top-[-10%] w-96 h-96 bg-white/10 rounded-full blur-3xl animate-float"></div>
169
+ </motion.div>
170
+
171
+ {/* Statistiques principales */}
172
+ {stats && (
173
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
174
+ <StatCard
175
+ title="Total Datasets"
176
+ value={stats.total_datasets}
177
+ icon={Database}
178
+ color="bg-indigo-500"
179
+ description="Datasets certifiés"
180
+ />
181
+ <StatCard
182
+ title="Datasets Actifs"
183
+ value={stats.active_datasets}
184
+ icon={Activity}
185
+ color="bg-emerald-500"
186
+ description="Flux temps réel"
187
+ />
188
+ <StatCard
189
+ title="Points de Données"
190
+ value={stats.total_data_points?.toLocaleString() || '0'}
191
+ icon={Zap}
192
+ color="bg-amber-500"
193
+ description="Intelligence collectée"
194
+ />
195
+ <StatCard
196
+ title="Pays Couverts"
197
+ value={Object.keys(stats.countries_count || {}).length}
198
+ icon={Globe}
199
+ color="bg-rose-500"
200
+ description="Souveraineté numérique"
201
+ />
202
+ </div>
203
+ )}
204
+
205
+ {/* Répartition par domaine et mises à jour récentes */}
206
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
207
+ {/* Domaines */}
208
+ <motion.div
209
+ initial={{ opacity: 0, x: -20 }}
210
+ animate={{ opacity: 1, x: 0 }}
211
+ className="glass rounded-[2.5rem] p-10"
212
+ >
213
+ <div className="flex items-center justify-between mb-8">
214
+ <h3 className="text-2xl font-black text-foreground flex items-center">
215
+ <BarChart3 className="h-6 w-6 mr-3 text-primary" />
216
+ Domaines d'impact
217
+ </h3>
218
+ <div className="p-2 bg-primary/10 rounded-xl">
219
+ <Activity className="h-5 w-5 text-primary" />
220
+ </div>
221
+ </div>
222
+ <div className="grid grid-cols-1 gap-4">
223
+ {stats?.domains_count && Object.entries(stats.domains_count).map(([domain, count]) => (
224
+ <DomainCard key={domain} domain={domain} count={count} />
225
+ ))}
226
+ </div>
227
+ </motion.div>
228
+
229
+ {/* Mises à jour récentes */}
230
+ <motion.div
231
+ initial={{ opacity: 0, x: 20 }}
232
+ animate={{ opacity: 1, x: 0 }}
233
+ className="glass rounded-[2.5rem] p-10"
234
+ >
235
+ <div className="flex items-center justify-between mb-8">
236
+ <h3 className="text-2xl font-black text-foreground flex items-center">
237
+ <TrendingUp className="h-6 w-6 mr-3 text-secondary" />
238
+ Flux d'actualité
239
+ </h3>
240
+ <div className="p-2 bg-secondary/10 rounded-xl">
241
+ <Zap className="h-5 w-5 text-secondary" />
242
+ </div>
243
+ </div>
244
+ <div className="space-y-4">
245
+ {stats?.recent_updates?.map((dataset, i) => (
246
+ <motion.div
247
+ key={dataset.slug}
248
+ initial={{ opacity: 0, y: 10 }}
249
+ animate={{ opacity: 1, y: 0 }}
250
+ transition={{ delay: i * 0.1 }}
251
+ className="flex items-center justify-between p-5 bg-white/50 hover:bg-white rounded-2xl transition-all group cursor-pointer border border-transparent hover:border-secondary/20 shadow-sm"
252
+ >
253
+ <div className="flex items-center">
254
+ <div className="w-12 h-12 rounded-xl bg-secondary/10 flex items-center justify-center mr-4 group-hover:scale-110 transition-transform">
255
+ <Database className="h-5 w-5 text-secondary" />
256
+ </div>
257
+ <div>
258
+ <p className="font-bold text-foreground group-hover:text-secondary transition-colors">{dataset.title}</p>
259
+ <p className="text-xs font-semibold text-muted-foreground uppercase tracking-widest">{dataset.source_name}</p>
260
+ </div>
261
+ </div>
262
+ <div className="text-right">
263
+ <p className="text-sm font-black text-foreground/60">
264
+ {new Date(dataset.last_updated).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
265
+ </p>
266
+ </div>
267
+ </motion.div>
268
+ ))}
269
+ </div>
270
+ </motion.div>
271
+ </div>
272
+
273
+ {/* Carte de l'Afrique interactive */}
274
+ <motion.div
275
+ initial={{ opacity: 0, y: 40 }}
276
+ animate={{ opacity: 1, y: 0 }}
277
+ className="glass rounded-[3rem] p-12 overflow-hidden relative"
278
+ >
279
+ <div className="absolute top-0 right-0 p-12 opacity-5 pointer-events-none">
280
+ <Globe className="w-64 h-64 text-primary" />
281
+ </div>
282
+ <div className="relative z-10">
283
+ <div className="mb-10">
284
+ <h3 className="text-3xl font-black text-foreground mb-2">Cartographie Interactive</h3>
285
+ <p className="text-muted-foreground font-medium">Visualisez l'état du continent en temps réel par indicateur.</p>
286
+ </div>
287
+ <AfricaMap
288
+ onCountryClick={(code, info) => console.log('Pays sélectionné:', code, info)}
289
+ />
290
+ </div>
291
+ </motion.div>
292
+ </div>
293
+ )
294
+ }
295
+
296
+ export default Dashboard
297
+
afridatahub-frontend/src/components/DatasetAnalysis.jsx ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion, AnimatePresence } from 'framer-motion'
2
+ import ReactMarkdown from 'react-markdown'
3
+ import remarkGfm from 'remark-gfm'
4
+ import {
5
+ LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
6
+ BarChart, Bar, Legend
7
+ } from 'recharts'
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ Brain, TrendingUp, Lightbulb, Activity, Globe, Users, Zap, AlertTriangle
11
+ } from 'lucide-react'
12
+
13
+ // Mappage des icônes
14
+ const iconMap = {
15
+ trending: TrendingUp,
16
+ globe: Globe,
17
+ users: Users,
18
+ activity: Activity,
19
+ zap: Zap,
20
+ peak: Activity
21
+ }
22
+
23
+ const WidgetFactory = ({ widget, chartsData, themeColor }) => {
24
+ const Icon = iconMap[widget.icon] || Activity
25
+
26
+ switch (widget.type) {
27
+ case 'header_section':
28
+ return (
29
+ <div className="mb-12">
30
+ <h3 className="text-3xl font-black text-foreground mb-6 tracking-tight">{widget.title}</h3>
31
+ <div className="prose prose-lg max-w-none text-muted-foreground leading-relaxed font-medium">
32
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
33
+ {widget.content}
34
+ </ReactMarkdown>
35
+ </div>
36
+ </div>
37
+ )
38
+
39
+ case 'kpi_grid':
40
+ return (
41
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
42
+ {widget.items.map((item, idx) => {
43
+ const ItemIcon = iconMap[item.icon] || Activity
44
+ return (
45
+ <motion.div
46
+ key={idx}
47
+ initial={{ opacity: 0, y: 20 }}
48
+ animate={{ opacity: 1, y: 0 }}
49
+ transition={{ delay: idx * 0.1 }}
50
+ className="glass p-8 rounded-[2rem] shadow-xl flex items-center space-x-6 group hover:scale-105 transition-transform"
51
+ >
52
+ <div className={`p-5 rounded-2xl bg-white/50 shadow-inner group-hover:rotate-12 transition-transform`}>
53
+ <ItemIcon className={`h-8 w-8 ${item.color || 'text-primary'}`} />
54
+ </div>
55
+ <div>
56
+ <p className="text-xs text-muted-foreground font-black uppercase tracking-widest mb-1">{item.label}</p>
57
+ <p className="text-3xl font-black text-foreground">{item.value}</p>
58
+ </div>
59
+ </motion.div>
60
+ )
61
+ })}
62
+ </div>
63
+ )
64
+
65
+ case 'chart_section': {
66
+ const isEvolution = widget.chart_type === 'evolution'
67
+ const data = isEvolution ? chartsData.evolution : chartsData.top_countries
68
+
69
+ return (
70
+ <div className="glass p-10 rounded-[3rem] shadow-xl mb-12 relative overflow-hidden">
71
+ <div className="relative z-10">
72
+ <h3 className="text-2xl font-black text-foreground mb-8 flex items-center">
73
+ <div className="p-3 rounded-xl bg-primary/10 mr-4">
74
+ {isEvolution ? <TrendingUp className="h-6 w-6 text-primary" /> : <Activity className="h-6 w-6 text-primary" />}
75
+ </div>
76
+ {widget.title}
77
+ </h3>
78
+
79
+ <div className="h-96 mb-8">
80
+ <ResponsiveContainer width="100%" height="100%">
81
+ {isEvolution ? (
82
+ <LineChart data={data}>
83
+ <defs>
84
+ <linearGradient id="lineGradient" x1="0" y1="0" x2="0" y2="1">
85
+ <stop offset="5%" stopColor={`var(--color-${themeColor})`} stopOpacity={0.3} />
86
+ <stop offset="95%" stopColor={`var(--color-${themeColor})`} stopOpacity={0} />
87
+ </linearGradient>
88
+ </defs>
89
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(0,0,0,0.05)" vertical={false} />
90
+ <XAxis dataKey="year" stroke="rgba(0,0,0,0.3)" fontSize={12} fontWeight="bold" />
91
+ <YAxis stroke="rgba(0,0,0,0.3)" fontSize={12} fontWeight="bold" />
92
+ <Tooltip
93
+ contentStyle={{ borderRadius: '20px', border: 'none', boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1)', padding: '15px' }}
94
+ />
95
+ <Line
96
+ type="monotone"
97
+ dataKey="value"
98
+ stroke={`var(--color-${themeColor})`}
99
+ strokeWidth={4}
100
+ dot={{ fill: `var(--color-${themeColor})`, strokeWidth: 3, r: 6 }}
101
+ activeDot={{ r: 10, strokeWidth: 0 }}
102
+ />
103
+ </LineChart>
104
+ ) : (
105
+ <BarChart data={data} layout="vertical">
106
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(0,0,0,0.05)" horizontal={false} />
107
+ <XAxis type="number" stroke="rgba(0,0,0,0.3)" fontSize={12} fontWeight="bold" />
108
+ <YAxis dataKey="country" type="category" width={120} stroke="rgba(0,0,0,0.6)" fontSize={12} fontWeight="bold" />
109
+ <Tooltip
110
+ contentStyle={{ borderRadius: '20px', border: 'none', boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1)', padding: '15px' }}
111
+ />
112
+ <Bar dataKey="value" fill={`var(--color-${themeColor})`} radius={[0, 10, 10, 0]} barSize={30} />
113
+ </BarChart>
114
+ )}
115
+ </ResponsiveContainer>
116
+ </div>
117
+
118
+ <div className="bg-white/30 backdrop-blur-md p-6 rounded-2xl border border-white/20">
119
+ <p className="text-foreground font-medium italic leading-relaxed">
120
+ "{widget.commentary}"
121
+ </p>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ case 'insight_cards':
129
+ return (
130
+ <div className="mb-12">
131
+ <h3 className="text-2xl font-black text-foreground mb-8">{widget.title}</h3>
132
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
133
+ {widget.items.map((insight, idx) => (
134
+ <motion.div
135
+ key={idx}
136
+ initial={{ opacity: 0, scale: 0.95 }}
137
+ animate={{ opacity: 1, scale: 1 }}
138
+ transition={{ delay: idx * 0.1 }}
139
+ className="glass p-8 rounded-[2rem] shadow-lg border border-white/20 group hover:bg-white/40 transition-all"
140
+ >
141
+ <div className="flex flex-col space-y-4">
142
+ <div className="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center group-hover:scale-110 transition-transform">
143
+ <Lightbulb className={`h-6 w-6 text-primary`} />
144
+ </div>
145
+ <p className="text-foreground font-bold leading-relaxed">{insight}</p>
146
+ </div>
147
+ </motion.div>
148
+ ))}
149
+ </div>
150
+ </div>
151
+ )
152
+
153
+ case 'text_section':
154
+ return (
155
+ <div className="relative overflow-hidden rounded-[3rem] p-12 text-white shadow-2xl mb-12">
156
+ <div className="absolute inset-0 bg-gradient-to-br from-primary to-secondary opacity-90"></div>
157
+ <div className="absolute inset-0 bg-mesh opacity-20"></div>
158
+
159
+ <div className="relative z-10">
160
+ <h3 className="text-3xl font-black mb-8 flex items-center">
161
+ <Brain className="h-8 w-8 mr-4 text-yellow-300" />
162
+ {widget.title}
163
+ </h3>
164
+ <div className="prose prose-invert max-w-none text-white/90 leading-relaxed text-xl font-medium">
165
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
166
+ {widget.content}
167
+ </ReactMarkdown>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ )
172
+
173
+ default:
174
+ return null
175
+ }
176
+ }
177
+
178
+ const DatasetAnalysis = ({ dataset, analysisData, onBack }) => {
179
+ const { analysis, charts_data } = analysisData
180
+
181
+ // Extraire la couleur du thème (par défaut purple-600)
182
+ const themeColor = analysis.theme_color ? analysis.theme_color.split('-')[0] + '-600' : 'primary'
183
+
184
+ // Style CSS variable pour les graphiques
185
+ const style = {
186
+ [`--color-primary`]: '#7c3aed',
187
+ [`--color-emerald-600`]: '#059669',
188
+ [`--color-blue-600`]: '#2563eb',
189
+ [`--color-red-600`]: '#dc2626',
190
+ [`--color-purple-600`]: '#7c3aed',
191
+ }
192
+
193
+ return (
194
+ <motion.div
195
+ initial={{ opacity: 0 }}
196
+ animate={{ opacity: 1 }}
197
+ className="min-h-screen bg-background pb-20"
198
+ style={style}
199
+ >
200
+ {/* Header Premium avec Mesh Gradient */}
201
+ <div className="relative overflow-hidden pt-12 pb-24 px-8">
202
+ <div className="absolute inset-0 bg-mesh opacity-30"></div>
203
+ <div className="absolute inset-0 bg-gradient-to-b from-primary/10 to-transparent"></div>
204
+
205
+ <div className="max-w-7xl mx-auto relative z-10">
206
+ <Button
207
+ variant="ghost"
208
+ onClick={onBack}
209
+ className="rounded-xl hover:bg-primary/10 text-primary font-bold mb-12"
210
+ >
211
+ ← Retour aux datasets
212
+ </Button>
213
+
214
+ <div className="flex flex-col md:flex-row md:items-end justify-between gap-12">
215
+ <div className="flex-1">
216
+ <div className="flex items-center space-x-3 mb-6">
217
+ <div className="px-4 py-2 bg-primary/10 rounded-full border border-primary/20 flex items-center">
218
+ <Brain className="h-4 w-4 text-primary mr-2" />
219
+ <span className="text-primary font-black uppercase tracking-widest text-xs">Intelligence Artificielle</span>
220
+ </div>
221
+ <div className="px-4 py-2 bg-secondary/10 rounded-full border border-secondary/20 flex items-center">
222
+ <Zap className="h-4 w-4 text-secondary mr-2" />
223
+ <span className="text-secondary font-black uppercase tracking-widest text-xs">Rapport Stratégique</span>
224
+ </div>
225
+ </div>
226
+ <h1 className="text-6xl font-black text-foreground tracking-tight mb-6 leading-tight">
227
+ {analysis.report_title || dataset.title}
228
+ </h1>
229
+ <p className="text-muted-foreground font-medium text-2xl max-w-3xl leading-relaxed">
230
+ Analyse scientifique approfondie générée pour <span className="text-foreground font-black">{dataset.source_name}</span>.
231
+ </p>
232
+ </div>
233
+
234
+ <div className="hidden lg:block">
235
+ <div className="w-64 h-64 glass rounded-[3rem] flex items-center justify-center relative">
236
+ <div className="absolute inset-0 bg-primary/5 rounded-[3rem] animate-pulse"></div>
237
+ <Brain className="h-32 w-32 text-primary opacity-20" />
238
+ <div className="absolute -bottom-4 -right-4 bg-white shadow-2xl rounded-2xl p-4 border border-primary/10">
239
+ <div className="flex items-center space-x-2">
240
+ <div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></div>
241
+ <span className="text-xs font-black uppercase tracking-tighter">Gemini 2.5 Live</span>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <div className="max-w-7xl mx-auto px-8 -mt-12">
251
+ {/* Rendu dynamique des widgets */}
252
+ {analysis.layout ? (
253
+ analysis.layout.map((widget, idx) => (
254
+ <WidgetFactory
255
+ key={idx}
256
+ widget={widget}
257
+ chartsData={charts_data}
258
+ themeColor={themeColor}
259
+ />
260
+ ))
261
+ ) : (
262
+ <div className="glass rounded-[3rem] py-24 text-center">
263
+ <div className="w-20 h-20 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-6">
264
+ <AlertTriangle className="h-10 w-10 text-primary" />
265
+ </div>
266
+ <h3 className="text-2xl font-black text-foreground mb-2">Format non reconnu</h3>
267
+ <p className="text-muted-foreground font-medium">Veuillez régénérer l'analyse pour corriger ce problème.</p>
268
+ </div>
269
+ )}
270
+ </div>
271
+ </motion.div>
272
+ )
273
+ }
274
+
275
+ export default DatasetAnalysis
afridatahub-frontend/src/components/DatasetCard.jsx ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composant DatasetCard pour AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { motion } from 'framer-motion'
7
+ import { Button } from '@/components/ui/button'
8
+ import {
9
+ Calendar,
10
+ Database,
11
+ ExternalLink,
12
+ TrendingUp,
13
+ MapPin
14
+ } from 'lucide-react'
15
+
16
+ const DatasetCard = ({ dataset, onViewDetails }) => {
17
+ const getDomainColor = (domain) => {
18
+ const colors = {
19
+ agriculture: 'bg-green-100 text-green-800',
20
+ health: 'bg-red-100 text-red-800',
21
+ economy: 'bg-blue-100 text-blue-800',
22
+ weather: 'bg-cyan-100 text-cyan-800',
23
+ energy: 'bg-yellow-100 text-yellow-800',
24
+ education: 'bg-purple-100 text-purple-800',
25
+ population: 'bg-pink-100 text-pink-800',
26
+ environment: 'bg-emerald-100 text-emerald-800',
27
+ transport: 'bg-orange-100 text-orange-800',
28
+ other: 'bg-gray-100 text-gray-800',
29
+ }
30
+ return colors[domain] || colors.other
31
+ }
32
+
33
+ const formatDate = (dateString) => {
34
+ return new Date(dateString).toLocaleDateString('fr-FR', {
35
+ year: 'numeric',
36
+ month: 'short',
37
+ day: 'numeric'
38
+ })
39
+ }
40
+
41
+ return (
42
+ <motion.div
43
+ initial={{ opacity: 0, y: 20 }}
44
+ animate={{ opacity: 1, y: 0 }}
45
+ whileHover={{ y: -4, boxShadow: "0 10px 25px rgba(0,0,0,0.1)" }}
46
+ transition={{ duration: 0.2 }}
47
+ className="bg-white rounded-lg border border-gray-200 p-6 hover:border-purple-300 transition-all duration-200"
48
+ >
49
+ {/* En-tête */}
50
+ <div className="flex items-start justify-between mb-4">
51
+ <div className="flex-1">
52
+ <h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">
53
+ {dataset.title}
54
+ </h3>
55
+ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getDomainColor(dataset.domain)}`}>
56
+ {dataset.domain}
57
+ </span>
58
+ </div>
59
+ </div>
60
+
61
+ {/* Description */}
62
+ <p className="text-gray-600 text-sm mb-4 line-clamp-3">
63
+ {dataset.description}
64
+ </p>
65
+
66
+ {/* Métadonnées */}
67
+ <div className="space-y-2 mb-4">
68
+ <div className="flex items-center text-sm text-gray-500">
69
+ <Database className="h-4 w-4 mr-2" />
70
+ <span>{dataset.data_points_count} points de données</span>
71
+ </div>
72
+
73
+ <div className="flex items-center text-sm text-gray-500">
74
+ <Calendar className="h-4 w-4 mr-2" />
75
+ <span>Mis à jour le {formatDate(dataset.last_updated)}</span>
76
+ </div>
77
+
78
+ <div className="flex items-center text-sm text-gray-500">
79
+ <MapPin className="h-4 w-4 mr-2" />
80
+ <span>Source: {dataset.source_name}</span>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Statut */}
85
+ <div className="flex items-center justify-between mb-4">
86
+ <div className="flex items-center">
87
+ <div className={`w-2 h-2 rounded-full mr-2 ${
88
+ dataset.status === 'active' ? 'bg-green-500' :
89
+ dataset.status === 'updating' ? 'bg-yellow-500' : 'bg-red-500'
90
+ }`} />
91
+ <span className="text-sm text-gray-600 capitalize">
92
+ {dataset.status}
93
+ </span>
94
+ </div>
95
+
96
+ {dataset.latest_data_date && (
97
+ <div className="flex items-center text-sm text-gray-500">
98
+ <TrendingUp className="h-4 w-4 mr-1" />
99
+ <span>{formatDate(dataset.latest_data_date)}</span>
100
+ </div>
101
+ )}
102
+ </div>
103
+
104
+ {/* Actions */}
105
+ <div className="flex items-center justify-between">
106
+ <Button
107
+ onClick={() => onViewDetails(dataset)}
108
+ className="bg-purple-600 hover:bg-purple-700 text-white"
109
+ >
110
+ Voir les données
111
+ </Button>
112
+
113
+ {dataset.source_url && (
114
+ <Button
115
+ variant="ghost"
116
+ size="sm"
117
+ onClick={() => window.open(dataset.source_url, '_blank')}
118
+ className="text-purple-600 hover:text-purple-700 hover:bg-purple-50"
119
+ >
120
+ <ExternalLink className="h-4 w-4" />
121
+ </Button>
122
+ )}
123
+ </div>
124
+ </motion.div>
125
+ )
126
+ }
127
+
128
+ export default DatasetCard
129
+
afridatahub-frontend/src/components/Datasets.jsx ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composant Datasets pour AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { useState, useEffect, useCallback } from 'react'
7
+ import { motion, AnimatePresence } from 'framer-motion'
8
+ import { Button } from '@/components/ui/button'
9
+ import DatasetCard from './DatasetCard'
10
+ import {
11
+ Search,
12
+ Filter,
13
+ Download,
14
+ Upload,
15
+ RefreshCw,
16
+ Brain
17
+ } from 'lucide-react'
18
+ import { API_URL } from '../config'
19
+
20
+ const Datasets = ({ onStartAnalysis }) => {
21
+ const [datasets, setDatasets] = useState([])
22
+ const [loading, setLoading] = useState(true)
23
+ const [searchTerm, setSearchTerm] = useState('')
24
+ const [selectedDomain, setSelectedDomain] = useState('')
25
+ const [selectedDataset, setSelectedDataset] = useState(null)
26
+ const [isAnalyzing, setIsAnalyzing] = useState(false)
27
+
28
+ const domains = [
29
+ { value: '', label: 'Tous les domaines' },
30
+ { value: 'agriculture', label: 'Agriculture' },
31
+ { value: 'health', label: 'Santé' },
32
+ { value: 'economy', label: 'Économie' },
33
+ { value: 'weather', label: 'Météorologie' },
34
+ { value: 'energy', label: 'Énergie' },
35
+ { value: 'education', label: 'Éducation' },
36
+ { value: 'population', label: 'Population' },
37
+ { value: 'environment', label: 'Environnement' },
38
+ { value: 'transport', label: 'Transport' },
39
+ { value: 'other', label: 'Autre' },
40
+ ]
41
+
42
+ useEffect(() => {
43
+ fetchDatasets()
44
+ }, [])
45
+
46
+ const fetchDatasets = async () => {
47
+ try {
48
+ setLoading(true)
49
+ const response = await fetch(`${API_URL}datasets/`)
50
+ const data = await response.json()
51
+ setDatasets(data.results || [])
52
+ } catch (error) {
53
+ console.error('Erreur lors du chargement des datasets:', error)
54
+ } finally {
55
+ setLoading(false)
56
+ }
57
+ }
58
+
59
+ const filteredDatasets = datasets.filter(dataset => {
60
+ const matchesSearch = dataset.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
61
+ dataset.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
62
+ dataset.source_name.toLowerCase().includes(searchTerm.toLowerCase())
63
+ const matchesDomain = !selectedDomain || dataset.domain === selectedDomain
64
+ return matchesSearch && matchesDomain
65
+ })
66
+
67
+ const handleViewDetails = (dataset) => {
68
+ setSelectedDataset(dataset)
69
+ }
70
+
71
+ const handleAnalyze = async (dataset) => {
72
+ try {
73
+ setIsAnalyzing(true)
74
+ // Fermer le modal de détails si ouvert
75
+ setSelectedDataset(null)
76
+
77
+ const response = await fetch(`${API_URL}datasets/${dataset.slug}/analyze/`)
78
+ const data = await response.json()
79
+
80
+ if (response.ok) {
81
+ if (onStartAnalysis) {
82
+ onStartAnalysis(dataset, data)
83
+ }
84
+ } else {
85
+ console.error('Erreur analyse:', data)
86
+ }
87
+ } catch (error) {
88
+ console.error('Erreur lors de l\'analyse:', error)
89
+ } finally {
90
+ setIsAnalyzing(false)
91
+ }
92
+ }
93
+
94
+ const DatasetDetails = ({ dataset, onClose }) => {
95
+ const [dataPoints, setDataPoints] = useState([])
96
+ const [loadingData, setLoadingData] = useState(true)
97
+
98
+ const fetchDataPoints = useCallback(async () => {
99
+ try {
100
+ setLoadingData(true)
101
+ const response = await fetch(`${API_URL}datasets/${dataset.slug}/data/?limit=50`)
102
+ const result = await response.json()
103
+ setDataPoints(result.data_points || [])
104
+ } catch (error) {
105
+ console.error('Erreur lors du chargement des données:', error)
106
+ } finally {
107
+ setLoadingData(false)
108
+ }
109
+ }, [dataset.slug])
110
+
111
+ useEffect(() => {
112
+ fetchDataPoints()
113
+ }, [fetchDataPoints])
114
+
115
+ const handleDownload = () => {
116
+ if (dataPoints.length === 0) return
117
+
118
+ // Créer le contenu CSV
119
+ const headers = ['Pays', 'Date', 'Valeur', 'Unité', 'Source']
120
+ const csvContent = [
121
+ headers.join(','),
122
+ ...dataPoints.map(dp => [
123
+ dp.country_name,
124
+ dp.date,
125
+ dp.value,
126
+ dp.unit,
127
+ dataset.source_name
128
+ ].join(','))
129
+ ].join('\n')
130
+
131
+ // Créer un blob et télécharger
132
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
133
+ const link = document.createElement('a')
134
+ const url = URL.createObjectURL(blob)
135
+ link.setAttribute('href', url)
136
+ link.setAttribute('download', `${dataset.title.replace(/\s+/g, '_')}_preview.csv`)
137
+ link.style.visibility = 'hidden'
138
+ document.body.appendChild(link)
139
+ link.click()
140
+ document.body.removeChild(link)
141
+ }
142
+
143
+ return (
144
+ <motion.div
145
+ initial={{ opacity: 0 }}
146
+ animate={{ opacity: 1 }}
147
+ className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
148
+ onClick={onClose}
149
+ >
150
+ <motion.div
151
+ initial={{ scale: 0.9, opacity: 0 }}
152
+ animate={{ scale: 1, opacity: 1 }}
153
+ className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto flex flex-col"
154
+ onClick={(e) => e.stopPropagation()}
155
+ >
156
+ <div className="p-6">
157
+ <div className="flex items-center justify-between mb-6">
158
+ <h2 className="text-2xl font-bold text-gray-900">{dataset.title}</h2>
159
+ <Button variant="ghost" onClick={onClose}>
160
+
161
+ </Button>
162
+ </div>
163
+
164
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
165
+ <div>
166
+ <h3 className="font-semibold text-gray-900 mb-2">Description</h3>
167
+ <p className="text-gray-600">{dataset.description}</p>
168
+ </div>
169
+ <div>
170
+ <h3 className="font-semibold text-gray-900 mb-2">Informations</h3>
171
+ <div className="space-y-2 text-sm">
172
+ <div><span className="font-medium">Domaine:</span> {dataset.domain}</div>
173
+ <div><span className="font-medium">Source:</span> {dataset.source_name}</div>
174
+ <div><span className="font-medium">Points de données:</span> {dataset.data_points_count}</div>
175
+ <div><span className="font-medium">Dernière mise à jour:</span> {new Date(dataset.last_updated).toLocaleDateString('fr-FR')}</div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
181
+ <div className="px-4 py-3 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
182
+ <h3 className="font-semibold text-gray-900">Aperçu des données (50 premières lignes)</h3>
183
+ <span className="text-xs text-gray-500">{dataPoints.length} lignes affichées</span>
184
+ </div>
185
+
186
+ <div className="overflow-x-auto max-h-64">
187
+ <table className="min-w-full divide-y divide-gray-200">
188
+ <thead className="bg-gray-50 sticky top-0">
189
+ <tr>
190
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Pays</th>
191
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
192
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valeur</th>
193
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Unité</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody className="bg-white divide-y divide-gray-200">
197
+ {loadingData ? (
198
+ <tr>
199
+ <td colSpan="4" className="px-6 py-4 text-center text-sm text-gray-500">
200
+ Chargement des données...
201
+ </td>
202
+ </tr>
203
+ ) : dataPoints.length > 0 ? (
204
+ dataPoints.map((dp, idx) => (
205
+ <tr key={idx} className="hover:bg-gray-50">
206
+ <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{dp.country_name}</td>
207
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{dp.date}</td>
208
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{dp.value}</td>
209
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{dp.unit}</td>
210
+ </tr>
211
+ ))
212
+ ) : (
213
+ <tr>
214
+ <td colSpan="4" className="px-6 py-4 text-center text-sm text-gray-500">
215
+ Aucune donnée disponible pour l'aperçu.
216
+ </td>
217
+ </tr>
218
+ )}
219
+ </tbody>
220
+ </table>
221
+ </div>
222
+ </div>
223
+
224
+ <div className="flex justify-end space-x-3 mt-6">
225
+ <Button variant="outline" onClick={handleDownload} disabled={loadingData || dataPoints.length === 0}>
226
+ <Download className="h-4 w-4 mr-2" />
227
+ Télécharger CSV
228
+ </Button>
229
+ <Button
230
+ className="bg-purple-600 hover:bg-purple-700"
231
+ onClick={() => handleAnalyze(dataset)}
232
+ disabled={isAnalyzing}
233
+ >
234
+ {isAnalyzing ? (
235
+ <>
236
+ <RefreshCw className="h-4 w-4 mr-2 animate-spin" />
237
+ Analyse en cours...
238
+ </>
239
+ ) : (
240
+ <>
241
+ <Brain className="h-4 w-4 mr-2" />
242
+ Analyser les données
243
+ </>
244
+ )}
245
+ </Button>
246
+ </div>
247
+ </div>
248
+ </motion.div>
249
+ </motion.div>
250
+ )
251
+ }
252
+
253
+ return (
254
+ <div className="space-y-10 pb-20">
255
+ {/* En-tête */}
256
+ <motion.div
257
+ initial={{ opacity: 0, y: -20 }}
258
+ animate={{ opacity: 1, y: 0 }}
259
+ className="flex flex-col md:flex-row md:items-end md:justify-between gap-6"
260
+ >
261
+ <div>
262
+ <h1 className="text-4xl font-black text-foreground tracking-tight">Bibliothèque de <span className="text-gradient">Datasets</span></h1>
263
+ <p className="text-muted-foreground font-medium mt-2 text-lg">
264
+ Explorez les {datasets.length} sources de vérité disponibles sur le continent.
265
+ </p>
266
+ </div>
267
+ <div className="flex items-center space-x-3">
268
+ <Button
269
+ variant="ghost"
270
+ onClick={fetchDatasets}
271
+ disabled={loading}
272
+ className="rounded-2xl hover:bg-primary/10 text-primary font-bold"
273
+ >
274
+ <RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
275
+ Actualiser
276
+ </Button>
277
+ <Button className="rounded-2xl bg-gradient-to-r from-primary to-secondary text-white font-bold shadow-lg hover:shadow-primary/20 transition-all hover:scale-105">
278
+ <Upload className="h-4 w-4 mr-2" />
279
+ Importer CSV
280
+ </Button>
281
+ </div>
282
+ </motion.div>
283
+
284
+ {/* Filtres Glassmorphism */}
285
+ <motion.div
286
+ initial={{ opacity: 0, y: 20 }}
287
+ animate={{ opacity: 1, y: 0 }}
288
+ className="glass rounded-[2rem] p-6 shadow-xl"
289
+ >
290
+ <div className="flex flex-col lg:flex-row gap-6">
291
+ {/* Recherche */}
292
+ <div className="flex-1">
293
+ <div className="relative group">
294
+ <Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
295
+ <input
296
+ type="text"
297
+ placeholder="Rechercher par titre, description ou source..."
298
+ value={searchTerm}
299
+ onChange={(e) => setSearchTerm(e.target.value)}
300
+ className="w-full pl-12 pr-6 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-medium text-foreground"
301
+ />
302
+ </div>
303
+ </div>
304
+
305
+ {/* Filtre par domaine */}
306
+ <div className="lg:w-80">
307
+ <div className="relative group">
308
+ <Filter className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" />
309
+ <select
310
+ value={selectedDomain}
311
+ onChange={(e) => setSelectedDomain(e.target.value)}
312
+ className="w-full pl-12 pr-10 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-bold text-foreground appearance-none cursor-pointer"
313
+ >
314
+ {domains.map(domain => (
315
+ <option key={domain.value} value={domain.value}>
316
+ {domain.label}
317
+ </option>
318
+ ))}
319
+ </select>
320
+ <div className="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none text-muted-foreground">
321
+ <Download className="h-4 w-4 rotate-180" />
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </motion.div>
327
+
328
+ {/* Résultats */}
329
+ <div>
330
+ <div className="flex items-center justify-between mb-8 px-2">
331
+ <div className="flex items-center space-x-2">
332
+ <div className="w-2 h-2 rounded-full bg-primary animate-pulse"></div>
333
+ <p className="text-muted-foreground font-bold uppercase tracking-widest text-xs">
334
+ {filteredDatasets.length} résultat{filteredDatasets.length > 1 ? 's' : ''} trouvé{filteredDatasets.length > 1 ? 's' : ''}
335
+ </p>
336
+ </div>
337
+ </div>
338
+
339
+ {loading ? (
340
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
341
+ {[...Array(6)].map((_, i) => (
342
+ <div key={i} className="glass rounded-[2rem] p-8 animate-pulse">
343
+ <div className="h-6 bg-primary/10 rounded-xl w-3/4 mb-4"></div>
344
+ <div className="h-4 bg-primary/5 rounded-lg w-full mb-2"></div>
345
+ <div className="h-4 bg-primary/5 rounded-lg w-5/6 mb-6"></div>
346
+ <div className="h-12 bg-primary/10 rounded-2xl w-full"></div>
347
+ </div>
348
+ ))}
349
+ </div>
350
+ ) : (
351
+ <motion.div
352
+ initial={{ opacity: 0 }}
353
+ animate={{ opacity: 1 }}
354
+ className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
355
+ >
356
+ {filteredDatasets.map((dataset, index) => (
357
+ <motion.div
358
+ key={dataset.slug}
359
+ initial={{ opacity: 0, y: 20 }}
360
+ animate={{ opacity: 1, y: 0 }}
361
+ transition={{ delay: index * 0.05 }}
362
+ >
363
+ <DatasetCard
364
+ dataset={dataset}
365
+ onViewDetails={handleViewDetails}
366
+ />
367
+ </motion.div>
368
+ ))}
369
+ </motion.div>
370
+ )}
371
+
372
+ {!loading && filteredDatasets.length === 0 && (
373
+ <motion.div
374
+ initial={{ opacity: 0, scale: 0.9 }}
375
+ animate={{ opacity: 1, scale: 1 }}
376
+ className="text-center py-24 glass rounded-[3rem]"
377
+ >
378
+ <div className="w-24 h-24 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-6">
379
+ <Search className="h-10 w-10 text-primary" />
380
+ </div>
381
+ <h3 className="text-2xl font-black text-foreground mb-2">
382
+ Aucun dataset trouvé
383
+ </h3>
384
+ <p className="text-muted-foreground font-medium max-w-md mx-auto">
385
+ Nous n'avons trouvé aucun résultat pour votre recherche. Essayez d'utiliser des termes plus larges ou un autre domaine.
386
+ </p>
387
+ <Button
388
+ variant="ghost"
389
+ onClick={() => { setSearchTerm(''); setSelectedDomain('') }}
390
+ className="mt-6 text-primary font-bold hover:bg-primary/10 rounded-xl"
391
+ >
392
+ Réinitialiser les filtres
393
+ </Button>
394
+ </motion.div>
395
+ )}
396
+ </div>
397
+
398
+ {/* Modal de détails Premium */}
399
+ <AnimatePresence>
400
+ {selectedDataset && (
401
+ <DatasetDetails
402
+ dataset={selectedDataset}
403
+ onClose={() => setSelectedDataset(null)}
404
+ />
405
+ )}
406
+ </AnimatePresence>
407
+
408
+ {/* Loader d'analyse global */}
409
+ <AnimatePresence>
410
+ {isAnalyzing && !selectedDataset && (
411
+ <motion.div
412
+ initial={{ opacity: 0 }}
413
+ animate={{ opacity: 1 }}
414
+ exit={{ opacity: 0 }}
415
+ className="fixed inset-0 bg-background/80 backdrop-blur-xl flex items-center justify-center z-[100]"
416
+ >
417
+ <div className="flex flex-col items-center max-w-md text-center p-12">
418
+ <div className="relative w-24 h-24 mb-8">
419
+ <div className="absolute inset-0 border-4 border-primary/20 rounded-full"></div>
420
+ <div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
421
+ <Brain className="absolute inset-0 m-auto h-10 w-10 text-primary animate-pulse" />
422
+ </div>
423
+ <h3 className="text-3xl font-black text-foreground mb-4">Intelligence Artificielle en action</h3>
424
+ <p className="text-muted-foreground font-medium leading-relaxed">
425
+ Gemini analyse actuellement des milliers de points de données pour générer votre rapport stratégique.
426
+ </p>
427
+ <div className="mt-8 w-full bg-primary/10 h-2 rounded-full overflow-hidden">
428
+ <motion.div
429
+ initial={{ x: '-100%' }}
430
+ animate={{ x: '100%' }}
431
+ transition={{ repeat: Infinity, duration: 2, ease: "linear" }}
432
+ className="w-1/2 h-full bg-gradient-to-r from-transparent via-primary to-transparent"
433
+ />
434
+ </div>
435
+ </div>
436
+ </motion.div>
437
+ )}
438
+ </AnimatePresence>
439
+ </div>
440
+ )
441
+ }
442
+
443
+ export default Datasets
afridatahub-frontend/src/components/LandingPage.jsx ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Landing Page pour AfriDataHub
3
+ * Created by BlackBenAI Team - AfriDataHub Platform
4
+ */
5
+
6
+ import { motion } from 'framer-motion'
7
+ import { Button } from '@/components/ui/button'
8
+ import {
9
+ Globe,
10
+ Zap,
11
+ Shield,
12
+ BarChart3,
13
+ Cpu,
14
+ Users,
15
+ ArrowRight,
16
+ CheckCircle2,
17
+ Database,
18
+ TrendingUp,
19
+ Code
20
+ } from 'lucide-react'
21
+
22
+ const LandingPage = ({ onGetStarted, onLogin }) => {
23
+ const containerVariants = {
24
+ hidden: { opacity: 0 },
25
+ visible: {
26
+ opacity: 1,
27
+ transition: {
28
+ staggerChildren: 0.2
29
+ }
30
+ }
31
+ }
32
+
33
+ const itemVariants = {
34
+ hidden: { opacity: 0, y: 20 },
35
+ visible: { opacity: 1, y: 0 }
36
+ }
37
+
38
+ return (
39
+ <div className="min-h-screen bg-[#050505] text-white overflow-x-hidden">
40
+ {/* Background Elements */}
41
+ <div className="fixed inset-0 z-0">
42
+ <div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-primary/10 rounded-full blur-[120px] animate-pulse" />
43
+ <div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-secondary/10 rounded-full blur-[120px] animate-pulse" style={{ animationDelay: '2s' }} />
44
+ <div className="absolute top-[20%] right-[10%] w-[30%] h-[30%] bg-accent/5 rounded-full blur-[100px]" />
45
+ </div>
46
+
47
+ {/* Hero Section */}
48
+ <section className="relative z-10 pt-32 pb-20 px-6 lg:px-8 max-w-7xl mx-auto">
49
+ <motion.div
50
+ initial="hidden"
51
+ animate="visible"
52
+ variants={containerVariants}
53
+ className="text-center"
54
+ >
55
+ <motion.div variants={itemVariants} className="inline-flex items-center space-x-3 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
56
+ <span className="relative flex h-2 w-2">
57
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary opacity-75"></span>
58
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-secondary"></span>
59
+ </span>
60
+ <span className="text-xs font-black uppercase tracking-widest text-secondary">Propulsé par BlackBenAI</span>
61
+ </motion.div>
62
+
63
+ <motion.h1 variants={itemVariants} className="text-6xl md:text-8xl font-black tracking-tight mb-8 leading-[1.1]">
64
+ L'Émancipation <span className="text-gradient">Data</span> <br />
65
+ du Continent Africain
66
+ </motion.h1>
67
+
68
+ <motion.p variants={itemVariants} className="text-xl md:text-2xl text-white/60 max-w-3xl mx-auto mb-12 font-medium leading-relaxed">
69
+ AfriDataHub est la plateforme d'intelligence souveraine conçue pour transformer les données brutes en leviers de développement pour les 54 nations d'Afrique.
70
+ </motion.p>
71
+
72
+ <motion.div variants={itemVariants} className="flex flex-col sm:flex-row items-center justify-center gap-6">
73
+ <Button
74
+ onClick={onGetStarted}
75
+ className="px-10 py-8 rounded-2xl bg-gradient-to-r from-primary to-secondary text-white font-black text-xl shadow-2xl shadow-primary/20 hover:shadow-primary/40 transition-all hover:scale-105"
76
+ >
77
+ Commencer l'Aventure <ArrowRight className="ml-3 h-6 w-6" />
78
+ </Button>
79
+ <Button
80
+ variant="outline"
81
+ onClick={onLogin}
82
+ className="px-10 py-8 rounded-2xl border-white/10 bg-white/5 backdrop-blur-xl text-white font-black text-xl hover:bg-white/10 transition-all"
83
+ >
84
+ Se Connecter
85
+ </Button>
86
+ </motion.div>
87
+ </motion.div>
88
+
89
+ {/* Dashboard Preview Mockup */}
90
+ <motion.div
91
+ initial={{ opacity: 0, y: 100 }}
92
+ animate={{ opacity: 1, y: 0 }}
93
+ transition={{ duration: 1, delay: 0.5 }}
94
+ className="mt-24 relative"
95
+ >
96
+ <div className="glass rounded-[3rem] border border-white/20 p-4 shadow-[0_0_100px_rgba(var(--primary-rgb),0.1)]">
97
+ <div className="bg-[#0a0a0a] rounded-[2.5rem] overflow-hidden aspect-[16/9] flex items-center justify-center relative group">
98
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/20 via-transparent to-secondary/20 opacity-50 group-hover:opacity-70 transition-opacity" />
99
+ <div className="relative z-10 flex flex-col items-center">
100
+ <div className="grid grid-cols-3 gap-8 mb-8">
101
+ {[1, 2, 3].map(i => (
102
+ <div key={i} className="w-32 h-32 rounded-3xl bg-white/5 border border-white/10 animate-pulse" style={{ animationDelay: `${i * 0.2}s` }} />
103
+ ))}
104
+ </div>
105
+ <div className="w-64 h-4 bg-white/10 rounded-full animate-pulse" />
106
+ </div>
107
+ <div className="absolute bottom-8 left-8 right-8 h-32 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md" />
108
+ </div>
109
+ </div>
110
+ {/* Floating elements */}
111
+ <motion.div
112
+ animate={{ y: [0, -20, 0] }}
113
+ transition={{ duration: 4, repeat: Infinity }}
114
+ className="absolute -top-10 -left-10 glass p-6 rounded-3xl border border-white/20 hidden lg:block"
115
+ >
116
+ <TrendingUp className="h-10 w-10 text-secondary" />
117
+ </motion.div>
118
+ <motion.div
119
+ animate={{ y: [0, 20, 0] }}
120
+ transition={{ duration: 5, repeat: Infinity }}
121
+ className="absolute -bottom-10 -right-10 glass p-6 rounded-3xl border border-white/20 hidden lg:block"
122
+ >
123
+ <Cpu className="h-10 w-10 text-primary" />
124
+ </motion.div>
125
+ </motion.div>
126
+ </section>
127
+
128
+ {/* Features Grid */}
129
+ <section className="relative z-10 py-32 px-6 lg:px-8 max-w-7xl mx-auto">
130
+ <div className="text-center mb-20">
131
+ <h2 className="text-4xl md:text-5xl font-black mb-6">Une Technologie au Service de <span className="text-gradient">l'Impact</span></h2>
132
+ <p className="text-white/50 text-xl max-w-2xl mx-auto">Des outils de pointe pour décrypter les tendances socio-économiques du continent.</p>
133
+ </div>
134
+
135
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-10">
136
+ {[
137
+ {
138
+ icon: Cpu,
139
+ title: "Analyse par IA",
140
+ desc: "Des modèles de langage avancés pour interpréter les données et générer des insights stratégiques.",
141
+ color: "text-primary"
142
+ },
143
+ {
144
+ icon: Globe,
145
+ title: "Couverture Totale",
146
+ desc: "Accès centralisé aux indicateurs des 54 pays africains, de l'agriculture à l'économie numérique.",
147
+ color: "text-secondary"
148
+ },
149
+ {
150
+ icon: Code,
151
+ title: "API Développeur",
152
+ desc: "Intégrez nos flux de données directement dans vos applications via notre SDK robuste.",
153
+ color: "text-accent"
154
+ }
155
+ ].map((feature, i) => (
156
+ <motion.div
157
+ key={i}
158
+ whileHover={{ y: -10 }}
159
+ className="glass rounded-[2.5rem] p-10 border border-white/10 hover:border-white/20 transition-all group"
160
+ >
161
+ <div className={`w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-8 group-hover:scale-110 transition-transform`}>
162
+ <feature.icon className={`h-8 w-8 ${feature.color}`} />
163
+ </div>
164
+ <h3 className="text-2xl font-black mb-4">{feature.title}</h3>
165
+ <p className="text-white/50 leading-relaxed font-medium">{feature.desc}</p>
166
+ </motion.div>
167
+ ))}
168
+ </div>
169
+ </section>
170
+
171
+ {/* BlackBenAI Mission Section */}
172
+ <section className="relative z-10 py-32 bg-white/5 backdrop-blur-3xl border-y border-white/10">
173
+ <div className="max-w-7xl mx-auto px-6 lg:px-8 flex flex-col lg:flex-row items-center gap-20">
174
+ <div className="flex-1">
175
+ <div className="inline-flex items-center space-x-3 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 mb-8">
176
+ <Shield className="h-4 w-4 text-primary" />
177
+ <span className="text-xs font-black uppercase tracking-widest text-primary">Notre Vision</span>
178
+ </div>
179
+ <h2 className="text-5xl font-black mb-8 leading-tight">
180
+ Par <span className="text-gradient">BlackBenAI</span>, <br />
181
+ Pour l'Afrique.
182
+ </h2>
183
+ <p className="text-xl text-white/60 mb-10 leading-relaxed font-medium">
184
+ Nous croyons que la souveraineté numérique commence par la maîtrise des données. BlackBenAI développe des solutions d'intelligence artificielle éthiques et contextuelles pour accélérer la croissance du continent.
185
+ </p>
186
+ <div className="space-y-4">
187
+ {[
188
+ "Démocratisation de l'accès à l'information",
189
+ "Soutien aux décideurs et entrepreneurs",
190
+ "Innovation technologique inclusive"
191
+ ].map((item, i) => (
192
+ <div key={i} className="flex items-center space-x-3">
193
+ <CheckCircle2 className="h-6 w-6 text-secondary" />
194
+ <span className="text-lg font-bold text-white/80">{item}</span>
195
+ </div>
196
+ ))}
197
+ </div>
198
+ </div>
199
+ <div className="flex-1 relative">
200
+ <div className="relative z-10 glass rounded-[3rem] p-12 border border-white/20">
201
+ <div className="grid grid-cols-2 gap-8">
202
+ <div className="text-center">
203
+ <p className="text-5xl font-black text-gradient mb-2">54</p>
204
+ <p className="text-sm font-black uppercase tracking-widest text-white/40">Pays</p>
205
+ </div>
206
+ <div className="text-center">
207
+ <p className="text-5xl font-black text-gradient mb-2">1M+</p>
208
+ <p className="text-sm font-black uppercase tracking-widest text-white/40">Data Points</p>
209
+ </div>
210
+ <div className="text-center">
211
+ <p className="text-5xl font-black text-gradient mb-2">24/7</p>
212
+ <p className="text-sm font-black uppercase tracking-widest text-white/40">Monitoring</p>
213
+ </div>
214
+ <div className="text-center">
215
+ <p className="text-5xl font-black text-gradient mb-2">AI</p>
216
+ <p className="text-sm font-black uppercase tracking-widest text-white/40">Powered</p>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ {/* Decorative circles */}
221
+ <div className="absolute -top-10 -right-10 w-40 h-40 bg-primary/20 rounded-full blur-3xl" />
222
+ <div className="absolute -bottom-10 -left-10 w-40 h-40 bg-secondary/20 rounded-full blur-3xl" />
223
+ </div>
224
+ </div>
225
+ </section>
226
+
227
+ {/* CTA Section */}
228
+ <section className="relative z-10 py-32 px-6 lg:px-8 max-w-5xl mx-auto text-center">
229
+ <motion.div
230
+ whileInView={{ opacity: 1, scale: 1 }}
231
+ initial={{ opacity: 0, scale: 0.9 }}
232
+ className="glass rounded-[4rem] p-16 md:p-24 border border-white/20 relative overflow-hidden"
233
+ >
234
+ <div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-primary/10 to-secondary/10 opacity-50" />
235
+ <div className="relative z-10">
236
+ <h2 className="text-4xl md:text-6xl font-black mb-8">Prêt à explorer le <br /><span className="text-gradient">futur de l'Afrique ?</span></h2>
237
+ <p className="text-xl text-white/60 mb-12 max-w-2xl mx-auto font-medium">
238
+ Rejoignez des milliers de chercheurs, développeurs et décideurs qui utilisent déjà AfriDataHub.
239
+ </p>
240
+ <Button
241
+ onClick={onGetStarted}
242
+ className="px-12 py-8 rounded-2xl bg-white text-black font-black text-xl hover:bg-white/90 transition-all hover:scale-105 shadow-2xl"
243
+ >
244
+ Créer mon compte gratuit
245
+ </Button>
246
+ </div>
247
+ </motion.div>
248
+ </section>
249
+
250
+ {/* Footer */}
251
+ <footer className="relative z-10 py-20 px-6 lg:px-8 border-t border-white/10">
252
+ <div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-10">
253
+ <div className="flex items-center space-x-3">
254
+ <div className="p-2 bg-gradient-to-br from-primary to-secondary rounded-xl">
255
+ <Globe className="h-6 w-6 text-white" />
256
+ </div>
257
+ <span className="text-2xl font-black tracking-tight text-gradient">AfriDataHub</span>
258
+ </div>
259
+
260
+ <p className="text-white/40 font-medium text-center">
261
+ © 2025 AfriDataHub. Développé avec ❤️ par <span className="text-white font-bold">BlackBenAI</span> pour l'émancipation du continent.
262
+ </p>
263
+
264
+ <div className="flex space-x-6">
265
+ <Users className="h-6 w-6 text-white/40 hover:text-white transition-colors cursor-pointer" />
266
+ <Database className="h-6 w-6 text-white/40 hover:text-white transition-colors cursor-pointer" />
267
+ <Shield className="h-6 w-6 text-white/40 hover:text-white transition-colors cursor-pointer" />
268
+ </div>
269
+ </div>
270
+ </footer>
271
+ </div>
272
+ )
273
+ }
274
+
275
+ export default LandingPage
afridatahub-frontend/src/components/Login.jsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { motion, AnimatePresence } from 'framer-motion'
3
+ import { login } from '../services/auth'
4
+ import { Button } from '@/components/ui/button'
5
+ import {
6
+ User,
7
+ Lock,
8
+ ArrowRight,
9
+ Globe,
10
+ ShieldCheck,
11
+ Zap,
12
+ Loader2
13
+ } from 'lucide-react'
14
+
15
+ const Login = ({ onLoginSuccess, onNavigateToRegister, onNavigateToLanding }) => {
16
+ const [username, setUsername] = useState('')
17
+ const [password, setPassword] = useState('')
18
+ const [error, setError] = useState('')
19
+ const [loading, setLoading] = useState(false)
20
+
21
+ const handleSubmit = async (e) => {
22
+ e.preventDefault()
23
+ setError('')
24
+ setLoading(true)
25
+ try {
26
+ const data = await login(username, password)
27
+ localStorage.setItem('token', data.token)
28
+ localStorage.setItem('user', JSON.stringify(data.user))
29
+ onLoginSuccess(data.user)
30
+ } catch (err) {
31
+ setError('Identifiants incorrects. Veuillez réessayer.')
32
+ } finally {
33
+ setLoading(false)
34
+ }
35
+ }
36
+
37
+ return (
38
+ <div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[#050505]">
39
+ {/* Background Mesh Gradients */}
40
+ <div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/20 rounded-full blur-[120px] animate-pulse" />
41
+ <div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-secondary/20 rounded-full blur-[120px] animate-pulse" style={{ animationDelay: '2s' }} />
42
+
43
+ <div className="relative z-10 w-full max-w-6xl px-4 flex flex-col lg:flex-row items-center gap-16">
44
+ {/* Left Side: Branding & Info */}
45
+ <motion.div
46
+ initial={{ opacity: 0, x: -50 }}
47
+ animate={{ opacity: 1, x: 0 }}
48
+ transition={{ duration: 0.8, ease: "easeOut" }}
49
+ className="flex-1 text-center lg:text-left hidden lg:block"
50
+ >
51
+ <div className="inline-flex items-center space-x-3 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
52
+ <Zap className="h-4 w-4 text-secondary" />
53
+ <span className="text-xs font-black uppercase tracking-widest text-secondary">Intelligence de Données Africaines</span>
54
+ </div>
55
+ <h1 className="text-6xl xl:text-7xl font-black text-white leading-tight mb-6">
56
+ Reconnectez-vous à <br />
57
+ <span className="text-gradient">l'Afrique de Demain</span>
58
+ </h1>
59
+ <p className="text-xl text-white/60 font-medium max-w-xl mb-10">
60
+ Accédez à la plateforme d'analyse de données la plus avancée du continent.
61
+ Visualisez, analysez et agissez en temps réel.
62
+ </p>
63
+
64
+ <div className="grid grid-cols-2 gap-6 max-w-md">
65
+ <div className="p-6 rounded-3xl bg-white/5 border border-white/10">
66
+ <ShieldCheck className="h-8 w-8 text-primary mb-4" />
67
+ <h3 className="text-white font-bold">Sécurisé</h3>
68
+ <p className="text-white/40 text-sm">Protection de données de niveau bancaire.</p>
69
+ </div>
70
+ <div className="p-6 rounded-3xl bg-white/5 border border-white/10">
71
+ <Globe className="h-8 w-8 text-secondary mb-4" />
72
+ <h3 className="text-white font-bold">Continental</h3>
73
+ <p className="text-white/40 text-sm">Couverture totale des 54 pays.</p>
74
+ </div>
75
+ </div>
76
+ </motion.div>
77
+
78
+ {/* Right Side: Login Card */}
79
+ <motion.div
80
+ initial={{ opacity: 0, scale: 0.9 }}
81
+ animate={{ opacity: 1, scale: 1 }}
82
+ transition={{ duration: 0.6, delay: 0.2 }}
83
+ className="w-full max-w-[480px]"
84
+ >
85
+ <div className="glass rounded-[3rem] p-10 md:p-12 border border-white/20 shadow-2xl relative overflow-hidden">
86
+ <div className="absolute top-0 right-0 p-8 opacity-5 pointer-events-none">
87
+ <Lock className="w-32 h-32 text-white" />
88
+ </div>
89
+
90
+ <div className="relative z-10">
91
+ <div className="mb-10 text-center lg:text-left">
92
+ <div className="lg:hidden flex justify-center mb-6">
93
+ <div className="p-3 bg-gradient-to-br from-primary to-secondary rounded-2xl shadow-lg">
94
+ <Globe className="h-8 w-8 text-white" />
95
+ </div>
96
+ </div>
97
+ <h2 className="text-3xl font-black text-white mb-2">Bienvenue</h2>
98
+ <p className="text-white/50 font-medium">Entrez vos identifiants pour continuer</p>
99
+ </div>
100
+
101
+ <form onSubmit={handleSubmit} className="space-y-6">
102
+ <div className="space-y-4">
103
+ <div className="relative group">
104
+ <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
105
+ <User className="h-5 w-5 text-white/30 group-focus-within:text-primary transition-colors" />
106
+ </div>
107
+ <input
108
+ type="text"
109
+ required
110
+ placeholder="Nom d'utilisateur"
111
+ value={username}
112
+ onChange={(e) => setUsername(e.target.value)}
113
+ className="w-full pl-12 pr-4 py-4 bg-white/5 border border-white/10 rounded-2xl text-white placeholder:text-white/20 focus:ring-4 focus:ring-primary/10 focus:border-primary/30 outline-none transition-all font-medium"
114
+ />
115
+ </div>
116
+
117
+ <div className="relative group">
118
+ <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
119
+ <Lock className="h-5 w-5 text-white/30 group-focus-within:text-secondary transition-colors" />
120
+ </div>
121
+ <input
122
+ type="password"
123
+ required
124
+ placeholder="Mot de passe"
125
+ value={password}
126
+ onChange={(e) => setPassword(e.target.value)}
127
+ className="w-full pl-12 pr-4 py-4 bg-white/5 border border-white/10 rounded-2xl text-white placeholder:text-white/20 focus:ring-4 focus:ring-secondary/10 focus:border-secondary/30 outline-none transition-all font-medium"
128
+ />
129
+ </div>
130
+ </div>
131
+
132
+ <AnimatePresence mode="wait">
133
+ {error && (
134
+ <motion.div
135
+ initial={{ opacity: 0, height: 0 }}
136
+ animate={{ opacity: 1, height: 'auto' }}
137
+ exit={{ opacity: 0, height: 0 }}
138
+ className="p-4 bg-red-500/10 border border-red-500/20 rounded-2xl text-red-400 text-sm font-bold text-center"
139
+ >
140
+ {error}
141
+ </motion.div>
142
+ )}
143
+ </AnimatePresence>
144
+
145
+ <div className="flex items-center justify-between text-sm">
146
+ <label className="flex items-center space-x-2 cursor-pointer group">
147
+ <input type="checkbox" className="w-4 h-4 rounded border-white/10 bg-white/5 text-primary focus:ring-primary/20" />
148
+ <span className="text-white/40 group-hover:text-white/60 transition-colors">Se souvenir de moi</span>
149
+ </label>
150
+ <button type="button" className="text-primary font-bold hover:text-primary/80 transition-colors">Oublié ?</button>
151
+ </div>
152
+
153
+ <Button
154
+ type="submit"
155
+ disabled={loading}
156
+ className="w-full py-7 rounded-2xl bg-gradient-to-r from-primary to-secondary text-white font-black text-lg shadow-xl shadow-primary/20 hover:shadow-primary/40 transition-all hover:scale-[1.02] active:scale-[0.98]"
157
+ >
158
+ {loading ? (
159
+ <Loader2 className="h-6 w-6 animate-spin" />
160
+ ) : (
161
+ <span className="flex items-center">
162
+ Se connecter <ArrowRight className="ml-2 h-5 w-5" />
163
+ </span>
164
+ )}
165
+ </Button>
166
+ </form>
167
+
168
+ <div className="mt-10 text-center space-y-4">
169
+ <p className="text-white/40 font-medium">
170
+ Pas encore de compte ?{' '}
171
+ <button
172
+ onClick={onNavigateToRegister}
173
+ className="text-white font-black hover:text-secondary transition-colors underline underline-offset-4"
174
+ >
175
+ Créer un profil
176
+ </button>
177
+ </p>
178
+ <button
179
+ onClick={onNavigateToLanding}
180
+ className="text-white/20 hover:text-white/40 text-sm font-bold transition-colors"
181
+ >
182
+ ← Retour à l'accueil
183
+ </button>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </motion.div>
188
+ </div>
189
+ </div>
190
+ )
191
+ }
192
+
193
+ export default Login
194
+
afridatahub-frontend/src/components/Navigation.jsx ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Composant de navigation pour AfriDataHub
3
+ * Created by Marino ATOHOUN - AfriDataHub Platform
4
+ */
5
+
6
+ import { useState } from 'react'
7
+ import { motion, AnimatePresence } from 'framer-motion'
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ BarChart3,
11
+ Database,
12
+ AlertTriangle,
13
+ User,
14
+ Menu,
15
+ X,
16
+ Globe,
17
+ TrendingUp
18
+ } from 'lucide-react'
19
+
20
+ const Navigation = ({ currentPage, onPageChange, user, onLogout }) => {
21
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
22
+
23
+ const menuItems = [
24
+ { id: 'dashboard', label: 'Dashboard', icon: BarChart3 },
25
+ { id: 'datasets', label: 'Datasets', icon: Database },
26
+ { id: 'analytics', label: 'Analyse', icon: TrendingUp },
27
+ { id: 'alerts', label: 'Alertes', icon: AlertTriangle },
28
+ { id: 'api-docs', label: 'API', icon: Globe },
29
+ { id: 'profile', label: 'Profil', icon: User },
30
+ ]
31
+
32
+ const handlePageChange = (pageId) => {
33
+ onPageChange(pageId)
34
+ setIsMobileMenuOpen(false)
35
+ }
36
+
37
+ return (
38
+ <nav className="sticky top-0 z-50 glass border-b border-white/20">
39
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
40
+ <div className="flex justify-between h-20">
41
+ {/* Logo et titre */}
42
+ <div className="flex items-center">
43
+ <div className="flex-shrink-0 flex items-center group cursor-pointer" onClick={() => handlePageChange('dashboard')}>
44
+ <div className="p-2 bg-gradient-to-br from-primary to-secondary rounded-xl shadow-lg group-hover:scale-110 transition-transform duration-300">
45
+ <Globe className="h-6 w-6 text-white animate-pulse" />
46
+ </div>
47
+ <span className="ml-3 text-2xl font-black tracking-tight text-gradient">
48
+ AfriDataHub
49
+ </span>
50
+ </div>
51
+ </div>
52
+
53
+ {/* Menu desktop */}
54
+ <div className="hidden md:flex items-center space-x-2">
55
+ {menuItems.map((item) => {
56
+ const Icon = item.icon
57
+ const isActive = currentPage === item.id
58
+ return (
59
+ <Button
60
+ key={item.id}
61
+ variant="ghost"
62
+ onClick={() => handlePageChange(item.id)}
63
+ className={`relative px-4 py-2 rounded-xl transition-all duration-300 group ${isActive
64
+ ? 'text-primary bg-primary/10'
65
+ : 'text-muted-foreground hover:text-primary hover:bg-primary/5'
66
+ }`}
67
+ >
68
+ <Icon className={`h-4 w-4 mr-2 ${isActive ? 'scale-110' : 'group-hover:scale-110'} transition-transform`} />
69
+ <span className="font-semibold">{item.label}</span>
70
+ {isActive && (
71
+ <motion.div
72
+ layoutId="nav-active"
73
+ className="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-r from-primary to-secondary rounded-full"
74
+ />
75
+ )}
76
+ </Button>
77
+ )
78
+ })}
79
+ {user && (
80
+ <div className="flex items-center ml-6 pl-6 border-l border-white/20">
81
+ <div className="flex items-center space-x-3 mr-4">
82
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-white font-bold text-xs shadow-md">
83
+ {user.username.charAt(0).toUpperCase()}
84
+ </div>
85
+ <span className="text-sm font-medium text-foreground/80">
86
+ {user.username}
87
+ </span>
88
+ </div>
89
+ <Button
90
+ variant="ghost"
91
+ onClick={onLogout}
92
+ className="rounded-xl text-destructive hover:bg-destructive/10 transition-colors"
93
+ >
94
+ Déconnexion
95
+ </Button>
96
+ </div>
97
+ )}
98
+ </div>
99
+
100
+ {/* Bouton menu mobile */}
101
+ <div className="md:hidden flex items-center">
102
+ <Button
103
+ variant="ghost"
104
+ onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
105
+ className="rounded-xl text-foreground hover:bg-primary/10"
106
+ >
107
+ {isMobileMenuOpen ? (
108
+ <X className="h-6 w-6" />
109
+ ) : (
110
+ <Menu className="h-6 w-6" />
111
+ )}
112
+ </Button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ {/* Menu mobile */}
118
+ <AnimatePresence>
119
+ {isMobileMenuOpen && (
120
+ <motion.div
121
+ initial={{ opacity: 0, height: 0 }}
122
+ animate={{ opacity: 1, height: 'auto' }}
123
+ exit={{ opacity: 0, height: 0 }}
124
+ className="md:hidden glass border-t border-white/20 overflow-hidden"
125
+ >
126
+ <div className="px-4 pt-4 pb-6 space-y-2">
127
+ {menuItems.map((item) => {
128
+ const Icon = item.icon
129
+ const isActive = currentPage === item.id
130
+ return (
131
+ <Button
132
+ key={item.id}
133
+ variant="ghost"
134
+ onClick={() => handlePageChange(item.id)}
135
+ className={`w-full justify-start flex items-center space-x-3 p-4 rounded-xl transition-all ${isActive
136
+ ? 'bg-primary/20 text-primary shadow-inner'
137
+ : 'text-muted-foreground hover:bg-primary/10 hover:text-primary'
138
+ }`}
139
+ >
140
+ <Icon className="h-5 w-5" />
141
+ <span className="font-bold">{item.label}</span>
142
+ </Button>
143
+ )
144
+ })}
145
+ {user && (
146
+ <div className="pt-4 mt-4 border-t border-white/20">
147
+ <Button
148
+ variant="ghost"
149
+ onClick={onLogout}
150
+ className="w-full justify-start flex items-center space-x-3 p-4 rounded-xl text-destructive hover:bg-destructive/10"
151
+ >
152
+ <X className="h-5 w-5" />
153
+ <span className="font-bold">Déconnexion</span>
154
+ </Button>
155
+ </div>
156
+ )}
157
+ </div>
158
+ </motion.div>
159
+ )}
160
+ </AnimatePresence>
161
+ </nav>
162
+ )
163
+ }
164
+
165
+ export default Navigation
166
+
afridatahub-frontend/src/components/Profile.jsx ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { motion, AnimatePresence } from 'framer-motion'
3
+ import { Button } from '@/components/ui/button'
4
+ import {
5
+ User,
6
+ Mail,
7
+ Phone,
8
+ Building,
9
+ MapPin,
10
+ Bell,
11
+ Settings,
12
+ Save,
13
+ Edit,
14
+ Shield,
15
+ Download,
16
+ Trash2,
17
+ CheckCircle2,
18
+ Globe,
19
+ Briefcase,
20
+ Info
21
+ } from 'lucide-react'
22
+
23
+ import { updateProfile } from '../services/auth'
24
+
25
+ const Profile = ({ user, onUpdateUser }) => {
26
+ const [isEditing, setIsEditing] = useState(false)
27
+ const [loading, setLoading] = useState(false)
28
+ const [error, setError] = useState(null)
29
+ const [profile, setProfile] = useState({
30
+ firstName: user?.first_name || '',
31
+ lastName: user?.last_name || '',
32
+ email: user?.email || '',
33
+ phone: user?.profile?.phone_number || '',
34
+ organization: user?.profile?.organization || '',
35
+ country: user?.profile?.country || '',
36
+ bio: user?.profile?.bio || '',
37
+ interests: user?.profile?.interests || [],
38
+ notifications: {
39
+ email: user?.profile?.notification_preference === 'email',
40
+ push: user?.profile?.notification_preference === 'push',
41
+ alerts: user?.profile?.enable_alerts || false
42
+ }
43
+ })
44
+
45
+ const countries = [
46
+ { code: 'BJ', name: 'Bénin' },
47
+ { code: 'NG', name: 'Nigéria' },
48
+ { code: 'GH', name: 'Ghana' },
49
+ { code: 'CI', name: 'Côte d\'Ivoire' },
50
+ { code: 'SN', name: 'Sénégal' },
51
+ { code: 'KE', name: 'Kenya' },
52
+ { code: 'ZA', name: 'Afrique du Sud' },
53
+ { code: 'MA', name: 'Maroc' },
54
+ { code: 'EG', name: 'Égypte' },
55
+ { code: 'ET', name: 'Éthiopie' },
56
+ ]
57
+
58
+ const interests = [
59
+ { id: 'agriculture', label: 'Agriculture', icon: '🌾' },
60
+ { id: 'health', label: 'Santé', icon: '🏥' },
61
+ { id: 'economy', label: 'Économie', icon: '💰' },
62
+ { id: 'weather', label: 'Météo', icon: '🌤️' },
63
+ { id: 'energy', label: 'Énergie', icon: '⚡' },
64
+ { id: 'education', label: 'Éducation', icon: '📚' },
65
+ { id: 'population', label: 'Population', icon: '👥' },
66
+ { id: 'environment', label: 'Environnement', icon: '🌍' },
67
+ ]
68
+
69
+ const handleSave = async () => {
70
+ setLoading(true)
71
+ setError(null)
72
+ try {
73
+ const updatedData = {
74
+ first_name: profile.firstName,
75
+ last_name: profile.lastName,
76
+ email: profile.email,
77
+ profile: {
78
+ phone_number: profile.phone,
79
+ organization: profile.organization,
80
+ country: profile.country,
81
+ bio: profile.bio,
82
+ interests: profile.interests,
83
+ notification_preference: profile.notifications.email ? 'email' : (profile.notifications.push ? 'push' : 'none'),
84
+ enable_alerts: profile.notifications.alerts
85
+ }
86
+ }
87
+ const updatedUser = await updateProfile(updatedData)
88
+ onUpdateUser(updatedUser)
89
+ setIsEditing(false)
90
+ } catch (err) {
91
+ console.error('Error updating profile:', err)
92
+ setError('Erreur lors de la mise à jour du profil. Veuillez réessayer.')
93
+ } finally {
94
+ setLoading(false)
95
+ }
96
+ }
97
+
98
+ const toggleInterest = (interestId) => {
99
+ if (!isEditing) return
100
+ setProfile(prev => ({
101
+ ...prev,
102
+ interests: prev.interests.includes(interestId)
103
+ ? prev.interests.filter(id => id !== interestId)
104
+ : [...prev.interests, interestId]
105
+ }))
106
+ }
107
+
108
+ return (
109
+ <div className="space-y-10 pb-20">
110
+ {/* En-tête */}
111
+ <motion.div
112
+ initial={{ opacity: 0, y: -20 }}
113
+ animate={{ opacity: 1, y: 0 }}
114
+ className="flex flex-col md:flex-row md:items-center md:justify-between gap-6"
115
+ >
116
+ <div>
117
+ <h1 className="text-4xl font-black text-foreground tracking-tight">Mon <span className="text-gradient">Profil</span></h1>
118
+ <p className="text-muted-foreground font-medium mt-2 text-lg">
119
+ Gérez vos informations personnelles et vos préférences d'analyse.
120
+ </p>
121
+ </div>
122
+ <div className="flex flex-col items-end gap-2">
123
+ <Button
124
+ onClick={() => isEditing ? handleSave() : setIsEditing(true)}
125
+ disabled={loading}
126
+ className={`px-8 py-6 rounded-2xl font-bold text-white shadow-lg transition-all hover:scale-105 ${isEditing ? 'bg-green-600 hover:bg-green-700 shadow-green-500/20' : 'bg-primary hover:bg-primary/90 shadow-primary/20'
127
+ } ${loading ? 'opacity-70 cursor-not-allowed' : ''}`}
128
+ >
129
+ {loading ? (
130
+ <div className="h-5 w-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
131
+ ) : isEditing ? (
132
+ <Save className="h-5 w-5 mr-2" />
133
+ ) : (
134
+ <Edit className="h-5 w-5 mr-2" />
135
+ )}
136
+ {loading ? 'Traitement...' : isEditing ? 'Sauvegarder' : 'Modifier le Profil'}
137
+ </Button>
138
+ {error && <p className="text-red-500 text-sm font-bold">{error}</p>}
139
+ </div>
140
+ </motion.div>
141
+
142
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
143
+ {/* Informations personnelles */}
144
+ <motion.div
145
+ initial={{ opacity: 0, x: -20 }}
146
+ animate={{ opacity: 1, x: 0 }}
147
+ className="lg:col-span-2 space-y-8"
148
+ >
149
+ <div className="glass rounded-[3rem] p-10 border border-white/20 relative overflow-hidden">
150
+ <div className="absolute top-0 right-0 p-8 opacity-5 pointer-events-none">
151
+ <User className="w-48 h-48 text-primary" />
152
+ </div>
153
+
154
+ <div className="relative z-10">
155
+ <h2 className="text-2xl font-black text-foreground mb-8 flex items-center">
156
+ <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center mr-4">
157
+ <User className="h-5 w-5 text-primary" />
158
+ </div>
159
+ Informations Générales
160
+ </h2>
161
+
162
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
163
+ <div className="space-y-2">
164
+ <label className="text-sm font-black uppercase tracking-widest text-muted-foreground/60 ml-1">Prénom</label>
165
+ <input
166
+ type="text"
167
+ value={profile.firstName}
168
+ onChange={(e) => setProfile(prev => ({ ...prev, firstName: e.target.value }))}
169
+ disabled={!isEditing}
170
+ className="w-full px-5 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 outline-none transition-all font-medium disabled:opacity-60"
171
+ />
172
+ </div>
173
+
174
+ <div className="space-y-2">
175
+ <label className="text-sm font-black uppercase tracking-widest text-muted-foreground/60 ml-1">Nom</label>
176
+ <input
177
+ type="text"
178
+ value={profile.lastName}
179
+ onChange={(e) => setProfile(prev => ({ ...prev, lastName: e.target.value }))}
180
+ disabled={!isEditing}
181
+ className="w-full px-5 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 outline-none transition-all font-medium disabled:opacity-60"
182
+ />
183
+ </div>
184
+
185
+ <div className="space-y-2">
186
+ <label className="text-sm font-black uppercase tracking-widest text-muted-foreground/60 ml-1 flex items-center">
187
+ <Mail className="h-3 w-3 mr-2" /> Email
188
+ </label>
189
+ <input
190
+ type="email"
191
+ value={profile.email}
192
+ onChange={(e) => setProfile(prev => ({ ...prev, email: e.target.value }))}
193
+ disabled={!isEditing}
194
+ className="w-full px-5 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 outline-none transition-all font-medium disabled:opacity-60"
195
+ />
196
+ </div>
197
+
198
+ <div className="space-y-2">
199
+ <label className="text-sm font-black uppercase tracking-widest text-muted-foreground/60 ml-1 flex items-center">
200
+ <Phone className="h-3 w-3 mr-2" /> Téléphone
201
+ </label>
202
+ <input
203
+ type="tel"
204
+ value={profile.phone}
205
+ onChange={(e) => setProfile(prev => ({ ...prev, phone: e.target.value }))}
206
+ disabled={!isEditing}
207
+ className="w-full px-5 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 outline-none transition-all font-medium disabled:opacity-60"
208
+ />
209
+ </div>
210
+
211
+ <div className="space-y-2">
212
+ <label className="text-sm font-black uppercase tracking-widest text-muted-foreground/60 ml-1 flex items-center">
213
+ <Briefcase className="h-3 w-3 mr-2" /> Organisation
214
+ </label>
215
+ <input
216
+ type="text"
217
+ value={profile.organization}
218
+ onChange={(e) => setProfile(prev => ({ ...prev, organization: e.target.value }))}
219
+ disabled={!isEditing}
220
+ className="w-full px-5 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 outline-none transition-all font-medium disabled:opacity-60"
221
+ />
222
+ </div>
223
+
224
+ <div className="space-y-2">
225
+ <label className="text-sm font-black uppercase tracking-widest text-muted-foreground/60 ml-1 flex items-center">
226
+ <Globe className="h-3 w-3 mr-2" /> Pays
227
+ </label>
228
+ <select
229
+ value={profile.country}
230
+ onChange={(e) => setProfile(prev => ({ ...prev, country: e.target.value }))}
231
+ disabled={!isEditing}
232
+ className="w-full px-5 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 outline-none transition-all font-medium disabled:opacity-60 appearance-none"
233
+ >
234
+ {countries.map(country => (
235
+ <option key={country.code} value={country.code}>
236
+ {country.name}
237
+ </option>
238
+ ))}
239
+ </select>
240
+ </div>
241
+ </div>
242
+
243
+ <div className="mt-8 space-y-2">
244
+ <label className="text-sm font-black uppercase tracking-widest text-muted-foreground/60 ml-1 flex items-center">
245
+ <Info className="h-3 w-3 mr-2" /> Biographie
246
+ </label>
247
+ <textarea
248
+ value={profile.bio}
249
+ onChange={(e) => setProfile(prev => ({ ...prev, bio: e.target.value }))}
250
+ disabled={!isEditing}
251
+ rows={4}
252
+ className="w-full px-5 py-4 bg-white/50 border border-white/20 rounded-2xl focus:ring-4 focus:ring-primary/10 outline-none transition-all font-medium disabled:opacity-60 resize-none"
253
+ placeholder="Parlez-nous de votre intérêt pour les données africaines..."
254
+ />
255
+ </div>
256
+ </div>
257
+ </div>
258
+
259
+ {/* Domaines d'intérêt */}
260
+ <div className="glass rounded-[3rem] p-10 border border-white/20">
261
+ <h2 className="text-2xl font-black text-foreground mb-8 flex items-center">
262
+ <div className="w-10 h-10 rounded-xl bg-secondary/10 flex items-center justify-center mr-4">
263
+ <Globe className="h-5 w-5 text-secondary" />
264
+ </div>
265
+ Domaines d'Intérêt
266
+ </h2>
267
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
268
+ {interests.map(interest => (
269
+ <button
270
+ key={interest.id}
271
+ onClick={() => toggleInterest(interest.id)}
272
+ disabled={!isEditing}
273
+ className={`p-6 rounded-3xl border transition-all flex flex-col items-center text-center gap-3 group ${profile.interests.includes(interest.id)
274
+ ? 'bg-secondary text-white border-secondary shadow-lg shadow-secondary/20'
275
+ : 'bg-white/50 border-white/20 hover:border-secondary/30'
276
+ } ${!isEditing && 'cursor-default'}`}
277
+ >
278
+ <span className="text-3xl group-hover:scale-110 transition-transform">{interest.icon}</span>
279
+ <span className={`text-xs font-black uppercase tracking-widest ${profile.interests.includes(interest.id) ? 'text-white' : 'text-muted-foreground'
280
+ }`}>
281
+ {interest.label}
282
+ </span>
283
+ {profile.interests.includes(interest.id) && (
284
+ <CheckCircle2 className="h-4 w-4 absolute top-4 right-4 text-white" />
285
+ )}
286
+ </button>
287
+ ))}
288
+ </div>
289
+ </div>
290
+ </motion.div>
291
+
292
+ {/* Sidebar: Préférences & Paramètres */}
293
+ <motion.div
294
+ initial={{ opacity: 0, x: 20 }}
295
+ animate={{ opacity: 1, x: 0 }}
296
+ className="space-y-8"
297
+ >
298
+ {/* Notifications */}
299
+ <div className="glass rounded-[3rem] p-10 border border-white/20">
300
+ <h3 className="text-xl font-black text-foreground mb-8 flex items-center">
301
+ <Bell className="h-5 w-5 mr-3 text-primary" />
302
+ Notifications
303
+ </h3>
304
+ <div className="space-y-6">
305
+ {[
306
+ { id: 'email', label: 'Alertes par Email', desc: 'Recevez les mises à jour critiques.' },
307
+ { id: 'push', label: 'Notifications Push', desc: 'Alertes en temps réel sur navigateur.' },
308
+ { id: 'alerts', label: 'Alertes Automatiques', desc: 'Basées sur vos intérêts.' }
309
+ ].map((notif) => (
310
+ <div key={notif.id} className="flex items-center justify-between group">
311
+ <div className="flex-1">
312
+ <p className="text-sm font-bold text-foreground group-hover:text-primary transition-colors">{notif.label}</p>
313
+ <p className="text-xs text-muted-foreground">{notif.desc}</p>
314
+ </div>
315
+ <label className="relative inline-flex items-center cursor-pointer">
316
+ <input
317
+ type="checkbox"
318
+ checked={profile.notifications[notif.id]}
319
+ onChange={(e) => setProfile(prev => ({
320
+ ...prev,
321
+ notifications: { ...prev.notifications, [notif.id]: e.target.checked }
322
+ }))}
323
+ disabled={!isEditing}
324
+ className="sr-only peer"
325
+ />
326
+ <div className="w-11 h-6 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
327
+ </label>
328
+ </div>
329
+ ))}
330
+ </div>
331
+ </div>
332
+
333
+ {/* Paramètres de Sécurité */}
334
+ <div className="glass rounded-[3rem] p-10 border border-white/20">
335
+ <h3 className="text-xl font-black text-foreground mb-8 flex items-center">
336
+ <Settings className="h-5 w-5 mr-3 text-secondary" />
337
+ Paramètres
338
+ </h3>
339
+ <div className="space-y-4">
340
+ <Button variant="outline" className="w-full justify-start py-6 rounded-2xl border-white/20 hover:bg-white/10 font-bold group">
341
+ <Shield className="h-4 w-4 mr-3 text-muted-foreground group-hover:text-secondary transition-colors" />
342
+ Changer le mot de passe
343
+ </Button>
344
+ <Button variant="outline" className="w-full justify-start py-6 rounded-2xl border-white/20 hover:bg-white/10 font-bold group">
345
+ <Download className="h-4 w-4 mr-3 text-muted-foreground group-hover:text-secondary transition-colors" />
346
+ Exporter mes données
347
+ </Button>
348
+ <div className="pt-4 border-t border-white/10">
349
+ <Button variant="ghost" className="w-full justify-start py-6 rounded-2xl text-red-500 hover:bg-red-500/10 font-bold group">
350
+ <Trash2 className="h-4 w-4 mr-3 text-red-400 group-hover:scale-110 transition-transform" />
351
+ Supprimer le compte
352
+ </Button>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ </motion.div>
357
+ </div>
358
+ </div>
359
+ )
360
+ }
361
+
362
+ export default Profile
363
+
364
+
afridatahub-frontend/src/components/Register.jsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { motion, AnimatePresence } from 'framer-motion'
3
+ import { register } from '../services/auth'
4
+ import { Button } from '@/components/ui/button'
5
+ import {
6
+ User,
7
+ Mail,
8
+ Lock,
9
+ ArrowRight,
10
+ Globe,
11
+ Sparkles,
12
+ CheckCircle2,
13
+ Loader2,
14
+ ChevronRight
15
+ } from 'lucide-react'
16
+
17
+ const Register = ({ onRegisterSuccess, onNavigateToLogin, onNavigateToLanding }) => {
18
+ const [formData, setFormData] = useState({
19
+ username: '',
20
+ email: '',
21
+ password: '',
22
+ first_name: '',
23
+ last_name: ''
24
+ })
25
+ const [error, setError] = useState('')
26
+ const [loading, setLoading] = useState(false)
27
+
28
+ const handleChange = (e) => {
29
+ setFormData({ ...formData, [e.target.name]: e.target.value })
30
+ }
31
+
32
+ const handleSubmit = async (e) => {
33
+ e.preventDefault()
34
+ setError('')
35
+ setLoading(true)
36
+ try {
37
+ const data = await register(formData)
38
+ localStorage.setItem('token', data.token)
39
+ localStorage.setItem('user', JSON.stringify(data.user))
40
+ onRegisterSuccess(data.user)
41
+ } catch (err) {
42
+ setError('Erreur lors de l\'inscription. Veuillez vérifier vos informations.')
43
+ } finally {
44
+ setLoading(false)
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[#050505]">
50
+ {/* Background Mesh Gradients */}
51
+ <div className="absolute top-[-10%] right-[-10%] w-[40%] h-[40%] bg-secondary/20 rounded-full blur-[120px] animate-pulse" />
52
+ <div className="absolute bottom-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/20 rounded-full blur-[120px] animate-pulse" style={{ animationDelay: '2s' }} />
53
+
54
+ <div className="relative z-10 w-full max-w-6xl px-4 flex flex-col lg:flex-row-reverse items-center gap-16">
55
+ {/* Left Side: Info & Features */}
56
+ <motion.div
57
+ initial={{ opacity: 0, x: 50 }}
58
+ animate={{ opacity: 1, x: 0 }}
59
+ transition={{ duration: 0.8, ease: "easeOut" }}
60
+ className="flex-1 text-center lg:text-left hidden lg:block"
61
+ >
62
+ <div className="inline-flex items-center space-x-3 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
63
+ <Sparkles className="h-4 w-4 text-primary" />
64
+ <span className="text-xs font-black uppercase tracking-widest text-primary">Rejoignez la Révolution des Données</span>
65
+ </div>
66
+ <h1 className="text-6xl xl:text-7xl font-black text-white leading-tight mb-6">
67
+ Façonnez le futur de <br />
68
+ <span className="text-gradient">l'Analyse Africaine</span>
69
+ </h1>
70
+
71
+ <div className="space-y-6 mt-10">
72
+ {[
73
+ "Accès illimité aux datasets continentaux",
74
+ "Outils d'analyse prédictive par IA",
75
+ "Alertes personnalisées en temps réel",
76
+ "Exportation de données haute résolution"
77
+ ].map((feature, i) => (
78
+ <motion.div
79
+ key={i}
80
+ initial={{ opacity: 0, x: 20 }}
81
+ animate={{ opacity: 1, x: 0 }}
82
+ transition={{ delay: 0.4 + (i * 0.1) }}
83
+ className="flex items-center space-x-4"
84
+ >
85
+ <div className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center">
86
+ <CheckCircle2 className="h-4 w-4 text-primary" />
87
+ </div>
88
+ <span className="text-lg text-white/70 font-medium">{feature}</span>
89
+ </motion.div>
90
+ ))}
91
+ </div>
92
+ </motion.div>
93
+
94
+ {/* Right Side: Register Card */}
95
+ <motion.div
96
+ initial={{ opacity: 0, scale: 0.9 }}
97
+ animate={{ opacity: 1, scale: 1 }}
98
+ transition={{ duration: 0.6, delay: 0.2 }}
99
+ className="w-full max-w-[540px]"
100
+ >
101
+ <div className="glass rounded-[3rem] p-10 md:p-12 border border-white/20 shadow-2xl relative overflow-hidden">
102
+ <div className="relative z-10">
103
+ <div className="mb-10 text-center lg:text-left">
104
+ <h2 className="text-3xl font-black text-white mb-2">Créer un compte</h2>
105
+ <p className="text-white/50 font-medium">Commencez votre voyage avec AfriDataHub</p>
106
+ </div>
107
+
108
+ <form onSubmit={handleSubmit} className="space-y-5">
109
+ <div className="grid grid-cols-2 gap-4">
110
+ <div className="relative group">
111
+ <input
112
+ type="text"
113
+ name="first_name"
114
+ required
115
+ placeholder="Prénom"
116
+ value={formData.first_name}
117
+ onChange={handleChange}
118
+ className="w-full px-5 py-4 bg-white/5 border border-white/10 rounded-2xl text-white placeholder:text-white/20 focus:ring-4 focus:ring-primary/10 focus:border-primary/30 outline-none transition-all font-medium"
119
+ />
120
+ </div>
121
+ <div className="relative group">
122
+ <input
123
+ type="text"
124
+ name="last_name"
125
+ required
126
+ placeholder="Nom"
127
+ value={formData.last_name}
128
+ onChange={handleChange}
129
+ className="w-full px-5 py-4 bg-white/5 border border-white/10 rounded-2xl text-white placeholder:text-white/20 focus:ring-4 focus:ring-primary/10 focus:border-primary/30 outline-none transition-all font-medium"
130
+ />
131
+ </div>
132
+ </div>
133
+
134
+ <div className="relative group">
135
+ <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
136
+ <User className="h-5 w-5 text-white/30 group-focus-within:text-primary transition-colors" />
137
+ </div>
138
+ <input
139
+ type="text"
140
+ name="username"
141
+ required
142
+ placeholder="Nom d'utilisateur"
143
+ value={formData.username}
144
+ onChange={handleChange}
145
+ className="w-full pl-12 pr-4 py-4 bg-white/5 border border-white/10 rounded-2xl text-white placeholder:text-white/20 focus:ring-4 focus:ring-primary/10 focus:border-primary/30 outline-none transition-all font-medium"
146
+ />
147
+ </div>
148
+
149
+ <div className="relative group">
150
+ <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
151
+ <Mail className="h-5 w-5 text-white/30 group-focus-within:text-secondary transition-colors" />
152
+ </div>
153
+ <input
154
+ type="email"
155
+ name="email"
156
+ required
157
+ placeholder="Adresse Email"
158
+ value={formData.email}
159
+ onChange={handleChange}
160
+ className="w-full pl-12 pr-4 py-4 bg-white/5 border border-white/10 rounded-2xl text-white placeholder:text-white/20 focus:ring-4 focus:ring-secondary/10 focus:border-secondary/30 outline-none transition-all font-medium"
161
+ />
162
+ </div>
163
+
164
+ <div className="relative group">
165
+ <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
166
+ <Lock className="h-5 w-5 text-white/30 group-focus-within:text-primary transition-colors" />
167
+ </div>
168
+ <input
169
+ type="password"
170
+ name="password"
171
+ required
172
+ placeholder="Mot de passe"
173
+ value={formData.password}
174
+ onChange={handleChange}
175
+ className="w-full pl-12 pr-4 py-4 bg-white/5 border border-white/10 rounded-2xl text-white placeholder:text-white/20 focus:ring-4 focus:ring-primary/10 focus:border-primary/30 outline-none transition-all font-medium"
176
+ />
177
+ </div>
178
+
179
+ <AnimatePresence mode="wait">
180
+ {error && (
181
+ <motion.div
182
+ initial={{ opacity: 0, height: 0 }}
183
+ animate={{ opacity: 1, height: 'auto' }}
184
+ exit={{ opacity: 0, height: 0 }}
185
+ className="p-4 bg-red-500/10 border border-red-500/20 rounded-2xl text-red-400 text-sm font-bold text-center"
186
+ >
187
+ {error}
188
+ </motion.div>
189
+ )}
190
+ </AnimatePresence>
191
+
192
+ <div className="pt-4">
193
+ <Button
194
+ type="submit"
195
+ disabled={loading}
196
+ className="w-full py-7 rounded-2xl bg-gradient-to-r from-secondary to-primary text-white font-black text-lg shadow-xl shadow-secondary/20 hover:shadow-secondary/40 transition-all hover:scale-[1.02] active:scale-[0.98]"
197
+ >
198
+ {loading ? (
199
+ <Loader2 className="h-6 w-6 animate-spin" />
200
+ ) : (
201
+ <span className="flex items-center">
202
+ Créer mon compte <ChevronRight className="ml-2 h-5 w-5" />
203
+ </span>
204
+ )}
205
+ </Button>
206
+ </div>
207
+ </form>
208
+
209
+ <div className="mt-10 text-center space-y-4">
210
+ <p className="text-white/40 font-medium">
211
+ Déjà membre ?{' '}
212
+ <button
213
+ onClick={onNavigateToLogin}
214
+ className="text-white font-black hover:text-primary transition-colors underline underline-offset-4"
215
+ >
216
+ Se connecter
217
+ </button>
218
+ </p>
219
+ <button
220
+ onClick={onNavigateToLanding}
221
+ className="text-white/20 hover:text-white/40 text-sm font-bold transition-colors"
222
+ >
223
+ ← Retour à l'accueil
224
+ </button>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </motion.div>
229
+ </div>
230
+ </div>
231
+ )
232
+ }
233
+
234
+ export default Register
235
+
afridatahub-frontend/src/components/ui/accordion.jsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
3
+ import { ChevronDownIcon } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Accordion({
8
+ ...props
9
+ }) {
10
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
11
+ }
12
+
13
+ function AccordionItem({
14
+ className,
15
+ ...props
16
+ }) {
17
+ return (
18
+ <AccordionPrimitive.Item
19
+ data-slot="accordion-item"
20
+ className={cn("border-b last:border-b-0", className)}
21
+ {...props} />
22
+ );
23
+ }
24
+
25
+ function AccordionTrigger({
26
+ className,
27
+ children,
28
+ ...props
29
+ }) {
30
+ return (
31
+ <AccordionPrimitive.Header className="flex">
32
+ <AccordionPrimitive.Trigger
33
+ data-slot="accordion-trigger"
34
+ className={cn(
35
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
36
+ className
37
+ )}
38
+ {...props}>
39
+ {children}
40
+ <ChevronDownIcon
41
+ className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
42
+ </AccordionPrimitive.Trigger>
43
+ </AccordionPrimitive.Header>
44
+ );
45
+ }
46
+
47
+ function AccordionContent({
48
+ className,
49
+ children,
50
+ ...props
51
+ }) {
52
+ return (
53
+ <AccordionPrimitive.Content
54
+ data-slot="accordion-content"
55
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
56
+ {...props}>
57
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
58
+ </AccordionPrimitive.Content>
59
+ );
60
+ }
61
+
62
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
afridatahub-frontend/src/components/ui/alert-dialog.jsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { buttonVariants } from "@/components/ui/button"
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }) {
18
+ return (<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />);
19
+ }
20
+
21
+ function AlertDialogPortal({
22
+ ...props
23
+ }) {
24
+ return (<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />);
25
+ }
26
+
27
+ function AlertDialogOverlay({
28
+ className,
29
+ ...props
30
+ }) {
31
+ return (
32
+ <AlertDialogPrimitive.Overlay
33
+ data-slot="alert-dialog-overlay"
34
+ className={cn(
35
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
36
+ className
37
+ )}
38
+ {...props} />
39
+ );
40
+ }
41
+
42
+ function AlertDialogContent({
43
+ className,
44
+ ...props
45
+ }) {
46
+ return (
47
+ <AlertDialogPortal>
48
+ <AlertDialogOverlay />
49
+ <AlertDialogPrimitive.Content
50
+ data-slot="alert-dialog-content"
51
+ className={cn(
52
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
53
+ className
54
+ )}
55
+ {...props} />
56
+ </AlertDialogPortal>
57
+ );
58
+ }
59
+
60
+ function AlertDialogHeader({
61
+ className,
62
+ ...props
63
+ }) {
64
+ return (
65
+ <div
66
+ data-slot="alert-dialog-header"
67
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
68
+ {...props} />
69
+ );
70
+ }
71
+
72
+ function AlertDialogFooter({
73
+ className,
74
+ ...props
75
+ }) {
76
+ return (
77
+ <div
78
+ data-slot="alert-dialog-footer"
79
+ className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
80
+ {...props} />
81
+ );
82
+ }
83
+
84
+ function AlertDialogTitle({
85
+ className,
86
+ ...props
87
+ }) {
88
+ return (
89
+ <AlertDialogPrimitive.Title
90
+ data-slot="alert-dialog-title"
91
+ className={cn("text-lg font-semibold", className)}
92
+ {...props} />
93
+ );
94
+ }
95
+
96
+ function AlertDialogDescription({
97
+ className,
98
+ ...props
99
+ }) {
100
+ return (
101
+ <AlertDialogPrimitive.Description
102
+ data-slot="alert-dialog-description"
103
+ className={cn("text-muted-foreground text-sm", className)}
104
+ {...props} />
105
+ );
106
+ }
107
+
108
+ function AlertDialogAction({
109
+ className,
110
+ ...props
111
+ }) {
112
+ return (<AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />);
113
+ }
114
+
115
+ function AlertDialogCancel({
116
+ className,
117
+ ...props
118
+ }) {
119
+ return (
120
+ <AlertDialogPrimitive.Cancel
121
+ className={cn(buttonVariants({ variant: "outline" }), className)}
122
+ {...props} />
123
+ );
124
+ }
125
+
126
+ export {
127
+ AlertDialog,
128
+ AlertDialogPortal,
129
+ AlertDialogOverlay,
130
+ AlertDialogTrigger,
131
+ AlertDialogContent,
132
+ AlertDialogHeader,
133
+ AlertDialogFooter,
134
+ AlertDialogTitle,
135
+ AlertDialogDescription,
136
+ AlertDialogAction,
137
+ AlertDialogCancel,
138
+ }
afridatahub-frontend/src/components/ui/alert.jsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props} />
33
+ );
34
+ }
35
+
36
+ function AlertTitle({
37
+ className,
38
+ ...props
39
+ }) {
40
+ return (
41
+ <div
42
+ data-slot="alert-title"
43
+ className={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
44
+ {...props} />
45
+ );
46
+ }
47
+
48
+ function AlertDescription({
49
+ className,
50
+ ...props
51
+ }) {
52
+ return (
53
+ <div
54
+ data-slot="alert-description"
55
+ className={cn(
56
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
57
+ className
58
+ )}
59
+ {...props} />
60
+ );
61
+ }
62
+
63
+ export { Alert, AlertTitle, AlertDescription }
afridatahub-frontend/src/components/ui/aspect-ratio.jsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2
+
3
+ function AspectRatio({
4
+ ...props
5
+ }) {
6
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
7
+ }
8
+
9
+ export { AspectRatio }
afridatahub-frontend/src/components/ui/avatar.jsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
16
+ {...props} />
17
+ );
18
+ }
19
+
20
+ function AvatarImage({
21
+ className,
22
+ ...props
23
+ }) {
24
+ return (
25
+ <AvatarPrimitive.Image
26
+ data-slot="avatar-image"
27
+ className={cn("aspect-square size-full", className)}
28
+ {...props} />
29
+ );
30
+ }
31
+
32
+ function AvatarFallback({
33
+ className,
34
+ ...props
35
+ }) {
36
+ return (
37
+ <AvatarPrimitive.Fallback
38
+ data-slot="avatar-fallback"
39
+ className={cn(
40
+ "bg-muted flex size-full items-center justify-center rounded-full",
41
+ className
42
+ )}
43
+ {...props} />
44
+ );
45
+ }
46
+
47
+ export { Avatar, AvatarImage, AvatarFallback }
afridatahub-frontend/src/components/ui/badge.jsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }) {
34
+ const Comp = asChild ? Slot : "span"
35
+
36
+ return (
37
+ <Comp
38
+ data-slot="badge"
39
+ className={cn(badgeVariants({ variant }), className)}
40
+ {...props} />
41
+ );
42
+ }
43
+
44
+ export { Badge, badgeVariants }
afridatahub-frontend/src/components/ui/breadcrumb.jsx ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Breadcrumb({
8
+ ...props
9
+ }) {
10
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
11
+ }
12
+
13
+ function BreadcrumbList({
14
+ className,
15
+ ...props
16
+ }) {
17
+ return (
18
+ <ol
19
+ data-slot="breadcrumb-list"
20
+ className={cn(
21
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
22
+ className
23
+ )}
24
+ {...props} />
25
+ );
26
+ }
27
+
28
+ function BreadcrumbItem({
29
+ className,
30
+ ...props
31
+ }) {
32
+ return (
33
+ <li
34
+ data-slot="breadcrumb-item"
35
+ className={cn("inline-flex items-center gap-1.5", className)}
36
+ {...props} />
37
+ );
38
+ }
39
+
40
+ function BreadcrumbLink({
41
+ asChild,
42
+ className,
43
+ ...props
44
+ }) {
45
+ const Comp = asChild ? Slot : "a"
46
+
47
+ return (
48
+ <Comp
49
+ data-slot="breadcrumb-link"
50
+ className={cn("hover:text-foreground transition-colors", className)}
51
+ {...props} />
52
+ );
53
+ }
54
+
55
+ function BreadcrumbPage({
56
+ className,
57
+ ...props
58
+ }) {
59
+ return (
60
+ <span
61
+ data-slot="breadcrumb-page"
62
+ role="link"
63
+ aria-disabled="true"
64
+ aria-current="page"
65
+ className={cn("text-foreground font-normal", className)}
66
+ {...props} />
67
+ );
68
+ }
69
+
70
+ function BreadcrumbSeparator({
71
+ children,
72
+ className,
73
+ ...props
74
+ }) {
75
+ return (
76
+ <li
77
+ data-slot="breadcrumb-separator"
78
+ role="presentation"
79
+ aria-hidden="true"
80
+ className={cn("[&>svg]:size-3.5", className)}
81
+ {...props}>
82
+ {children ?? <ChevronRight />}
83
+ </li>
84
+ );
85
+ }
86
+
87
+ function BreadcrumbEllipsis({
88
+ className,
89
+ ...props
90
+ }) {
91
+ return (
92
+ <span
93
+ data-slot="breadcrumb-ellipsis"
94
+ role="presentation"
95
+ aria-hidden="true"
96
+ className={cn("flex size-9 items-center justify-center", className)}
97
+ {...props}>
98
+ <MoreHorizontal className="size-4" />
99
+ <span className="sr-only">More</span>
100
+ </span>
101
+ );
102
+ }
103
+
104
+ export {
105
+ Breadcrumb,
106
+ BreadcrumbList,
107
+ BreadcrumbItem,
108
+ BreadcrumbLink,
109
+ BreadcrumbPage,
110
+ BreadcrumbSeparator,
111
+ BreadcrumbEllipsis,
112
+ }
afridatahub-frontend/src/components/ui/button.jsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
+ ghost:
21
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ }
36
+ )
37
+
38
+ function Button({
39
+ className,
40
+ variant,
41
+ size,
42
+ asChild = false,
43
+ ...props
44
+ }) {
45
+ const Comp = asChild ? Slot : "button"
46
+
47
+ return (
48
+ <Comp
49
+ data-slot="button"
50
+ className={cn(buttonVariants({ variant, size, className }))}
51
+ {...props} />
52
+ );
53
+ }
54
+
55
+ export { Button, buttonVariants }
afridatahub-frontend/src/components/ui/calendar.jsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { ChevronLeft, ChevronRight } from "lucide-react"
3
+ import { DayPicker } from "react-day-picker"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { buttonVariants } from "@/components/ui/button"
7
+
8
+ function Calendar({
9
+ className,
10
+ classNames,
11
+ showOutsideDays = true,
12
+ ...props
13
+ }) {
14
+ return (
15
+ <DayPicker
16
+ showOutsideDays={showOutsideDays}
17
+ className={cn("p-3", className)}
18
+ classNames={{
19
+ months: "flex flex-col sm:flex-row gap-2",
20
+ month: "flex flex-col gap-4",
21
+ caption: "flex justify-center pt-1 relative items-center w-full",
22
+ caption_label: "text-sm font-medium",
23
+ nav: "flex items-center gap-1",
24
+ nav_button: cn(
25
+ buttonVariants({ variant: "outline" }),
26
+ "size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
27
+ ),
28
+ nav_button_previous: "absolute left-1",
29
+ nav_button_next: "absolute right-1",
30
+ table: "w-full border-collapse space-x-1",
31
+ head_row: "flex",
32
+ head_cell:
33
+ "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
34
+ row: "flex w-full mt-2",
35
+ cell: cn(
36
+ "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
37
+ props.mode === "range"
38
+ ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
39
+ : "[&:has([aria-selected])]:rounded-md"
40
+ ),
41
+ day: cn(
42
+ buttonVariants({ variant: "ghost" }),
43
+ "size-8 p-0 font-normal aria-selected:opacity-100"
44
+ ),
45
+ day_range_start:
46
+ "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
47
+ day_range_end:
48
+ "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
49
+ day_selected:
50
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
51
+ day_today: "bg-accent text-accent-foreground",
52
+ day_outside:
53
+ "day-outside text-muted-foreground aria-selected:text-muted-foreground",
54
+ day_disabled: "text-muted-foreground opacity-50",
55
+ day_range_middle:
56
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
57
+ day_hidden: "invisible",
58
+ ...classNames,
59
+ }}
60
+ components={{
61
+ IconLeft: ({ className, ...props }) => (
62
+ <ChevronLeft className={cn("size-4", className)} {...props} />
63
+ ),
64
+ IconRight: ({ className, ...props }) => (
65
+ <ChevronRight className={cn("size-4", className)} {...props} />
66
+ ),
67
+ }}
68
+ {...props} />
69
+ );
70
+ }
71
+
72
+ export { Calendar }
afridatahub-frontend/src/components/ui/card.jsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({
6
+ className,
7
+ ...props
8
+ }) {
9
+ return (
10
+ <div
11
+ data-slot="card"
12
+ className={cn(
13
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
14
+ className
15
+ )}
16
+ {...props} />
17
+ );
18
+ }
19
+
20
+ function CardHeader({
21
+ className,
22
+ ...props
23
+ }) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
29
+ className
30
+ )}
31
+ {...props} />
32
+ );
33
+ }
34
+
35
+ function CardTitle({
36
+ className,
37
+ ...props
38
+ }) {
39
+ return (
40
+ <div
41
+ data-slot="card-title"
42
+ className={cn("leading-none font-semibold", className)}
43
+ {...props} />
44
+ );
45
+ }
46
+
47
+ function CardDescription({
48
+ className,
49
+ ...props
50
+ }) {
51
+ return (
52
+ <div
53
+ data-slot="card-description"
54
+ className={cn("text-muted-foreground text-sm", className)}
55
+ {...props} />
56
+ );
57
+ }
58
+
59
+ function CardAction({
60
+ className,
61
+ ...props
62
+ }) {
63
+ return (
64
+ <div
65
+ data-slot="card-action"
66
+ className={cn(
67
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
68
+ className
69
+ )}
70
+ {...props} />
71
+ );
72
+ }
73
+
74
+ function CardContent({
75
+ className,
76
+ ...props
77
+ }) {
78
+ return (<div data-slot="card-content" className={cn("px-6", className)} {...props} />);
79
+ }
80
+
81
+ function CardFooter({
82
+ className,
83
+ ...props
84
+ }) {
85
+ return (
86
+ <div
87
+ data-slot="card-footer"
88
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
89
+ {...props} />
90
+ );
91
+ }
92
+
93
+ export {
94
+ Card,
95
+ CardHeader,
96
+ CardFooter,
97
+ CardTitle,
98
+ CardAction,
99
+ CardDescription,
100
+ CardContent,
101
+ }
afridatahub-frontend/src/components/ui/carousel.jsx ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import * as React from "react"
3
+ import useEmblaCarousel from "embla-carousel-react";
4
+ import { ArrowLeft, ArrowRight } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Button } from "@/components/ui/button"
8
+
9
+ const CarouselContext = React.createContext(null)
10
+
11
+ function useCarousel() {
12
+ const context = React.useContext(CarouselContext)
13
+
14
+ if (!context) {
15
+ throw new Error("useCarousel must be used within a <Carousel />")
16
+ }
17
+
18
+ return context
19
+ }
20
+
21
+ function Carousel({
22
+ orientation = "horizontal",
23
+ opts,
24
+ setApi,
25
+ plugins,
26
+ className,
27
+ children,
28
+ ...props
29
+ }) {
30
+ const [carouselRef, api] = useEmblaCarousel({
31
+ ...opts,
32
+ axis: orientation === "horizontal" ? "x" : "y",
33
+ }, plugins)
34
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
35
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
36
+
37
+ const onSelect = React.useCallback((api) => {
38
+ if (!api) return
39
+ setCanScrollPrev(api.canScrollPrev())
40
+ setCanScrollNext(api.canScrollNext())
41
+ }, [])
42
+
43
+ const scrollPrev = React.useCallback(() => {
44
+ api?.scrollPrev()
45
+ }, [api])
46
+
47
+ const scrollNext = React.useCallback(() => {
48
+ api?.scrollNext()
49
+ }, [api])
50
+
51
+ const handleKeyDown = React.useCallback((event) => {
52
+ if (event.key === "ArrowLeft") {
53
+ event.preventDefault()
54
+ scrollPrev()
55
+ } else if (event.key === "ArrowRight") {
56
+ event.preventDefault()
57
+ scrollNext()
58
+ }
59
+ }, [scrollPrev, scrollNext])
60
+
61
+ React.useEffect(() => {
62
+ if (!api || !setApi) return
63
+ setApi(api)
64
+ }, [api, setApi])
65
+
66
+ React.useEffect(() => {
67
+ if (!api) return
68
+ onSelect(api)
69
+ api.on("reInit", onSelect)
70
+ api.on("select", onSelect)
71
+
72
+ return () => {
73
+ api?.off("select", onSelect)
74
+ };
75
+ }, [api, onSelect])
76
+
77
+ return (
78
+ <CarouselContext.Provider
79
+ value={{
80
+ carouselRef,
81
+ api: api,
82
+ opts,
83
+ orientation:
84
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
85
+ scrollPrev,
86
+ scrollNext,
87
+ canScrollPrev,
88
+ canScrollNext,
89
+ }}>
90
+ <div
91
+ onKeyDownCapture={handleKeyDown}
92
+ className={cn("relative", className)}
93
+ role="region"
94
+ aria-roledescription="carousel"
95
+ data-slot="carousel"
96
+ {...props}>
97
+ {children}
98
+ </div>
99
+ </CarouselContext.Provider>
100
+ );
101
+ }
102
+
103
+ function CarouselContent({
104
+ className,
105
+ ...props
106
+ }) {
107
+ const { carouselRef, orientation } = useCarousel()
108
+
109
+ return (
110
+ <div
111
+ ref={carouselRef}
112
+ className="overflow-hidden"
113
+ data-slot="carousel-content">
114
+ <div
115
+ className={cn(
116
+ "flex",
117
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
118
+ className
119
+ )}
120
+ {...props} />
121
+ </div>
122
+ );
123
+ }
124
+
125
+ function CarouselItem({
126
+ className,
127
+ ...props
128
+ }) {
129
+ const { orientation } = useCarousel()
130
+
131
+ return (
132
+ <div
133
+ role="group"
134
+ aria-roledescription="slide"
135
+ data-slot="carousel-item"
136
+ className={cn(
137
+ "min-w-0 shrink-0 grow-0 basis-full",
138
+ orientation === "horizontal" ? "pl-4" : "pt-4",
139
+ className
140
+ )}
141
+ {...props} />
142
+ );
143
+ }
144
+
145
+ function CarouselPrevious({
146
+ className,
147
+ variant = "outline",
148
+ size = "icon",
149
+ ...props
150
+ }) {
151
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
152
+
153
+ return (
154
+ <Button
155
+ data-slot="carousel-previous"
156
+ variant={variant}
157
+ size={size}
158
+ className={cn("absolute size-8 rounded-full", orientation === "horizontal"
159
+ ? "top-1/2 -left-12 -translate-y-1/2"
160
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
161
+ disabled={!canScrollPrev}
162
+ onClick={scrollPrev}
163
+ {...props}>
164
+ <ArrowLeft />
165
+ <span className="sr-only">Previous slide</span>
166
+ </Button>
167
+ );
168
+ }
169
+
170
+ function CarouselNext({
171
+ className,
172
+ variant = "outline",
173
+ size = "icon",
174
+ ...props
175
+ }) {
176
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
177
+
178
+ return (
179
+ <Button
180
+ data-slot="carousel-next"
181
+ variant={variant}
182
+ size={size}
183
+ className={cn("absolute size-8 rounded-full", orientation === "horizontal"
184
+ ? "top-1/2 -right-12 -translate-y-1/2"
185
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
186
+ disabled={!canScrollNext}
187
+ onClick={scrollNext}
188
+ {...props}>
189
+ <ArrowRight />
190
+ <span className="sr-only">Next slide</span>
191
+ </Button>
192
+ );
193
+ }
194
+
195
+ export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
afridatahub-frontend/src/components/ui/chart.jsx ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as RechartsPrimitive from "recharts"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = {
8
+ light: "",
9
+ dark: ".dark"
10
+ }
11
+
12
+ const ChartContext = React.createContext(null)
13
+
14
+ function useChart() {
15
+ const context = React.useContext(ChartContext)
16
+
17
+ if (!context) {
18
+ throw new Error("useChart must be used within a <ChartContainer />")
19
+ }
20
+
21
+ return context
22
+ }
23
+
24
+ function ChartContainer({
25
+ id,
26
+ className,
27
+ children,
28
+ config,
29
+ ...props
30
+ }) {
31
+ const uniqueId = React.useId()
32
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
33
+
34
+ return (
35
+ <ChartContext.Provider value={{ config }}>
36
+ <div
37
+ data-slot="chart"
38
+ data-chart={chartId}
39
+ className={cn(
40
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
41
+ className
42
+ )}
43
+ {...props}>
44
+ <ChartStyle id={chartId} config={config} />
45
+ <RechartsPrimitive.ResponsiveContainer>
46
+ {children}
47
+ </RechartsPrimitive.ResponsiveContainer>
48
+ </div>
49
+ </ChartContext.Provider>
50
+ );
51
+ }
52
+
53
+ const ChartStyle = ({
54
+ id,
55
+ config
56
+ }) => {
57
+ const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
58
+
59
+ if (!colorConfig.length) {
60
+ return null
61
+ }
62
+
63
+ return (
64
+ <style
65
+ dangerouslySetInnerHTML={{
66
+ __html: Object.entries(THEMES)
67
+ .map(([theme, prefix]) => `
68
+ ${prefix} [data-chart=${id}] {
69
+ ${colorConfig
70
+ .map(([key, itemConfig]) => {
71
+ const color =
72
+ itemConfig.theme?.[theme] ||
73
+ itemConfig.color
74
+ return color ? ` --color-${key}: ${color};` : null
75
+ })
76
+ .join("\n")}
77
+ }
78
+ `)
79
+ .join("\n"),
80
+ }} />
81
+ );
82
+ }
83
+
84
+ const ChartTooltip = RechartsPrimitive.Tooltip
85
+
86
+ function ChartTooltipContent({
87
+ active,
88
+ payload,
89
+ className,
90
+ indicator = "dot",
91
+ hideLabel = false,
92
+ hideIndicator = false,
93
+ label,
94
+ labelFormatter,
95
+ labelClassName,
96
+ formatter,
97
+ color,
98
+ nameKey,
99
+ labelKey
100
+ }) {
101
+ const { config } = useChart()
102
+
103
+ const tooltipLabel = React.useMemo(() => {
104
+ if (hideLabel || !payload?.length) {
105
+ return null
106
+ }
107
+
108
+ const [item] = payload
109
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`
110
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
111
+ const value =
112
+ !labelKey && typeof label === "string"
113
+ ? config[label]?.label || label
114
+ : itemConfig?.label
115
+
116
+ if (labelFormatter) {
117
+ return (
118
+ <div className={cn("font-medium", labelClassName)}>
119
+ {labelFormatter(value, payload)}
120
+ </div>
121
+ );
122
+ }
123
+
124
+ if (!value) {
125
+ return null
126
+ }
127
+
128
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
129
+ }, [
130
+ label,
131
+ labelFormatter,
132
+ payload,
133
+ hideLabel,
134
+ labelClassName,
135
+ config,
136
+ labelKey,
137
+ ])
138
+
139
+ if (!active || !payload?.length) {
140
+ return null
141
+ }
142
+
143
+ const nestLabel = payload.length === 1 && indicator !== "dot"
144
+
145
+ return (
146
+ <div
147
+ className={cn(
148
+ "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
149
+ className
150
+ )}>
151
+ {!nestLabel ? tooltipLabel : null}
152
+ <div className="grid gap-1.5">
153
+ {payload.map((item, index) => {
154
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
155
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
156
+ const indicatorColor = color || item.payload.fill || item.color
157
+
158
+ return (
159
+ <div
160
+ key={item.dataKey}
161
+ className={cn(
162
+ "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
163
+ indicator === "dot" && "items-center"
164
+ )}>
165
+ {formatter && item?.value !== undefined && item.name ? (
166
+ formatter(item.value, item.name, item, index, item.payload)
167
+ ) : (
168
+ <>
169
+ {itemConfig?.icon ? (
170
+ <itemConfig.icon />
171
+ ) : (
172
+ !hideIndicator && (
173
+ <div
174
+ className={cn("shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", {
175
+ "h-2.5 w-2.5": indicator === "dot",
176
+ "w-1": indicator === "line",
177
+ "w-0 border-[1.5px] border-dashed bg-transparent":
178
+ indicator === "dashed",
179
+ "my-0.5": nestLabel && indicator === "dashed",
180
+ })}
181
+ style={
182
+ {
183
+ "--color-bg": indicatorColor,
184
+ "--color-border": indicatorColor
185
+ }
186
+ } />
187
+ )
188
+ )}
189
+ <div
190
+ className={cn(
191
+ "flex flex-1 justify-between leading-none",
192
+ nestLabel ? "items-end" : "items-center"
193
+ )}>
194
+ <div className="grid gap-1.5">
195
+ {nestLabel ? tooltipLabel : null}
196
+ <span className="text-muted-foreground">
197
+ {itemConfig?.label || item.name}
198
+ </span>
199
+ </div>
200
+ {item.value && (
201
+ <span className="text-foreground font-mono font-medium tabular-nums">
202
+ {item.value.toLocaleString()}
203
+ </span>
204
+ )}
205
+ </div>
206
+ </>
207
+ )}
208
+ </div>
209
+ );
210
+ })}
211
+ </div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ const ChartLegend = RechartsPrimitive.Legend
217
+
218
+ function ChartLegendContent({
219
+ className,
220
+ hideIcon = false,
221
+ payload,
222
+ verticalAlign = "bottom",
223
+ nameKey
224
+ }) {
225
+ const { config } = useChart()
226
+
227
+ if (!payload?.length) {
228
+ return null
229
+ }
230
+
231
+ return (
232
+ <div
233
+ className={cn(
234
+ "flex items-center justify-center gap-4",
235
+ verticalAlign === "top" ? "pb-3" : "pt-3",
236
+ className
237
+ )}>
238
+ {payload.map((item) => {
239
+ const key = `${nameKey || item.dataKey || "value"}`
240
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
241
+
242
+ return (
243
+ <div
244
+ key={item.value}
245
+ className={cn(
246
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
247
+ )}>
248
+ {itemConfig?.icon && !hideIcon ? (
249
+ <itemConfig.icon />
250
+ ) : (
251
+ <div
252
+ className="h-2 w-2 shrink-0 rounded-[2px]"
253
+ style={{
254
+ backgroundColor: item.color,
255
+ }} />
256
+ )}
257
+ {itemConfig?.label}
258
+ </div>
259
+ );
260
+ })}
261
+ </div>
262
+ );
263
+ }
264
+
265
+ // Helper to extract item config from a payload.
266
+ function getPayloadConfigFromPayload(
267
+ config,
268
+ payload,
269
+ key
270
+ ) {
271
+ if (typeof payload !== "object" || payload === null) {
272
+ return undefined
273
+ }
274
+
275
+ const payloadPayload =
276
+ "payload" in payload &&
277
+ typeof payload.payload === "object" &&
278
+ payload.payload !== null
279
+ ? payload.payload
280
+ : undefined
281
+
282
+ let configLabelKey = key
283
+
284
+ if (
285
+ key in payload &&
286
+ typeof payload[key] === "string"
287
+ ) {
288
+ configLabelKey = payload[key]
289
+ } else if (
290
+ payloadPayload &&
291
+ key in payloadPayload &&
292
+ typeof payloadPayload[key] === "string"
293
+ ) {
294
+ configLabelKey = payloadPayload[key]
295
+ }
296
+
297
+ return configLabelKey in config
298
+ ? config[configLabelKey]
299
+ : config[key];
300
+ }
301
+
302
+ export {
303
+ ChartContainer,
304
+ ChartTooltip,
305
+ ChartTooltipContent,
306
+ ChartLegend,
307
+ ChartLegendContent,
308
+ ChartStyle,
309
+ }
afridatahub-frontend/src/components/ui/checkbox.jsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5
+ import { CheckIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Checkbox({
10
+ className,
11
+ ...props
12
+ }) {
13
+ return (
14
+ <CheckboxPrimitive.Root
15
+ data-slot="checkbox"
16
+ className={cn(
17
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
18
+ className
19
+ )}
20
+ {...props}>
21
+ <CheckboxPrimitive.Indicator
22
+ data-slot="checkbox-indicator"
23
+ className="flex items-center justify-center text-current transition-none">
24
+ <CheckIcon className="size-3.5" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ );
28
+ }
29
+
30
+ export { Checkbox }
afridatahub-frontend/src/components/ui/collapsible.jsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2
+
3
+ function Collapsible({
4
+ ...props
5
+ }) {
6
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
7
+ }
8
+
9
+ function CollapsibleTrigger({
10
+ ...props
11
+ }) {
12
+ return (<CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />);
13
+ }
14
+
15
+ function CollapsibleContent({
16
+ ...props
17
+ }) {
18
+ return (<CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />);
19
+ }
20
+
21
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
afridatahub-frontend/src/components/ui/command.jsx ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Command as CommandPrimitive } from "cmdk"
5
+ import { SearchIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "@/components/ui/dialog"
15
+
16
+ function Command({
17
+ className,
18
+ ...props
19
+ }) {
20
+ return (
21
+ <CommandPrimitive
22
+ data-slot="command"
23
+ className={cn(
24
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
25
+ className
26
+ )}
27
+ {...props} />
28
+ );
29
+ }
30
+
31
+ function CommandDialog({
32
+ title = "Command Palette",
33
+ description = "Search for a command to run...",
34
+ children,
35
+ ...props
36
+ }) {
37
+ return (
38
+ <Dialog {...props}>
39
+ <DialogHeader className="sr-only">
40
+ <DialogTitle>{title}</DialogTitle>
41
+ <DialogDescription>{description}</DialogDescription>
42
+ </DialogHeader>
43
+ <DialogContent className="overflow-hidden p-0">
44
+ <Command
45
+ className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
46
+ {children}
47
+ </Command>
48
+ </DialogContent>
49
+ </Dialog>
50
+ );
51
+ }
52
+
53
+ function CommandInput({
54
+ className,
55
+ ...props
56
+ }) {
57
+ return (
58
+ <div
59
+ data-slot="command-input-wrapper"
60
+ className="flex h-9 items-center gap-2 border-b px-3">
61
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
62
+ <CommandPrimitive.Input
63
+ data-slot="command-input"
64
+ className={cn(
65
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
66
+ className
67
+ )}
68
+ {...props} />
69
+ </div>
70
+ );
71
+ }
72
+
73
+ function CommandList({
74
+ className,
75
+ ...props
76
+ }) {
77
+ return (
78
+ <CommandPrimitive.List
79
+ data-slot="command-list"
80
+ className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
81
+ {...props} />
82
+ );
83
+ }
84
+
85
+ function CommandEmpty({
86
+ ...props
87
+ }) {
88
+ return (<CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm" {...props} />);
89
+ }
90
+
91
+ function CommandGroup({
92
+ className,
93
+ ...props
94
+ }) {
95
+ return (
96
+ <CommandPrimitive.Group
97
+ data-slot="command-group"
98
+ className={cn(
99
+ "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
100
+ className
101
+ )}
102
+ {...props} />
103
+ );
104
+ }
105
+
106
+ function CommandSeparator({
107
+ className,
108
+ ...props
109
+ }) {
110
+ return (
111
+ <CommandPrimitive.Separator
112
+ data-slot="command-separator"
113
+ className={cn("bg-border -mx-1 h-px", className)}
114
+ {...props} />
115
+ );
116
+ }
117
+
118
+ function CommandItem({
119
+ className,
120
+ ...props
121
+ }) {
122
+ return (
123
+ <CommandPrimitive.Item
124
+ data-slot="command-item"
125
+ className={cn(
126
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
127
+ className
128
+ )}
129
+ {...props} />
130
+ );
131
+ }
132
+
133
+ function CommandShortcut({
134
+ className,
135
+ ...props
136
+ }) {
137
+ return (
138
+ <span
139
+ data-slot="command-shortcut"
140
+ className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
141
+ {...props} />
142
+ );
143
+ }
144
+
145
+ export {
146
+ Command,
147
+ CommandDialog,
148
+ CommandInput,
149
+ CommandList,
150
+ CommandEmpty,
151
+ CommandGroup,
152
+ CommandItem,
153
+ CommandShortcut,
154
+ CommandSeparator,
155
+ }
afridatahub-frontend/src/components/ui/context-menu.jsx ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function ContextMenu({
10
+ ...props
11
+ }) {
12
+ return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
13
+ }
14
+
15
+ function ContextMenuTrigger({
16
+ ...props
17
+ }) {
18
+ return (<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />);
19
+ }
20
+
21
+ function ContextMenuGroup({
22
+ ...props
23
+ }) {
24
+ return (<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />);
25
+ }
26
+
27
+ function ContextMenuPortal({
28
+ ...props
29
+ }) {
30
+ return (<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />);
31
+ }
32
+
33
+ function ContextMenuSub({
34
+ ...props
35
+ }) {
36
+ return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
37
+ }
38
+
39
+ function ContextMenuRadioGroup({
40
+ ...props
41
+ }) {
42
+ return (<ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />);
43
+ }
44
+
45
+ function ContextMenuSubTrigger({
46
+ className,
47
+ inset,
48
+ children,
49
+ ...props
50
+ }) {
51
+ return (
52
+ <ContextMenuPrimitive.SubTrigger
53
+ data-slot="context-menu-sub-trigger"
54
+ data-inset={inset}
55
+ className={cn(
56
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
57
+ className
58
+ )}
59
+ {...props}>
60
+ {children}
61
+ <ChevronRightIcon className="ml-auto" />
62
+ </ContextMenuPrimitive.SubTrigger>
63
+ );
64
+ }
65
+
66
+ function ContextMenuSubContent({
67
+ className,
68
+ ...props
69
+ }) {
70
+ return (
71
+ <ContextMenuPrimitive.SubContent
72
+ data-slot="context-menu-sub-content"
73
+ className={cn(
74
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
75
+ className
76
+ )}
77
+ {...props} />
78
+ );
79
+ }
80
+
81
+ function ContextMenuContent({
82
+ className,
83
+ ...props
84
+ }) {
85
+ return (
86
+ <ContextMenuPrimitive.Portal>
87
+ <ContextMenuPrimitive.Content
88
+ data-slot="context-menu-content"
89
+ className={cn(
90
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
91
+ className
92
+ )}
93
+ {...props} />
94
+ </ContextMenuPrimitive.Portal>
95
+ );
96
+ }
97
+
98
+ function ContextMenuItem({
99
+ className,
100
+ inset,
101
+ variant = "default",
102
+ ...props
103
+ }) {
104
+ return (
105
+ <ContextMenuPrimitive.Item
106
+ data-slot="context-menu-item"
107
+ data-inset={inset}
108
+ data-variant={variant}
109
+ className={cn(
110
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
111
+ className
112
+ )}
113
+ {...props} />
114
+ );
115
+ }
116
+
117
+ function ContextMenuCheckboxItem({
118
+ className,
119
+ children,
120
+ checked,
121
+ ...props
122
+ }) {
123
+ return (
124
+ <ContextMenuPrimitive.CheckboxItem
125
+ data-slot="context-menu-checkbox-item"
126
+ className={cn(
127
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
128
+ className
129
+ )}
130
+ checked={checked}
131
+ {...props}>
132
+ <span
133
+ className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
134
+ <ContextMenuPrimitive.ItemIndicator>
135
+ <CheckIcon className="size-4" />
136
+ </ContextMenuPrimitive.ItemIndicator>
137
+ </span>
138
+ {children}
139
+ </ContextMenuPrimitive.CheckboxItem>
140
+ );
141
+ }
142
+
143
+ function ContextMenuRadioItem({
144
+ className,
145
+ children,
146
+ ...props
147
+ }) {
148
+ return (
149
+ <ContextMenuPrimitive.RadioItem
150
+ data-slot="context-menu-radio-item"
151
+ className={cn(
152
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
153
+ className
154
+ )}
155
+ {...props}>
156
+ <span
157
+ className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
158
+ <ContextMenuPrimitive.ItemIndicator>
159
+ <CircleIcon className="size-2 fill-current" />
160
+ </ContextMenuPrimitive.ItemIndicator>
161
+ </span>
162
+ {children}
163
+ </ContextMenuPrimitive.RadioItem>
164
+ );
165
+ }
166
+
167
+ function ContextMenuLabel({
168
+ className,
169
+ inset,
170
+ ...props
171
+ }) {
172
+ return (
173
+ <ContextMenuPrimitive.Label
174
+ data-slot="context-menu-label"
175
+ data-inset={inset}
176
+ className={cn(
177
+ "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
178
+ className
179
+ )}
180
+ {...props} />
181
+ );
182
+ }
183
+
184
+ function ContextMenuSeparator({
185
+ className,
186
+ ...props
187
+ }) {
188
+ return (
189
+ <ContextMenuPrimitive.Separator
190
+ data-slot="context-menu-separator"
191
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
192
+ {...props} />
193
+ );
194
+ }
195
+
196
+ function ContextMenuShortcut({
197
+ className,
198
+ ...props
199
+ }) {
200
+ return (
201
+ <span
202
+ data-slot="context-menu-shortcut"
203
+ className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
204
+ {...props} />
205
+ );
206
+ }
207
+
208
+ export {
209
+ ContextMenu,
210
+ ContextMenuTrigger,
211
+ ContextMenuContent,
212
+ ContextMenuItem,
213
+ ContextMenuCheckboxItem,
214
+ ContextMenuRadioItem,
215
+ ContextMenuLabel,
216
+ ContextMenuSeparator,
217
+ ContextMenuShortcut,
218
+ ContextMenuGroup,
219
+ ContextMenuPortal,
220
+ ContextMenuSub,
221
+ ContextMenuSubContent,
222
+ ContextMenuSubTrigger,
223
+ ContextMenuRadioGroup,
224
+ }
afridatahub-frontend/src/components/ui/dialog.jsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
3
+ import { XIcon } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Dialog({
8
+ ...props
9
+ }) {
10
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
11
+ }
12
+
13
+ function DialogTrigger({
14
+ ...props
15
+ }) {
16
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
17
+ }
18
+
19
+ function DialogPortal({
20
+ ...props
21
+ }) {
22
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
23
+ }
24
+
25
+ function DialogClose({
26
+ ...props
27
+ }) {
28
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
29
+ }
30
+
31
+ function DialogOverlay({
32
+ className,
33
+ ...props
34
+ }) {
35
+ return (
36
+ <DialogPrimitive.Overlay
37
+ data-slot="dialog-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className
41
+ )}
42
+ {...props} />
43
+ );
44
+ }
45
+
46
+ function DialogContent({
47
+ className,
48
+ children,
49
+ ...props
50
+ }) {
51
+ return (
52
+ <DialogPortal data-slot="dialog-portal">
53
+ <DialogOverlay />
54
+ <DialogPrimitive.Content
55
+ data-slot="dialog-content"
56
+ className={cn(
57
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
58
+ className
59
+ )}
60
+ {...props}>
61
+ {children}
62
+ <DialogPrimitive.Close
63
+ className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
64
+ <XIcon />
65
+ <span className="sr-only">Close</span>
66
+ </DialogPrimitive.Close>
67
+ </DialogPrimitive.Content>
68
+ </DialogPortal>
69
+ );
70
+ }
71
+
72
+ function DialogHeader({
73
+ className,
74
+ ...props
75
+ }) {
76
+ return (
77
+ <div
78
+ data-slot="dialog-header"
79
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
80
+ {...props} />
81
+ );
82
+ }
83
+
84
+ function DialogFooter({
85
+ className,
86
+ ...props
87
+ }) {
88
+ return (
89
+ <div
90
+ data-slot="dialog-footer"
91
+ className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
92
+ {...props} />
93
+ );
94
+ }
95
+
96
+ function DialogTitle({
97
+ className,
98
+ ...props
99
+ }) {
100
+ return (
101
+ <DialogPrimitive.Title
102
+ data-slot="dialog-title"
103
+ className={cn("text-lg leading-none font-semibold", className)}
104
+ {...props} />
105
+ );
106
+ }
107
+
108
+ function DialogDescription({
109
+ className,
110
+ ...props
111
+ }) {
112
+ return (
113
+ <DialogPrimitive.Description
114
+ data-slot="dialog-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props} />
117
+ );
118
+ }
119
+
120
+ export {
121
+ Dialog,
122
+ DialogClose,
123
+ DialogContent,
124
+ DialogDescription,
125
+ DialogFooter,
126
+ DialogHeader,
127
+ DialogOverlay,
128
+ DialogPortal,
129
+ DialogTitle,
130
+ DialogTrigger,
131
+ }
afridatahub-frontend/src/components/ui/drawer.jsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Drawer as DrawerPrimitive } from "vaul"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Drawer({
7
+ ...props
8
+ }) {
9
+ return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
10
+ }
11
+
12
+ function DrawerTrigger({
13
+ ...props
14
+ }) {
15
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
16
+ }
17
+
18
+ function DrawerPortal({
19
+ ...props
20
+ }) {
21
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
22
+ }
23
+
24
+ function DrawerClose({
25
+ ...props
26
+ }) {
27
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
28
+ }
29
+
30
+ function DrawerOverlay({
31
+ className,
32
+ ...props
33
+ }) {
34
+ return (
35
+ <DrawerPrimitive.Overlay
36
+ data-slot="drawer-overlay"
37
+ className={cn(
38
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
39
+ className
40
+ )}
41
+ {...props} />
42
+ );
43
+ }
44
+
45
+ function DrawerContent({
46
+ className,
47
+ children,
48
+ ...props
49
+ }) {
50
+ return (
51
+ <DrawerPortal data-slot="drawer-portal">
52
+ <DrawerOverlay />
53
+ <DrawerPrimitive.Content
54
+ data-slot="drawer-content"
55
+ className={cn(
56
+ "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
57
+ "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
58
+ "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
59
+ "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
60
+ "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
61
+ className
62
+ )}
63
+ {...props}>
64
+ <div
65
+ className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
66
+ {children}
67
+ </DrawerPrimitive.Content>
68
+ </DrawerPortal>
69
+ );
70
+ }
71
+
72
+ function DrawerHeader({
73
+ className,
74
+ ...props
75
+ }) {
76
+ return (
77
+ <div
78
+ data-slot="drawer-header"
79
+ className={cn("flex flex-col gap-1.5 p-4", className)}
80
+ {...props} />
81
+ );
82
+ }
83
+
84
+ function DrawerFooter({
85
+ className,
86
+ ...props
87
+ }) {
88
+ return (
89
+ <div
90
+ data-slot="drawer-footer"
91
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
92
+ {...props} />
93
+ );
94
+ }
95
+
96
+ function DrawerTitle({
97
+ className,
98
+ ...props
99
+ }) {
100
+ return (
101
+ <DrawerPrimitive.Title
102
+ data-slot="drawer-title"
103
+ className={cn("text-foreground font-semibold", className)}
104
+ {...props} />
105
+ );
106
+ }
107
+
108
+ function DrawerDescription({
109
+ className,
110
+ ...props
111
+ }) {
112
+ return (
113
+ <DrawerPrimitive.Description
114
+ data-slot="drawer-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props} />
117
+ );
118
+ }
119
+
120
+ export {
121
+ Drawer,
122
+ DrawerPortal,
123
+ DrawerOverlay,
124
+ DrawerTrigger,
125
+ DrawerClose,
126
+ DrawerContent,
127
+ DrawerHeader,
128
+ DrawerFooter,
129
+ DrawerTitle,
130
+ DrawerDescription,
131
+ }
afridatahub-frontend/src/components/ui/dropdown-menu.jsx ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }) {
18
+ return (<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />);
19
+ }
20
+
21
+ function DropdownMenuTrigger({
22
+ ...props
23
+ }) {
24
+ return (<DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />);
25
+ }
26
+
27
+ function DropdownMenuContent({
28
+ className,
29
+ sideOffset = 4,
30
+ ...props
31
+ }) {
32
+ return (
33
+ <DropdownMenuPrimitive.Portal>
34
+ <DropdownMenuPrimitive.Content
35
+ data-slot="dropdown-menu-content"
36
+ sideOffset={sideOffset}
37
+ className={cn(
38
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
39
+ className
40
+ )}
41
+ {...props} />
42
+ </DropdownMenuPrimitive.Portal>
43
+ );
44
+ }
45
+
46
+ function DropdownMenuGroup({
47
+ ...props
48
+ }) {
49
+ return (<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />);
50
+ }
51
+
52
+ function DropdownMenuItem({
53
+ className,
54
+ inset,
55
+ variant = "default",
56
+ ...props
57
+ }) {
58
+ return (
59
+ <DropdownMenuPrimitive.Item
60
+ data-slot="dropdown-menu-item"
61
+ data-inset={inset}
62
+ data-variant={variant}
63
+ className={cn(
64
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
65
+ className
66
+ )}
67
+ {...props} />
68
+ );
69
+ }
70
+
71
+ function DropdownMenuCheckboxItem({
72
+ className,
73
+ children,
74
+ checked,
75
+ ...props
76
+ }) {
77
+ return (
78
+ <DropdownMenuPrimitive.CheckboxItem
79
+ data-slot="dropdown-menu-checkbox-item"
80
+ className={cn(
81
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
82
+ className
83
+ )}
84
+ checked={checked}
85
+ {...props}>
86
+ <span
87
+ className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
88
+ <DropdownMenuPrimitive.ItemIndicator>
89
+ <CheckIcon className="size-4" />
90
+ </DropdownMenuPrimitive.ItemIndicator>
91
+ </span>
92
+ {children}
93
+ </DropdownMenuPrimitive.CheckboxItem>
94
+ );
95
+ }
96
+
97
+ function DropdownMenuRadioGroup({
98
+ ...props
99
+ }) {
100
+ return (<DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />);
101
+ }
102
+
103
+ function DropdownMenuRadioItem({
104
+ className,
105
+ children,
106
+ ...props
107
+ }) {
108
+ return (
109
+ <DropdownMenuPrimitive.RadioItem
110
+ data-slot="dropdown-menu-radio-item"
111
+ className={cn(
112
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
113
+ className
114
+ )}
115
+ {...props}>
116
+ <span
117
+ className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
118
+ <DropdownMenuPrimitive.ItemIndicator>
119
+ <CircleIcon className="size-2 fill-current" />
120
+ </DropdownMenuPrimitive.ItemIndicator>
121
+ </span>
122
+ {children}
123
+ </DropdownMenuPrimitive.RadioItem>
124
+ );
125
+ }
126
+
127
+ function DropdownMenuLabel({
128
+ className,
129
+ inset,
130
+ ...props
131
+ }) {
132
+ return (
133
+ <DropdownMenuPrimitive.Label
134
+ data-slot="dropdown-menu-label"
135
+ data-inset={inset}
136
+ className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
137
+ {...props} />
138
+ );
139
+ }
140
+
141
+ function DropdownMenuSeparator({
142
+ className,
143
+ ...props
144
+ }) {
145
+ return (
146
+ <DropdownMenuPrimitive.Separator
147
+ data-slot="dropdown-menu-separator"
148
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
149
+ {...props} />
150
+ );
151
+ }
152
+
153
+ function DropdownMenuShortcut({
154
+ className,
155
+ ...props
156
+ }) {
157
+ return (
158
+ <span
159
+ data-slot="dropdown-menu-shortcut"
160
+ className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
161
+ {...props} />
162
+ );
163
+ }
164
+
165
+ function DropdownMenuSub({
166
+ ...props
167
+ }) {
168
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
169
+ }
170
+
171
+ function DropdownMenuSubTrigger({
172
+ className,
173
+ inset,
174
+ children,
175
+ ...props
176
+ }) {
177
+ return (
178
+ <DropdownMenuPrimitive.SubTrigger
179
+ data-slot="dropdown-menu-sub-trigger"
180
+ data-inset={inset}
181
+ className={cn(
182
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
183
+ className
184
+ )}
185
+ {...props}>
186
+ {children}
187
+ <ChevronRightIcon className="ml-auto size-4" />
188
+ </DropdownMenuPrimitive.SubTrigger>
189
+ );
190
+ }
191
+
192
+ function DropdownMenuSubContent({
193
+ className,
194
+ ...props
195
+ }) {
196
+ return (
197
+ <DropdownMenuPrimitive.SubContent
198
+ data-slot="dropdown-menu-sub-content"
199
+ className={cn(
200
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
201
+ className
202
+ )}
203
+ {...props} />
204
+ );
205
+ }
206
+
207
+ export {
208
+ DropdownMenu,
209
+ DropdownMenuPortal,
210
+ DropdownMenuTrigger,
211
+ DropdownMenuContent,
212
+ DropdownMenuGroup,
213
+ DropdownMenuLabel,
214
+ DropdownMenuItem,
215
+ DropdownMenuCheckboxItem,
216
+ DropdownMenuRadioGroup,
217
+ DropdownMenuRadioItem,
218
+ DropdownMenuSeparator,
219
+ DropdownMenuShortcut,
220
+ DropdownMenuSub,
221
+ DropdownMenuSubTrigger,
222
+ DropdownMenuSubContent,
223
+ }
afridatahub-frontend/src/components/ui/form.jsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { Controller, FormProvider, useFormContext, useFormState } from "react-hook-form";
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { Label } from "@/components/ui/label"
7
+
8
+ const Form = FormProvider
9
+
10
+ const FormFieldContext = React.createContext({})
11
+
12
+ const FormField = (
13
+ {
14
+ ...props
15
+ }
16
+ ) => {
17
+ return (
18
+ <FormFieldContext.Provider value={{ name: props.name }}>
19
+ <Controller {...props} />
20
+ </FormFieldContext.Provider>
21
+ );
22
+ }
23
+
24
+ const useFormField = () => {
25
+ const fieldContext = React.useContext(FormFieldContext)
26
+ const itemContext = React.useContext(FormItemContext)
27
+ const { getFieldState } = useFormContext()
28
+ const formState = useFormState({ name: fieldContext.name })
29
+ const fieldState = getFieldState(fieldContext.name, formState)
30
+
31
+ if (!fieldContext) {
32
+ throw new Error("useFormField should be used within <FormField>")
33
+ }
34
+
35
+ const { id } = itemContext
36
+
37
+ return {
38
+ id,
39
+ name: fieldContext.name,
40
+ formItemId: `${id}-form-item`,
41
+ formDescriptionId: `${id}-form-item-description`,
42
+ formMessageId: `${id}-form-item-message`,
43
+ ...fieldState,
44
+ }
45
+ }
46
+
47
+ const FormItemContext = React.createContext({})
48
+
49
+ function FormItem({
50
+ className,
51
+ ...props
52
+ }) {
53
+ const id = React.useId()
54
+
55
+ return (
56
+ <FormItemContext.Provider value={{ id }}>
57
+ <div data-slot="form-item" className={cn("grid gap-2", className)} {...props} />
58
+ </FormItemContext.Provider>
59
+ );
60
+ }
61
+
62
+ function FormLabel({
63
+ className,
64
+ ...props
65
+ }) {
66
+ const { error, formItemId } = useFormField()
67
+
68
+ return (
69
+ <Label
70
+ data-slot="form-label"
71
+ data-error={!!error}
72
+ className={cn("data-[error=true]:text-destructive", className)}
73
+ htmlFor={formItemId}
74
+ {...props} />
75
+ );
76
+ }
77
+
78
+ function FormControl({
79
+ ...props
80
+ }) {
81
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
82
+
83
+ return (
84
+ <Slot
85
+ data-slot="form-control"
86
+ id={formItemId}
87
+ aria-describedby={
88
+ !error
89
+ ? `${formDescriptionId}`
90
+ : `${formDescriptionId} ${formMessageId}`
91
+ }
92
+ aria-invalid={!!error}
93
+ {...props} />
94
+ );
95
+ }
96
+
97
+ function FormDescription({
98
+ className,
99
+ ...props
100
+ }) {
101
+ const { formDescriptionId } = useFormField()
102
+
103
+ return (
104
+ <p
105
+ data-slot="form-description"
106
+ id={formDescriptionId}
107
+ className={cn("text-muted-foreground text-sm", className)}
108
+ {...props} />
109
+ );
110
+ }
111
+
112
+ function FormMessage({
113
+ className,
114
+ ...props
115
+ }) {
116
+ const { error, formMessageId } = useFormField()
117
+ const body = error ? String(error?.message ?? "") : props.children
118
+
119
+ if (!body) {
120
+ return null
121
+ }
122
+
123
+ return (
124
+ <p
125
+ data-slot="form-message"
126
+ id={formMessageId}
127
+ className={cn("text-destructive text-sm", className)}
128
+ {...props}>
129
+ {body}
130
+ </p>
131
+ );
132
+ }
133
+
134
+ export {
135
+ useFormField,
136
+ Form,
137
+ FormItem,
138
+ FormLabel,
139
+ FormControl,
140
+ FormDescription,
141
+ FormMessage,
142
+ FormField,
143
+ }
afridatahub-frontend/src/components/ui/hover-card.jsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function HoverCard({
7
+ ...props
8
+ }) {
9
+ return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
10
+ }
11
+
12
+ function HoverCardTrigger({
13
+ ...props
14
+ }) {
15
+ return (<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />);
16
+ }
17
+
18
+ function HoverCardContent({
19
+ className,
20
+ align = "center",
21
+ sideOffset = 4,
22
+ ...props
23
+ }) {
24
+ return (
25
+ <HoverCardPrimitive.Portal data-slot="hover-card-portal">
26
+ <HoverCardPrimitive.Content
27
+ data-slot="hover-card-content"
28
+ align={align}
29
+ sideOffset={sideOffset}
30
+ className={cn(
31
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
32
+ className
33
+ )}
34
+ {...props} />
35
+ </HoverCardPrimitive.Portal>
36
+ );
37
+ }
38
+
39
+ export { HoverCard, HoverCardTrigger, HoverCardContent }