diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md index 2241000..ce89597 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@
- GHBanner - -

Built with AI Studio

- -

The fastest path from prompt to production with Gemini.

- - Start building -
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1BMdgG2F3XgqJLpiOrmttuhBBTUR9rQVq + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/index.html b/index.html new file mode 100644 index 0000000..33cbf37 --- /dev/null +++ b/index.html @@ -0,0 +1,55 @@ + + + + + + + + Gedimat Lunel - Simulateur Stratégique + + + + + + + +
+ + + diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..17d6a61 --- /dev/null +++ b/index.tsx @@ -0,0 +1,524 @@ + +import React, { useState, useEffect, useRef } from 'react'; +import { createRoot } from 'react-dom/client'; +import { AlertTriangle, CheckCircle, TrendingUp, TrendingDown, Truck, MessageCircle, Clock, Smartphone } from 'lucide-react'; + +/** + * GEDIMAT LUNEL NEGOCE - SIMULATEUR STRATÉGIQUE V2.0 (CINEMA MODE) + * Version Pure JS pour compatibilité maximale + */ + +// --- CONSTANTES & STYLES --- +const COLORS = { + bg: "#0f172a", // Slate 900 + card: "#1e293b", // Slate 800 + success: "#10b981", // Emerald 500 + danger: "#ef4444", // Red 500 + warning: "#f59e0b", // Amber 500 + primary: "#3b82f6", // Blue 500 + text: "#f8fafc", + textMuted: "#94a3b8" +}; + +const STYLES = { + container: { + fontFamily: '"Inter", "Segoe UI", sans-serif', + backgroundColor: COLORS.bg, + color: COLORS.text, + height: '100vh', + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + position: 'relative', + } as React.CSSProperties, + header: { + padding: '20px', + backgroundColor: 'rgba(15, 23, 42, 0.9)', + borderBottom: `1px solid ${COLORS.card}`, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + zIndex: 10, + height: '80px' + } as React.CSSProperties, + metricBox: { + padding: '8px 16px', + borderRadius: '8px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + minWidth: '120px', + border: '1px solid rgba(255,255,255,0.05)' + } as React.CSSProperties, + mainStage: { + flex: 1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + padding: '20px' + } as React.CSSProperties, + card: { + backgroundColor: COLORS.card, + borderRadius: '16px', + padding: '40px', + width: '650px', + maxWidth: '100%', + boxShadow: '0 20px 50px rgba(0,0,0,0.5)', + position: 'relative', + border: `1px solid ${COLORS.textMuted}20` + } as React.CSSProperties, + button: (variant: string, isActive = false): React.CSSProperties => ({ + padding: '16px 24px', + borderRadius: '12px', + fontWeight: 600, + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: '12px', + width: '100%', + transition: 'all 0.2s', + backgroundColor: isActive ? (variant === 'success' ? `${COLORS.success}30` : `${COLORS.danger}30`) : 'rgba(255,255,255,0.03)', + color: isActive ? (variant === 'success' ? COLORS.success : COLORS.danger) : COLORS.textMuted, + border: `1px solid ${isActive ? (variant === 'success' ? COLORS.success : COLORS.danger) : 'rgba(255,255,255,0.1)'}`, + transform: isActive ? 'scale(1.02)' : 'scale(1)' + }) +}; + +// --- COMPOSANTS UI --- + +const StatCounter = ({ label, value, type }: { label: string; value: number; type: string }) => ( +
+ {label} +
+ {type === 'cost' ? : } + + {value.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 })} + +
+
+); + +const VirtualCursor = ({ x, y, clicking }: { x: number; y: number; clicking: boolean }) => ( +
+ + + + {clicking && ( +
+ )} +
+); + +const FlashCard = ({ title, message, type }: { title: string; message: string; type: string }) => ( +
+ {type === 'bad' ? : } +

{title}

+

{message}

+
+); + +const PhoneMockup = ({ show, type }: { show: boolean; type: string }) => { + if (!show) return null; + return ( +
+ {/* Notch */} +
+ + {/* Header */} +
+
16:02
+
+ {type === 'whatsapp' &&
} + {type === 'whatsapp' ? 'Chantier DUPONT' : 'Messagerie'} +
+
+ + {/* Body */} +
+ + {type === 'whatsapp' ? ( + <> +
Aujourd'hui
+ + {/* Message Commercial */} +
+
Sophie (Logistique)
+ ✅ Commande #402 chargée.
Arrivée demain 10h00. +
16:00
+
+ + {/* Photo */} +
+
+ + [PHOTO PALETTES] +
+
16:00
+
+ + ) : ( + <> +
Hier 17:12
+
+ GEDIMAT: Votre livraison est prévue pour demain dans la journée. +
+ + )} +
+ + {/* Footer Input */} +
+
+ ); +}; + +function GedimatSimulator() { + const [metrics, setMetrics] = useState({ waste: 0, savings: 0 }); + const [cursor, setCursor] = useState({ x: -100, y: -100, clicking: false }); + const [activeModule, setActiveModule] = useState(0); // 0: Intro, 1: Transport, 2: 15:30, 3: WhatsApp, 4: Summary + const [flashcard, setFlashcard] = useState<{ title: string; msg: string; type: string } | null>(null); + const [simStep, setSimStep] = useState(0); + const [phoneType, setPhoneType] = useState(null); + + const btnBadRef = useRef(null); + const btnGoodRef = useRef(null); + const startBtnRef = useRef(null); + + // Initialiser cursor au centre + useEffect(() => { + setCursor({ x: window.innerWidth / 2, y: window.innerHeight / 2, clicking: false }); + }, []); + + const moveCursorTo = (ref: React.RefObject, callback?: () => void) => { + if (ref.current) { + const rect = ref.current.getBoundingClientRect(); + const targetX = rect.left + rect.width / 2; + const targetY = rect.top + rect.height / 2; + + setCursor(prev => ({ ...prev, x: targetX, y: targetY })); + + setTimeout(() => { + setCursor(prev => ({ ...prev, clicking: true })); // Click down + setTimeout(() => { + setCursor(prev => ({ ...prev, clicking: false })); // Click up + if (callback) callback(); + }, 300); + }, 800); // Temps de trajet + } + }; + + // --- SCÉNARIO AUTOMATIQUE --- + useEffect(() => { + let timeout: ReturnType; + + const runScenario = async () => { + // AUTO START + if (activeModule === 0) { + timeout = setTimeout(() => { + if (startBtnRef.current) { + moveCursorTo(startBtnRef, () => setActiveModule(1)); + } + }, 2000); + } + + // ---------------- MODULE 1: TRANSPORT ---------------- + if (activeModule === 1) { + if (simStep === 0) { + // Expliquer le problème + setFlashcard({ title: "LE PROBLÈME", msg: "Gisors et Méru commandent séparément au même fournisseur.", type: 'bad' }); + timeout = setTimeout(() => { + setFlashcard(null); + // Action "Mauvaise" + moveCursorTo(btnBadRef, () => { + setMetrics(prev => ({ ...prev, waste: prev.waste + 180 })); + setFlashcard({ title: "DOUBLE FACTURATION", msg: "2 camions payés pour rien. Gaspillage : 180€.", type: 'bad' }); + setTimeout(() => { setFlashcard(null); setSimStep(1); }, 3000); + }); + }, 3000); + } else if (simStep === 1) { + // Expliquer la solution + timeout = setTimeout(() => { + // Action "Bonne" + moveCursorTo(btnGoodRef, () => { + setMetrics(prev => ({ ...prev, savings: prev.savings + 90 })); + setFlashcard({ title: "SOLUTION XCEL", msg: "Consolidation J-1. Livraison unique à Méru + Navette interne.", type: 'good' }); + setTimeout(() => { setFlashcard(null); setActiveModule(2); setSimStep(0); }, 3500); + }); + }, 1000); + } + } + + // ---------------- MODULE 2: PROTOCOLE 15:30 ---------------- + if (activeModule === 2) { + if (simStep === 0) { + setFlashcard({ title: "LE PROBLÈME", msg: "Le camion n'est pas là à 15h30. On ne sait rien.", type: 'bad' }); + timeout = setTimeout(() => { + setFlashcard(null); + moveCursorTo(btnBadRef, () => { + setMetrics(prev => ({ ...prev, waste: prev.waste + 2000 })); + setFlashcard({ title: "CLIENT PERDU", msg: "Le client découvre le retard demain matin. Chantier arrêté.", type: 'bad' }); + setTimeout(() => { setFlashcard(null); setSimStep(1); }, 3000); + }); + }, 3000); + } else if (simStep === 1) { + timeout = setTimeout(() => { + moveCursorTo(btnGoodRef, () => { + setMetrics(prev => ({ ...prev, savings: prev.savings + 500 })); + setFlashcard({ title: "PROTOCOLE 15:30", msg: "Alerte immédiate. Taxi-colis activé. Client prévenu à 16h.", type: 'good' }); + setTimeout(() => { setFlashcard(null); setActiveModule(3); setSimStep(0); }, 3500); + }); + }, 1000); + } + } + + // ---------------- MODULE 3: WHATSAPP CONCIERGE ---------------- + if (activeModule === 3) { + if (simStep === 0) { + setFlashcard({ title: "LE PROBLÈME", msg: "SMS impersonnel envoyé à 17h. Aucune réponse.", type: 'bad' }); + timeout = setTimeout(() => { + setFlashcard(null); + moveCursorTo(btnBadRef, () => { + setPhoneType('sms'); // Show SMS phone + setFlashcard({ title: "COMMUNICATION FROIDE", msg: "Le client n'est pas rassuré. Il rappelle le commercial.", type: 'bad' }); + setTimeout(() => { setPhoneType(null); setFlashcard(null); setSimStep(1); }, 4000); + }); + }, 3000); + } else if (simStep === 1) { + timeout = setTimeout(() => { + moveCursorTo(btnGoodRef, () => { + setMetrics(prev => ({ ...prev, savings: prev.savings + 1200 })); + setPhoneType('whatsapp'); // Show WhatsApp phone + setFlashcard({ title: "EFFET 'CONCIERGE'", msg: "Groupe WhatsApp Chantier. Photo envoyée. Confiance totale.", type: 'good' }); + setTimeout(() => { setPhoneType(null); setFlashcard(null); setActiveModule(4); }, 5000); + }); + }, 1000); + } + } + }; + + runScenario(); + return () => clearTimeout(timeout); + }, [activeModule, simStep]); + + // --- RENDU DES MODULES --- + + return ( +
+ {/* HEADER P&L */} +
+
GEDIMAT XCEL
+
+ + +
+
+ + {/* MAIN STAGE */} +
+
+ {activeModule === 0 && ( +
+

Gedimat XCEL v3.55

+

Simulation d'Impact Stratégique & Financier

+ +
+ )} + + {activeModule === 1 && ( +
+
+

+ Consolidation Gisors-Méru +

+ MODULE 1/3 +
+ +
+

+ Gisors commande des tuiles à Beauvais. Méru commande du ciment à Beauvais.
+ Que fait-on ? +

+
+ +
+ + +
+
+ )} + + {activeModule === 2 && ( +
+
+

+ Protocole 15:30 +

+ MODULE 2/3 +
+ +
+
15:30
+

+ Pas de nouvelles de Médiafret pour la livraison de demain. +

+
+ +
+ + +
+
+ )} + + {activeModule === 3 && ( +
+
+

+ Interface Client +

+ MODULE 3/3 +
+ +
+

+ Le client s'inquiète pour sa livraison de demain.
Comment communiquer ? +

+
+ +
+ + +
+ + +
+ )} + + {activeModule === 4 && ( +
+

DÉMONSTRATION TERMINÉE

+
+
+
+ {metrics.waste.toLocaleString()} € +
+
Pertes Identifiées
+
+
+
+ {metrics.savings.toLocaleString()} € +
+
Gains Stratégiques
+
+
+ +
+ )} + + {/* Overlay Flashcard */} + {flashcard && } +
+
+ + {/* CURSOR LAYER */} + + + {/* FOOTER LABEL */} +
+ Démonstration Automatique — Algorithme v3.55 +
+
+ ); +} + +const root = createRoot(document.getElementById('root')!); +root.render(); diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..1caf70c --- /dev/null +++ b/metadata.json @@ -0,0 +1,5 @@ +{ + "description": "Generated by Gemini.", + "requestFramePermissions": [], + "name": "App" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ef28dd0 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "lucide-react": "0.263.1" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ee5fb8d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +});