Spaces:
Sleeping
Sleeping
Commit ·
46e985f
0
Parent(s):
Deploy Frontend to HF
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +13 -0
- .gitignore +4 -0
- Dockerfile +35 -0
- README.md +46 -0
- afridatahub-frontend/components.json +21 -0
- afridatahub-frontend/eslint.config.js +33 -0
- afridatahub-frontend/index.html +13 -0
- afridatahub-frontend/jsconfig.json +8 -0
- afridatahub-frontend/package-lock.json +0 -0
- afridatahub-frontend/package.json +82 -0
- afridatahub-frontend/pnpm-lock.yaml +0 -0
- afridatahub-frontend/src/App.css +205 -0
- afridatahub-frontend/src/App.jsx +207 -0
- afridatahub-frontend/src/assets/africa.json +1 -0
- afridatahub-frontend/src/assets/react.svg +1 -0
- afridatahub-frontend/src/components/APIDocumentation.jsx +317 -0
- afridatahub-frontend/src/components/AfricaMap.jsx +190 -0
- afridatahub-frontend/src/components/Alerts.jsx +384 -0
- afridatahub-frontend/src/components/Analytics.jsx +311 -0
- afridatahub-frontend/src/components/Charts.jsx +242 -0
- afridatahub-frontend/src/components/Dashboard.jsx +297 -0
- afridatahub-frontend/src/components/DatasetAnalysis.jsx +275 -0
- afridatahub-frontend/src/components/DatasetCard.jsx +129 -0
- afridatahub-frontend/src/components/Datasets.jsx +443 -0
- afridatahub-frontend/src/components/LandingPage.jsx +275 -0
- afridatahub-frontend/src/components/Login.jsx +194 -0
- afridatahub-frontend/src/components/Navigation.jsx +166 -0
- afridatahub-frontend/src/components/Profile.jsx +364 -0
- afridatahub-frontend/src/components/Register.jsx +235 -0
- afridatahub-frontend/src/components/ui/accordion.jsx +62 -0
- afridatahub-frontend/src/components/ui/alert-dialog.jsx +138 -0
- afridatahub-frontend/src/components/ui/alert.jsx +63 -0
- afridatahub-frontend/src/components/ui/aspect-ratio.jsx +9 -0
- afridatahub-frontend/src/components/ui/avatar.jsx +47 -0
- afridatahub-frontend/src/components/ui/badge.jsx +44 -0
- afridatahub-frontend/src/components/ui/breadcrumb.jsx +112 -0
- afridatahub-frontend/src/components/ui/button.jsx +55 -0
- afridatahub-frontend/src/components/ui/calendar.jsx +72 -0
- afridatahub-frontend/src/components/ui/card.jsx +101 -0
- afridatahub-frontend/src/components/ui/carousel.jsx +195 -0
- afridatahub-frontend/src/components/ui/chart.jsx +309 -0
- afridatahub-frontend/src/components/ui/checkbox.jsx +30 -0
- afridatahub-frontend/src/components/ui/collapsible.jsx +21 -0
- afridatahub-frontend/src/components/ui/command.jsx +155 -0
- afridatahub-frontend/src/components/ui/context-menu.jsx +224 -0
- afridatahub-frontend/src/components/ui/dialog.jsx +131 -0
- afridatahub-frontend/src/components/ui/drawer.jsx +131 -0
- afridatahub-frontend/src/components/ui/dropdown-menu.jsx +223 -0
- afridatahub-frontend/src/components/ui/form.jsx +143 -0
- 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 }
|