WebApp#5 freelance Infirmier onboarding  > Prompt et discussion Gemini 

* WebApp complémentaire à la ​ WebApp#2

Ci-dessous le dialogue avec AI STUDIO qui semble utiliser le modèle Gemini 3 pour la génération de code 


Ci-dessous dans la section 1) j'ai juste demander une phrase générale, puis en suite  en 2° , 3) ... j'ai donné les planches de la cinématiques des vues que je souhaites que AI STUDIO puisse code les pages en REACT JS

1- Create a REACT JS Tailwind CSS WebApp for freelance for "infirmier" for the french market


LThis is a complete boilerplate for a React JS + Tailwind CSS WebApp designed for the French freelance nursing market (often called "Remplacements IDEL" or "Vacations").

I will use Vite for the build tool as it is faster than Create React App, and Lucide React for icons.

Prerequisites

You need Node.js installed.

Step 1: Project Setup

Open your terminal and run these commands:

# 1. Create the project

npm create vite@latest allo-infirmier -- --template react


# 2. Go into folder

cd allo-infirmier


# 3. Install Tailwind CSS

npm install -D tailwindcss postcss autoprefixer

npx tailwindcss init -p


# 4. Install Lucide Icons (for UI icons)

npm install lucide-react


# 5. Install Framer Motion (optional, for smooth animations)

npm install framer-motion

Step 2: Configure Tailwind

Update tailwind.config.js to look inside your source files:

codeJavaScript

downloadcontent_copy

expand_less
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        medical: {
          50: '#f0f9ff',
          100: '#e0f2fe',
          500: '#0ea5e9', // Sky blue
          600: '#0284c7',
          900: '#0c4a6e',
        }
      }
    },
  },
  plugins: [],
}

Add the directives to src/index.css:

codeCSS

downloadcontent_copy

expand_less
@tailwind base;
@tailwind components;
@tailwind utilities;

Step 3: The Code

Here is the complete code structure. You can copy/paste these into the src folder.

1. Mock Data (src/data.js)

Real French nursing context data.

codeJavaScript

downloadcontent_copy

expand_less
export const missions = [
  {
    id: 1,
    title: "Remplacement IDEL - Tournée Matin",
    type: "Libéral",
    location: "Lyon (69003)",
    date: "12 Oct - 15 Oct",
    price: "450€ / jour",
    tags: ["Soins à domicile", "Véhiculé"],
    urgent: true,
  },
  {
    id: 2,
    title: "Mission Intérim - Service Urgences",
    type: "Clinique Privée",
    location: "Paris (75015)",
    date: "20 Oct - 22 Oct",
    price: "32€ / heure",
    tags: ["Nuit", "12h"],
    urgent: false,
  },
  {
    id: 3,
    title: "Vaccination Grippe / Covid",
    type: "Pharmacie",
    location: "Bordeaux (33)",
    date: "10 Oct",
    price: "40€ / heure",
    tags: ["Ponctuel", "Journée"],
    urgent: true,
  },
  {
    id: 4,
    title: "Remplacement Cabinet - 15 jours",
    type: "Libéral",
    location: "Nice (06)",
    date: "01 Nov - 15 Nov",
    price: "Moy. 500€ / jour",
    tags: ["Logement inclus", "Zone touristique"],
    urgent: false,
  },
  {
    id: 5,
    title: "Surveillance Post-Op",
    type: "Clinique",
    location: "Marseille (13)",
    date: "Demain",
    price: "28€ / heure",
    tags: ["Après-midi"],
    urgent: true,
  },
];

2. Components

src/components/Navbar.jsx

codeJsx

downloadcontent_copy

expand_less
import React from 'react';
import { Stethoscope, User, Menu } from 'lucide-react';

const Navbar = () => {
  return (
    <nav className="bg-white shadow-sm sticky top-0 z-50">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between h-16">
          <div className="flex items-center gap-2">
            <Stethoscope className="h-8 w-8 text-medical-600" />
            <span className="font-bold text-xl text-gray-800">AlloInfirmier</span>
          </div>
          
          <div className="hidden md:flex items-center space-x-8">
            <a href="#" className="text-gray-600 hover:text-medical-600 font-medium">Trouver une mission</a>
            <a href="#" className="text-gray-600 hover:text-medical-600 font-medium">Publier une annonce</a>
            <a href="#" className="text-gray-600 hover:text-medical-600 font-medium">Blog & Ressources</a>
            <button className="bg-medical-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-medical-700 transition">
              <User size={18} />
              Connexion
            </button>
          </div>

          <div className="md:hidden flex items-center">
            <button className="text-gray-600">
              <Menu size={28} />
            </button>
          </div>
        </div>
      </div>
    </nav>
  );
};

export default Navbar;

src/components/Hero.jsx

codeJsx

downloadcontent_copy

expand_less
import React from 'react';
import { Search, MapPin } from 'lucide-react';

const Hero = ({ searchTerm, setSearchTerm, locationTerm, setLocationTerm }) => {
  return (
    <div className="bg-gradient-to-r from-medical-500 to-medical-900 py-20 px-4">
      <div className="max-w-4xl mx-auto text-center text-white">
        <h1 className="text-4xl md:text-5xl font-bold mb-6">
          La plateforme n°1 pour les <span className="text-yellow-300">Infirmiers Freelances</span>
        </h1>
        <p className="text-xl mb-8 text-medical-100">
          Trouvez des remplacements en cabinet libéral, clinique ou entreprise partout en France.
        </p>

        {/* Search Box */}
        <div className="bg-white p-4 rounded-xl shadow-lg flex flex-col md:flex-row gap-4 max-w-3xl mx-auto">
          <div className="flex-1 flex items-center border-b md:border-b-0 md:border-r border-gray-200 px-2">
            <Search className="text-gray-400 mr-2" />
            <input 
              type="text" 
              placeholder="Ex: Remplacement, Vaccination..." 
              className="w-full outline-none text-gray-700"
              value={searchTerm}
              onChange={(e) => setSearchTerm(e.target.value)}
            />
          </div>
          <div className="flex-1 flex items-center px-2">
            <MapPin className="text-gray-400 mr-2" />
            <input 
              type="text" 
              placeholder="Ville ou Code Postal" 
              className="w-full outline-none text-gray-700"
              value={locationTerm}
              onChange={(e) => setLocationTerm(e.target.value)}
            />
          </div>
          <button className="bg-medical-600 text-white font-bold py-3 px-8 rounded-lg hover:bg-medical-700 transition">
            Rechercher
          </button>
        </div>
      </div>
    </div>
  );
};

export default Hero;

src/components/MissionCard.jsx

codeJsx

downloadcontent_copy

expand_less
import React from 'react';
import { MapPin, Calendar, Euro, HeartPulse } from 'lucide-react';

const MissionCard = ({ mission }) => {
  return (
    <div className="bg-white rounded-xl shadow-md hover:shadow-xl transition duration-300 border border-gray-100 overflow-hidden flex flex-col">
      <div className="p-6 flex-grow">
        <div className="flex justify-between items-start mb-4">
          <span className={`px-3 py-1 rounded-full text-xs font-semibold ${
            mission.type === 'Libéral' ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'
          }`}>
            {mission.type}
          </span>
          {mission.urgent && (
            <span className="bg-red-100 text-red-600 text-xs font-bold px-2 py-1 rounded animate-pulse">
              URGENT
            </span>
          )}
        </div>

        <h3 className="text-lg font-bold text-gray-800 mb-2">{mission.title}</h3>

        <div className="space-y-2 text-gray-600 text-sm mb-4">
          <div className="flex items-center gap-2">
            <MapPin size={16} className="text-medical-500" />
            {mission.location}
          </div>
          <div className="flex items-center gap-2">
            <Calendar size={16} className="text-medical-500" />
            {mission.date}
          </div>
          <div className="flex items-center gap-2 font-semibold text-gray-900">
            <Euro size={16} className="text-medical-500" />
            {mission.price}
          </div>
        </div>

        <div className="flex flex-wrap gap-2">
          {mission.tags.map((tag, index) => (
            <span key={index} className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
              #{tag}
            </span>
          ))}
        </div>
      </div>

      <div className="p-4 bg-gray-50 border-t border-gray-100">
        <button className="w-full bg-white border-2 border-medical-600 text-medical-600 font-bold py-2 rounded-lg hover:bg-medical-600 hover:text-white transition flex items-center justify-center gap-2">
          <HeartPulse size={18} />
          Postuler
        </button>
      </div>
    </div>
  );
};

export default MissionCard;

3. Main App (src/App.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import MissionCard from './components/MissionCard';
import { missions } from './data';
import { Filter } from 'lucide-react';

function App() {
  const [searchTerm, setSearchTerm] = useState('');
  const [locationTerm, setLocationTerm] = useState('');

  // Simple filtering logic
  const filteredMissions = missions.filter(mission => {
    return (
      mission.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
      mission.location.toLowerCase().includes(locationTerm.toLowerCase())
    );
  });

  return (
    <div className="min-h-screen bg-gray-50 font-sans">
      <Navbar />
      
      <Hero 
        searchTerm={searchTerm} 
        setSearchTerm={setSearchTerm}
        locationTerm={locationTerm}
        setLocationTerm={setLocationTerm}
      />

      <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
        {/* Stats Section */}
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
          <div className="bg-white p-4 rounded-lg shadow-sm text-center">
            <div className="text-2xl font-bold text-medical-600">1,200+</div>
            <div className="text-sm text-gray-500">Missions actives</div>
          </div>
          <div className="bg-white p-4 rounded-lg shadow-sm text-center">
            <div className="text-2xl font-bold text-medical-600">450€</div>
            <div className="text-sm text-gray-500">Tarif moyen journalier</div>
          </div>
          <div className="bg-white p-4 rounded-lg shadow-sm text-center">
            <div className="text-2xl font-bold text-medical-600">8,500</div>
            <div className="text-sm text-gray-500">Infirmiers inscrits</div>
          </div>
          <div className="bg-white p-4 rounded-lg shadow-sm text-center">
            <div className="text-2xl font-bold text-medical-600">24/7</div>
            <div className="text-sm text-gray-500">Support dédié</div>
          </div>
        </div>

        {/* Missions Grid */}
        <div className="flex justify-between items-center mb-6">
          <h2 className="text-2xl font-bold text-gray-800">Dernières missions disponibles</h2>
          <button className="flex items-center gap-2 text-gray-600 hover:text-medical-600">
            <Filter size={18} />
            Filtres avancés
          </button>
        </div>

        {filteredMissions.length > 0 ? (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {filteredMissions.map((mission) => (
              <MissionCard key={mission.id} mission={mission} />
            ))}
          </div>
        ) : (
          <div className="text-center py-20 bg-white rounded-lg border border-dashed border-gray-300">
            <p className="text-gray-500 text-lg">Aucune mission ne correspond à votre recherche.</p>
            <button 
              onClick={() => {setSearchTerm(''); setLocationTerm('');}}
              className="mt-4 text-medical-600 font-medium hover:underline"
            >
              Voir toutes les offres
            </button>
          </div>
        )}
      </main>

      {/* Footer */}
      <footer className="bg-gray-800 text-white py-12">
        <div className="max-w-7xl mx-auto px-4 grid grid-cols-1 md:grid-cols-3 gap-8">
          <div>
            <h3 className="text-xl font-bold mb-4">AlloInfirmier</h3>
            <p className="text-gray-400 text-sm">
              La plateforme simplifiant la vie des infirmiers et infirmières en France. Trouvez des remplacements simplement.
            </p>
          </div>
          <div>
            <h4 className="font-bold mb-4 text-gray-200">Liens Rapides</h4>
            <ul className="space-y-2 text-gray-400 text-sm">
              <li><a href="#" className="hover:text-white">Comment ça marche ?</a></li>
              <li><a href="#" className="hover:text-white">Tarifs</a></li>
              <li><a href="#" className="hover:text-white">Charte de déontologie</a></li>
            </ul>
          </div>
          <div>
            <h4 className="font-bold mb-4 text-gray-200">Contact</h4>
            <p className="text-gray-400 text-sm">contact@alloinfirmier.fr</p>
            <p className="text-gray-400 text-sm">01 23 45 67 89</p>
            <p className="text-gray-400 text-sm mt-2">Paris, France</p>
          </div>
        </div>
      </footer>
    </div>
  );
}

export default App;

Step 4: Run the Application

In your terminal:

codeBash

downloadcontent_copy

expand_less
npm run dev

Key Features tailored for the French Market:

  1. Vocabulary: Uses terms like "Libéral", "Remplacement", "Tournée", "Vaccination", "Urgent".
  2. Visual Identity: Uses "Medical Blue" (teal or sky palettes in Tailwind) which is standard for health apps in France (like Doctolib).
  3. Functionality:
    • Live Search: Typing in the hero search bar instantly filters the missions below.
    • Urgency Badges: French hospitals/cabinets often need last-minute replacements, highlighted by the "URGENT" badge.
    • Tags: specific to the job (e.g., "Véhiculé" is crucial for home care/soins à domicile).
    • Responsive: Works perfectly on mobile phones, which nurses use between patients.

2- Puis je te transmettre des images HD de la cinématique souhaité que tu puisses analyser pour ensuite réaliser le code REACT JS Tailwind CSS ?

Oui, tout à fait ! C'est même la meilleure façon d'obtenir un résultat précis et fidèle à votre vision.

En tant qu'IA multimodale, je peux analyser vos images (captures d'écran, maquettes Figma, dessins) pour comprendre :

  1. La structure (Grid, Flexbox, disposition des éléments).
  2. Le style (Couleurs, typographies, ombres, arrondis).
  3. Les composants UI (Boutons, cartes, navigations, champs de formulaire).

Comment procéder :

  1. Envoyez les images directement dans ce chat (via le bouton trombone ou par glisser-déposer).
  2. Si vous avez une "cinématique" (un enchaînement d'écrans ou des animations spécifiques), précisez-moi l'ordre ou le comportement attendu (exemple : "Quand on clique sur ce bouton, le menu apparaît en glissant depuis la droite").
  3. Je générerai ensuite le code React (JSX) et les classes Tailwind CSS correspondants.

Si votre design inclut des animations complexes (transitions douces, apparitions au scroll), j'utiliserai probablement la librairie Framer Motion en plus de Tailwind pour reproduire l'effet "cinématique".

J'attends vos images pour commencer l'analyse !

2.1 -est ce que je te donnes tous les écrans d'un coup pour la cinématique ou plus par planche de 4 écrans qui suivent le cheminement de la cinématique ?

Je te recommande vivement de procéder par planches de 4 écrans (ou par groupe logique de fonctionnalités).

Voici pourquoi c'est la meilleure méthode pour obtenir un code de qualité :

1. Précision du Code

Si tu m'envoies tout d'un coup, je risque de devoir généraliser. En procédant par lot de 4, je peux me concentrer sur les détails spécifiques :

  • Les micro-interactions (hover, focus, transitions).
  • L'espacement exact (padding/margin) de Tailwind.
  • La structure des composants réutilisables pour cette section spécifique.

2. Logique de Développement (Component-Based)

En React, on construit brique par brique. Traiter par lots permet de structurer l'application intelligemment :

  • Lot 1 : Authentication & Onboarding (Login, Inscription, Profil).
  • Lot 2 : Dashboard & Recherche (Le fil d'actualité, les filtres).
  • Lot 3 : Détail d'une mission & Action (Postuler, Accepter, Map).
  • Lot 4 : Messagerie & Profil utilisateur.

3. Gestion de la "Cinématique"

Pour chaque planche de 4, n'hésite pas à ajouter une petite note explicative sur le flux, par exemple :

"Écran 1 : Accueil. Au clic sur 'Trouver une mission', on glisse vers l'Écran 2 (Liste). Si on clique sur une carte, ça ouvre l'Écran 3 (Détail) en modal."

Tu peux envoyer la première planche quand tu es prêt ! Commençons par le début du parcours utilisateur (ou la partie la plus critique selon toi).

C'est parfait. J'ai bien analysé les trois écrans de votre cinématique. Voici ce que j'ai identifié pour la reproduction :

  1. Charte Graphique :
    • Couleur Primaire (Action) : Un rose vibrant (type #FF2E63 ou Tailwind rose-500/600).
    • Couleur Secondaire (Logo) : Bleu Ciel/Azur (type blue-400).
    • Typographie : Sans-serif, propre, titres en gras (probablement Inter ou Roboto).
  2. Structure de la cinématique :
    • Écran 1 (Onboarding) : Logo centré, illustration vectorielle, titre/sous-titre, indicateurs de pagination (dots), bouton "M'inscrire" (primaire) et "Me connecter" (secondaire).
    • Écran 2 (Saisie Numéro) : Header avec bouton retour, titre accueillant, champ de saisie téléphonique avec préfixe pays (🇫🇷).
    • Écran 3 (Validation) : Même écran que le 2, mais le bouton "Continuer" apparaît une fois que l'utilisateur a commencé à saisir des données.

Voici le code React + Tailwind complet pour reproduire cette séquence. J'ai utilisé un seul fichier pour la simplicité, avec une gestion d'état pour passer de l'écran 1 à l'écran 2.

Prérequis

Assurez-vous d'avoir installé lucide-react pour les icônes (flèche retour, cœur logo) :

npm install lucide-react

Le Code (App.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState, useEffect } from 'react';
import { ChevronLeft, Heart } from 'lucide-react';

// --- Composant: Logo (Reconstitution CSS du logo Synergia) ---
const Logo = () => (
  <div className="flex flex-col items-center mb-8">
    <div className="flex items-center gap-2 mb-1">
      {/* Simulation du coeur bleu du logo */}
      <div className="relative">
        <Heart className="w-10 h-10 text-blue-400 fill-blue-400" />
        <div className="absolute top-0 right-0 w-4 h-4 bg-white opacity-20 rounded-full"></div>
      </div>
      <div className="text-left leading-tight">
        <h1 className="text-2xl font-bold text-slate-800">Synergia</h1>
        <h2 className="text-xl font-semibold text-slate-600 -mt-1">Sénior</h2>
      </div>
    </div>
  </div>
);

// --- Écran 1: Onboarding / Accueil ---
const WelcomeScreen = ({ onRegisterClick }) => {
  return (
    <div className="flex flex-col h-full bg-white px-6 pt-12 pb-8 animate-in fade-in duration-500">
      {/* Logo Section */}
      <Logo />

      {/* Illustration Area */}
      <div className="flex-grow flex items-center justify-center my-4">
        {/* Placeholder pour l'illustration "Mains tenant un téléphone" */}
        <div className="relative w-64 h-64 bg-slate-50 rounded-full flex items-center justify-center">
             {/* Remplacement par une image ou SVG similaire */}
             <img 
               src="https://illustrations.popsy.co/amber/mobile-app.svg" 
               alt="Application mobile"
               className="w-full h-full object-contain drop-shadow-lg"
             />
        </div>
      </div>

      {/* Text Content */}
      <div className="mb-8">
        <h3 className="text-xl font-bold text-slate-900 mb-3">
          Des missions taillées pour vous
        </h3>
        <p className="text-slate-500 text-sm leading-relaxed">
          Choisissez parmi les opportunités qui vous intéressent vraiment, puisqu'elles correspondent à vos compétences et disponibilités.
        </p>
      </div>

      {/* Pagination Dots */}
      <div className="flex justify-center gap-2 mb-8">
        <div className="w-2.5 h-2.5 rounded-full bg-[#FF2E63]"></div>
        <div className="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
        <div className="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
        <div className="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
      </div>

      {/* Actions */}
      <div className="space-y-3">
        <button 
          onClick={onRegisterClick}
          className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-lg hover:bg-[#e62552] transition active:scale-95"
        >
          M'inscrire
        </button>
        <button className="w-full bg-white text-slate-900 font-semibold py-4 rounded-xl border border-gray-100 shadow-sm hover:bg-gray-50 transition">
          Me connecter
        </button>
      </div>
    </div>
  );
};

// --- Écran 2 & 3: Saisie Téléphone ---
const PhoneInputScreen = ({ onBack }) => {
  const [phoneNumber, setPhoneNumber] = useState('');
  const [isFocused, setIsFocused] = useState(false);

  // Formattage auto du numéro (ex: 06 12 34 56 78)
  const handlePhoneChange = (e) => {
    // Ne garder que les chiffres
    let value = e.target.value.replace(/\D/g, '');
    if (value.length > 10) value = value.slice(0, 10);
    
    // Ajouter les espaces
    const formatted = value.replace(/(\d{2})(?=\d)/g, '$1 ');
    setPhoneNumber(formatted);
  };

  return (
    <div className="flex flex-col h-full bg-white px-6 pt-8 pb-6 animate-in slide-in-from-right duration-300">
      {/* Header Back */}
      <div className="flex items-center mb-8">
        <button onClick={onBack} className="p-2 -ml-2 text-slate-600 hover:bg-gray-100 rounded-full">
          <ChevronLeft size={28} />
        </button>
      </div>

      {/* Title Section */}
      <div className="mb-10">
        <h2 className="text-2xl font-bold text-slate-900 mb-4">
          Bienvenue chez <br/> Synergia Sénior
        </h2>
        <p className="text-slate-500 text-sm">
          Renseignez votre numéro de téléphone. Il vous permettra d'être mis en relation avec vos futurs clients.
        </p>
      </div>

      {/* Phone Input Field */}
      <div className={`
        flex items-center border rounded-xl p-1 transition-all duration-200
        ${isFocused ? 'border-[#FF2E63] ring-1 ring-[#FF2E63]/20' : 'border-slate-300'}
      `}>
        {/* Country Flag */}
        <div className="flex items-center px-3 border-r border-gray-200">
          <span className="text-2xl mr-1">🇫🇷</span>
          <span className="text-xs text-slate-500">▼</span>
        </div>
        
        {/* Input */}
        <input 
          type="tel"
          value={phoneNumber}
          onChange={handlePhoneChange}
          onFocus={() => setIsFocused(true)}
          onBlur={() => setIsFocused(false)}
          placeholder="06 12 34 56 78"
          className="w-full px-4 py-3 text-lg font-medium text-slate-900 placeholder:text-slate-300 outline-none bg-transparent"
          autoFocus
        />
      </div>

      {/* Spacer to push button to bottom or simulate keyboard space */}
      <div className="flex-grow"></div>

      {/* Button "Continuer" - Affiché conditionnellement comme sur l'image 3 */}
      {phoneNumber.length > 0 && (
        <div className="mt-auto animate-in fade-in slide-in-from-bottom-4 duration-300">
          <button className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95">
            Continuer
          </button>
        </div>
      )}

      {/* Simulation Keyboard Spacer (Optionnel pour voir le layout comme sur le mobile) */}
      <div className="h-[35vh] w-full bg-gray-50 mt-4 rounded-t-3xl flex items-center justify-center text-gray-300 border-t border-gray-200">
         <span className="text-sm">Zone Clavier iOS</span>
      </div>
    </div>
  );
};

// --- Main App Component ---
function App() {
  const [currentStep, setCurrentStep] = useState('welcome'); // 'welcome' or 'phone'

  return (
    // Container principal simulant un mobile
    <div className="min-h-screen bg-gray-100 flex items-center justify-center font-sans">
      <div className="w-full max-w-md h-screen md:h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative">
        
        {/* Status Bar Fake (Top of screen) */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900">
          <span>06:44</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className="w-6 h-3 bg-gray-800 rounded-sm"></div>
          </div>
        </div>

        {/* Content Render */}
        <div className="h-[calc(100%-3rem)]">
          {currentStep === 'welcome' && (
            <WelcomeScreen onRegisterClick={() => setCurrentStep('phone')} />
          )}
          
          {currentStep === 'phone' && (
            <PhoneInputScreen onBack={() => setCurrentStep('welcome')} />
          )}
        </div>

      </div>
    </div>
  );
}

export default App;

Détails Techniques & Explications

  1. Structure "Mobile First" :
    • J'ai encapsulé l'application dans un conteneur max-w-md (largeur max d'un téléphone) pour simuler le rendu exact de vos captures d'écran, même sur un navigateur de bureau.
  2. Couleurs Personnalisées (Tailwind Arbitrary Values) :
    • J'ai utilisé la valeur hexadécimale #FF2E63 pour le bouton rose. C'est ce qui se rapproche le plus du "Rose Synergia" visible sur vos images.
    • Les ombres (shadow-lg, shadow-[0_4px...]) ont été ajustées pour correspondre au style "flottant" moderne des boutons iOS.
  3. Logique des Champs (Input Logic) :
    • L'input téléphonique (type="tel") déclenchera automatiquement le pavé numérique sur un vrai téléphone.
    • J'ai ajouté un formateur automatique : quand vous tapez des chiffres, il ajoute automatiquement les espaces (ex: 06 12...) pour correspondre exactement à l'image 3.
  4. Transitions "Cinématiques" :
    • J'ai ajouté des classes utilitaires simples (animate-in, fade-in, slide-in-from-right) pour que le passage de l'écran 1 à 2 soit fluide, et pour que le bouton "Continuer" apparaisse doucement quand on commence à taper.

Comment tester ?

Copiez ce code dans votre fichier App.jsx (ou src/App.tsx). Si vous n'avez pas installé lucide-react, installez-le, sinon remplacez les icônes <ChevronLeft /> par du texte simple <.

Exemple de Mapping vers ODOO DB  JSON RPC ODOO version GEMINI  JSON RPS ODOO version ChatGPT

C'est parti pour la suite de la cinématique. J'ai analysé votre planche de 5 écrans.

On est sur une séquence de formulaire "étape par étape" (Wizard) très fluide.

Voici les points clés que j'ai relevés pour le code :

  1. Uniformité : Le titre est toujours en haut, gras (font-bold).
  2. Champs de saisie : Design minimaliste, bordure grise fine qui passe probablement en couleur active au focus.
  3. Bouton "Continuer" : Il n'apparaît (ou ne s'active) que si le champ est rempli. C'est un excellent pattern UX pour éviter les erreurs.
  4. Écran Email : Ajout d'un texte d'aide (helper text) gris sous le titre.
  5. Écran Pays : Changement de layout, on passe d'un input texte à deux "Cartes" de sélection (France / Royaume-Uni).

Voici le code complet intégrant cette logique de navigation séquentielle.

Le Code (App.jsx ou Wizard.jsx)

J'ai créé un composant RegistrationWizard qui gère toute cette logique.

codeJsx

downloadcontent_copy

expand_less
import React, { useState, useEffect, useRef } from 'react';
import { ChevronLeft } from 'lucide-react';

// --- Composant Reutilisable: Layout d'étape ---
const StepLayout = ({ title, subtitle, children, onBack }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 pb-6 animate-in slide-in-from-right duration-300">
    {/* Header Back */}
    <div className="flex items-center mb-6">
      <button onClick={onBack} className="p-2 -ml-2 text-slate-600 hover:bg-gray-100 rounded-full transition">
        <ChevronLeft size={28} />
      </button>
    </div>

    {/* Title Section */}
    <div className="mb-6">
      <h2 className="text-2xl font-bold text-slate-900 mb-2 leading-tight">
        {title}
      </h2>
      {subtitle && (
        <p className="text-slate-500 text-sm leading-relaxed">
          {subtitle}
        </p>
      )}
    </div>

    {/* Content (Inputs or Cards) */}
    <div className="flex-grow">
      {children}
    </div>
  </div>
);

// --- Composant Reutilisable: Bouton "Continuer" ---
const ContinueButton = ({ onClick, disabled }) => {
  if (disabled) return null; // Le bouton n'apparait pas si vide (comme sur la maquette)
  
  return (
    <div className="mt-auto animate-in fade-in slide-in-from-bottom-4 duration-300 pt-4">
      <button 
        onClick={onClick}
        className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
      >
        Continuer
      </button>
    </div>
  );
};

// --- Main Wizard Component ---
const RegistrationWizard = () => {
  // État pour suivre l'étape actuelle (0: Prénom, 1: Nom, 2: Email, 3: Pays)
  const [step, setStep] = useState(0);
  
  // État pour stocker les données du formulaire
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    country: ''
  });

  const handleNext = () => setStep(step + 1);
  const handleBack = () => step > 0 ? setStep(step - 1) : console.log("Retour accueil");

  // Mise à jour générique des champs
  const updateField = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  };

  // --- Rendu des différentes étapes ---

  // 1. Écran Prénom
  if (step === 0) {
    return (
      <StepLayout title="Quel est votre prénom ?" onBack={handleBack}>
        <input 
          type="text"
          autoFocus
          placeholder="Entrez votre prénom"
          value={formData.firstName}
          onChange={(e) => updateField('firstName', e.target.value)}
          className="w-full border border-slate-300 rounded-lg px-4 py-3 text-lg outline-none focus:border-[#FF2E63] focus:ring-1 focus:ring-[#FF2E63]/20 transition"
        />
        <ContinueButton 
          disabled={formData.firstName.length < 2} 
          onClick={handleNext} 
        />
      </StepLayout>
    );
  }

  // 2. Écran Nom
  if (step === 1) {
    return (
      <StepLayout title="Quel est votre nom de famille ?" onBack={handleBack}>
        <input 
          type="text"
          autoFocus
          placeholder="Entrez votre nom"
          value={formData.lastName}
          onChange={(e) => updateField('lastName', e.target.value)}
          className="w-full border border-slate-300 rounded-lg px-4 py-3 text-lg outline-none focus:border-[#FF2E63] focus:ring-1 focus:ring-[#FF2E63]/20 transition"
        />
        <ContinueButton 
          disabled={formData.lastName.length < 2} 
          onClick={handleNext} 
        />
      </StepLayout>
    );
  }

  // 3. Écran Email
  if (step === 2) {
    return (
      <StepLayout 
        title="Quelle est votre adresse email ?" 
        subtitle="Votre adresse email est indispensable au suivi de votre inscription et aux échanges avec notre équipe."
        onBack={handleBack}
      >
        <input 
          type="email"
          autoFocus
          placeholder="Entrez votre adresse email"
          value={formData.email}
          onChange={(e) => updateField('email', e.target.value)}
          className="w-full border border-slate-300 rounded-lg px-4 py-3 text-lg outline-none focus:border-[#FF2E63] focus:ring-1 focus:ring-[#FF2E63]/20 transition"
        />
        <ContinueButton 
          // Validation basique : contient @ et .
          disabled={!formData.email.includes('@') || !formData.email.includes('.')} 
          onClick={handleNext} 
        />
      </StepLayout>
    );
  }

  // 4. Écran Pays
  if (step === 3) {
    const countries = [
      { id: 'fr', name: 'France', flag: '🇫🇷' },
      { id: 'uk', name: 'Royaume-Uni', flag: '🇬🇧' }
    ];

    return (
      <StepLayout title="Dans quel pays souhaitez-vous recevoir des missions ?" onBack={handleBack}>
        <div className="grid grid-cols-2 gap-4 mt-2">
          {countries.map((c) => (
            <button
              key={c.id}
              onClick={() => {
                updateField('country', c.id);
                // Optionnel : Passer directement à l'étape suivante après clic
                // handleNext(); 
              }}
              className={`
                flex flex-col items-center justify-center p-6 rounded-xl border-2 transition-all duration-200
                ${formData.country === c.id 
                  ? 'border-[#FF2E63] bg-rose-50' 
                  : 'border-slate-100 bg-white hover:border-slate-200 shadow-sm'}
              `}
            >
              <span className="text-4xl mb-3 block">{c.flag}</span>
              <span className={`font-medium ${formData.country === c.id ? 'text-[#FF2E63]' : 'text-slate-600'}`}>
                {c.name}
              </span>
            </button>
          ))}
        </div>
        
        {/* Le bouton n'apparait que si un pays est selectionné */}
        <ContinueButton 
          disabled={!formData.country} 
          onClick={() => alert("Fin du flux d'inscription ! Données : " + JSON.stringify(formData))} 
        />
      </StepLayout>
    );
  }

  return null;
};

// --- App Container (Mobile Simulator) ---
function App() {
  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      {/* Simulation Mobile Device */}
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900">
        
        {/* Barre de statut iOS (Fake) */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900 bg-white">
          <span>06:46</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className="w-6 h-3 bg-gray-800 rounded-sm"></div>
          </div>
        </div>

        {/* Zone de contenu défilable mais ici fixe pour le wizard */}
        <div className="h-[calc(100%-3rem)] flex flex-col">
          <RegistrationWizard />
          
          {/* Simulation Espace Clavier (Optionnel : pour visualiser comme sur les screenshots) */}
          {/* Décommentez la ligne ci-dessous si vous voulez voir l'espace que prendrait le clavier */}
           <div className="h-[40%] bg-slate-800 flex items-center justify-center text-slate-500 text-sm">Zone Clavier Simulée</div> 
        </div>
        
        {/* Barre Home iOS */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full"></div>
      </div>
    </div>
  );
}

export default App;

Ce qui a été réalisé :

  1. Le pattern autoFocus : Sur chaque écran (Prénom, Nom, Email), le champ prend le focus automatiquement, ce qui déclencherait le clavier sur mobile, correspondant exactement à vos captures.
  2. Gestion de l'état (State Management) :
    • Le bouton "Continuer" est intelligent. Essayez de taper un nom : le bouton apparaît avec une animation fade-in slide-in-bottom, exactement comme suggéré par la différence entre vos écrans 3 et 4.
    • Si le champ est vide, le bouton disparaît (ou ne se monte pas dans le DOM).
  3. Les Cartes Pays :
    • J'ai transformé les inputs en boutons de sélection (grid grid-cols-2).
    • Au clic, la bordure devient rose (border-[#FF2E63]) et le fond légèrement teinté (bg-rose-50) pour marquer la sélection active.
  4. Typographie et Espacement : J'ai respecté les marges généreuses et la taille des titres pour garder l'aspect très aéré et "Sénior-friendly" (lisible).

Prochaine étape ?

Est-ce qu'on passe à la configuration du profil (upload de documents, photo) ou directement au Dashboard (la liste des missions) ?

Odoo Mapping JSON RPC > suggestion Google AI STUDIO   Categorie

C'est noté. On continue avec ces 4 nouveaux écrans (Domaine, Métier, Ville, Consentement).

J'ai remarqué quelques détails importants dans cette nouvelle planche :

  1. Barre de progression : Sur l'écran des métiers, une barre noire apparaît en haut.
  2. État "Indisponible" : La liste des métiers a une section grisée pour les métiers non disponibles.
  3. Checkboxes : Elles sont noires quand elles sont cochées (et non roses comme le bouton principal).
  4. Icons : Utilisation d'icônes spécifiques pour "Restauration" (Couverts) et "Soin" (Stéthoscope).

Voici le code mis à jour. J'ai étendu le RegistrationWizard pour inclure ces étapes.

Le Code (WizardExtension.jsx)

Vous pouvez remplacer ou étendre le fichier précédent avec celui-ci.

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import { ChevronLeft, Utensils, Stethoscope, Info, Check } from 'lucide-react';

// --- UI Components ---

// 1. Layout Global
const StepLayout = ({ title, subtitle, children, onBack, progress }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 pb-6 animate-in slide-in-from-right duration-300">
    
    {/* Header Navigation */}
    <div className="flex items-center justify-between mb-4">
      <button onClick={onBack} className="p-2 -ml-2 text-slate-800 hover:bg-gray-100 rounded-full transition">
        <ChevronLeft size={28} />
      </button>
      {/* Barre de progression (Optionnelle, visible sur écran métier) */}
      {progress && (
        <div className="flex-grow ml-4 h-1.5 bg-gray-100 rounded-full overflow-hidden">
          <div className="h-full bg-black rounded-full" style={{ width: `${progress}%` }}></div>
        </div>
      )}
      {/* Spacer pour équilibrer si pas de barre */}
      {!progress && <div className="w-8"></div>}
    </div>

    {/* Title Section */}
    <div className="mb-6">
      <h2 className="text-2xl font-bold text-slate-900 mb-2 leading-tight">
        {title}
      </h2>
      {subtitle && (
        <p className="text-slate-500 text-sm leading-relaxed mt-2">
          {subtitle}
        </p>
      )}
    </div>

    {/* Content Area */}
    <div className="flex-grow overflow-y-auto no-scrollbar pb-20">
      {children}
    </div>
  </div>
);

// 2. Bouton Continuer (Pink)
const ContinueButton = ({ onClick, disabled, label = "Continuer" }) => {
  if (disabled) return null;
  return (
    <div className="absolute bottom-6 left-6 right-6 animate-in fade-in slide-in-from-bottom-4 duration-300 bg-white pt-2">
      <button 
        onClick={onClick}
        className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
      >
        {label}
      </button>
    </div>
  );
};

// --- Main Wizard Logic ---

const RegistrationWizard = () => {
  const [step, setStep] = useState(4); // On commence à l'étape 4 pour la démo (Domaine)
  
  // État global du formulaire
  const [formData, setFormData] = useState({
    domain: '',         // 'restauration' | 'soin'
    job: '',            // ex: 'infirmier'
    cities: [],         // ex: ['Paris']
    consents: {
      age: false,
      tos: false,
      privacy: false
    }
  });

  const updateField = (field, value) => setFormData(prev => ({ ...prev, [field]: value }));
  const handleNext = () => setStep(step + 1);
  const handleBack = () => setStep(step - 1);

  // --- Step 4: Domaine (Restauration vs Soin) ---
  if (step === 4) {
    return (
      <StepLayout title="Dans quel domaine souhaitez-vous travailler ?" onBack={handleBack}>
        <div className="grid grid-cols-2 gap-4 mt-4">
          {[
            { id: 'restauration', label: 'Restauration', icon: <Utensils size={32} /> },
            { id: 'soin', label: 'Soin', icon: <Stethoscope size={32} /> }
          ].map((item) => (
            <button
              key={item.id}
              onClick={() => {
                updateField('domain', item.id);
                // Petit délai pour voir l'effet de sélection avant de changer de page
                setTimeout(handleNext, 150);
              }}
              className={`
                h-40 flex flex-col items-center justify-center rounded-xl border transition-all duration-200 gap-4 shadow-sm
                ${formData.domain === item.id 
                  ? 'border-slate-800 bg-slate-50 text-slate-900 ring-1 ring-slate-800' 
                  : 'border-slate-200 bg-white text-slate-500 hover:border-slate-300'}
              `}
            >
              <div className={formData.domain === item.id ? 'text-slate-900' : 'text-slate-400'}>
                {item.icon}
              </div>
              <span className="font-medium">{item.label}</span>
            </button>
          ))}
        </div>
      </StepLayout>
    );
  }

  // --- Step 5: Métier (avec Barre de progression) ---
  if (step === 5) {
    const availableJobs = [
      "Aide-soignant(e)", "Éducateur spécialisé", "Infirmier(ère)", 
      "Moniteur(rice) éducateur(e)", "Psychomotricien(ne)"
    ];
    const unavailableJobs = [
      "AES", "Auxiliaire de puériculture", "Auxiliaire de vie", "Auxiliaire de vie diplomé"
    ];

    return (
      <StepLayout 
        title="Pour quel métier souhaitez-vous recevoir des missions ?" 
        onBack={handleBack}
        progress={40} // Exemple de progression
      >
        <div className="flex flex-col gap-3">
          {/* Métiers Disponibles */}
          {availableJobs.map(job => (
            <button
              key={job}
              onClick={() => { updateField('job', job); handleNext(); }}
              className={`
                text-left px-6 py-4 rounded-full border font-medium transition-all
                ${formData.job === job 
                  ? 'bg-slate-900 text-white border-slate-900' // Selected state (implied)
                  : 'bg-white text-slate-700 border-slate-200 hover:border-slate-400'}
              `}
            >
              {job}
            </button>
          ))}

          {/* Section Indisponible */}
          <div className="mt-8 mb-4 flex items-center justify-between text-slate-900 font-bold text-sm">
            <span>Métiers indisponibles pour le moment</span>
            <Info size={16} className="text-slate-400" />
          </div>

          {unavailableJobs.map(job => (
            <div key={job} className="px-6 py-4 rounded-full bg-slate-100 text-slate-400 font-medium text-sm border border-transparent">
              {job}
            </div>
          ))}
        </div>
      </StepLayout>
    );
  }

  // --- Step 6: Ville ---
  if (step === 6) {
    const cities = ["Paris", "Lille", "Marseille", "Lyon"];
    
    // Logique multi-select simple pour l'exemple, ou mono-select
    const toggleCity = (city) => {
      const current = formData.cities;
      if (current.includes(city)) {
        updateField('cities', current.filter(c => c !== city));
      } else {
        updateField('cities', [...current, city]);
      }
    };

    return (
      <StepLayout title="Dans quelle ville souhaitez-vous recevoir des missions ?" onBack={handleBack}>
        <div className="flex flex-wrap gap-3">
          {cities.map(city => {
            const isSelected = formData.cities.includes(city);
            return (
              <button
                key={city}
                onClick={() => toggleCity(city)}
                className={`
                  px-6 py-3 rounded-full border font-medium transition-all
                  ${isSelected
                    ? 'bg-slate-900 text-white border-slate-900'
                    : 'bg-white text-slate-700 border-slate-200 hover:border-slate-300'}
                `}
              >
                {city}
              </button>
            );
          })}
        </div>
        <ContinueButton 
          disabled={formData.cities.length === 0} 
          onClick={handleNext} 
        />
      </StepLayout>
    );
  }

  // --- Step 7: Consentement (Legal) ---
  if (step === 7) {
    const toggleConsent = (key) => {
      setFormData(prev => ({
        ...prev,
        consents: { ...prev.consents, [key]: !prev.consents[key] }
      }));
    };

    const allChecked = formData.consents.age && formData.consents.tos && formData.consents.privacy;

    // Composant Checkbox personnalisé pour matcher le style noir carré
    const CheckboxRow = ({ checked, onClick, label }) => (
      <div className="flex items-start gap-4 py-2 cursor-pointer" onClick={onClick}>
        <div className={`
          w-6 h-6 flex-shrink-0 rounded border transition-colors duration-200 flex items-center justify-center mt-0.5
          ${checked ? 'bg-black border-black' : 'bg-white border-slate-300'}
        `}>
          {checked && <Check size={16} className="text-white" strokeWidth={3} />}
        </div>
        <div className="text-sm text-slate-800 font-medium leading-relaxed select-none">
          {label}
        </div>
      </div>
    );

    return (
      <StepLayout title="Avant d'aller plus loin" onBack={handleBack}>
        <div className="flex flex-col gap-6 mt-2">
          
          <CheckboxRow 
            checked={formData.consents.age}
            onClick={() => toggleConsent('age')}
            label="Je déclare avoir plus de 18 ans"
          />

          <CheckboxRow 
            checked={formData.consents.tos}
            onClick={() => toggleConsent('tos')}
            label={
              <span>
                J'accepte les Conditions Générales d'Utilisation de <span className="underline font-bold">Synergia</span>, <span className="underline font-bold">Stripe</span> et <span className="underline font-bold">Stripe Connect</span>
              </span>
            }
          />

          <CheckboxRow 
            checked={formData.consents.privacy}
            onClick={() => toggleConsent('privacy')}
            label={
              <span>
                J'accepte la <span className="underline font-bold">politique de confidentialité de Synergia</span>
              </span>
            }
          />

        </div>
        
        {/* Le bouton n'apparait que si tout est coché */}
        <ContinueButton 
          disabled={!allChecked} 
          onClick={() => alert("Inscription Terminée ! Bienvenue chez Synergia.")} 
        />
      </StepLayout>
    );
  }

  return null;
};

// --- App Container (Mobile Simulator) ---
function App() {
  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900">
        
        {/* Status Bar */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900 bg-white z-10 relative">
          <span>06:47</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className="w-6 h-3 bg-gray-800 rounded-sm"></div>
          </div>
        </div>

        {/* Content */}
        <div className="h-[calc(100%-3rem)] relative">
          <RegistrationWizard />
        </div>
        
        {/* Home Indicator */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-20"></div>
      </div>
    </div>
  );
}

export default App;

Points forts de cette implémentation :

  1. Respect des Checkboxes : J'ai créé un composant CheckboxRow. Quand on clique, la case devient noire avec une coche blanche (bg-black), exactement comme sur votre dernière capture (Écran 5).
  2. Gestion des états indisponibles : Sur l'écran métier, j'ai séparé visuellement les jobs actifs (cliquables) de ceux "Indisponibles pour le moment" (gris clair, fond gris, non cliquable), avec la petite icône Info.
  3. Progression : La barre noire en haut de l'écran "Métier" est dynamique. Vous pouvez changer la prop progress={40} pour ajuster le niveau de remplissage.
  4. UX "Pills" : Pour les villes et les métiers, j'ai utilisé des boutons arrondis (rounded-full) qui réagissent au clic.
  5. Restauration vs Soin : Les deux grandes cartes utilisent un Grid layout et des icônes vectorielles claires.


Ceci conclut le flux d'inscription complet ! Avez-vous besoin de voir l'écran suivant (Le Dashboard une fois connecté) ou une autre partie de l'app ?

Ci-dessous le Mapping vers ODOO via JSON RPC + ODOO studio extention 

Odoo Mapping via JSON RPC > suggestion AI STUDIO google  Categorie

C'est noté. Cette planche est très intéressante car elle nous fait sortir temporairement de l'interface "Synergia" pour simuler une expérience utilisateur réaliste : la vérification d'email via une application tierce (type Gmail), avant de revenir dans l'app.

Voici les 4 écrans à coder :

  1. Consultez vos emails : L'écran d'attente dans l'app Synergia.
  2. Inbox (Gmail) : Simulation de la liste des emails reçus.
  3. Détail du mail : Le contenu du mail avec le bouton de validation.
  4. Intro Profil : Retour dans l'app Synergia pour annoncer la durée du processus.

Voici le code complet pour cette séquence ("The Email Loop").

Le Code (EmailVerificationFlow.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import { 
  ArrowLeft, Mail, Menu, Search, Star, Pen, Video, 
  Archive, Trash2, MoreVertical, CornerUpLeft, CornerRightUp, 
  Smile, ChevronLeft, Clock 
} from 'lucide-react';

// --- Composant Helper: Logo Synergia (Version Texte) ---
const SynergiaLogo = () => (
  <div className="flex items-center gap-2 mb-4 select-none">
    <div className="relative">
       {/* Coeur simplifié */}
       <svg width="32" height="32" viewBox="0 0 24 24" fill="#60A5FA" className="text-blue-400">
         <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
       </svg>
    </div>
    <div className="text-left leading-none">
      <h1 className="text-xl font-bold text-slate-800">Synergia</h1>
      <h2 className="text-lg font-semibold text-slate-500">Sénior</h2>
    </div>
  </div>
);

// --- Écran 1: Consultez vos emails (App Synergia) ---
const CheckEmailScreen = ({ onOpenMailApp }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-12 pb-6 animate-in fade-in duration-500">
    <div className="flex justify-end">
       <MoreVertical size={24} className="text-slate-400" />
    </div>

    <div className="flex-grow flex flex-col items-center justify-center text-center -mt-20">
      {/* Illustration Enveloppe (Simplifiée en CSS/SVG) */}
      <div className="relative mb-8">
        <div className="w-32 h-24 bg-rose-50 border-2 border-slate-900 rounded-lg flex items-center justify-center relative transform rotate-[-5deg]">
          <Mail size={48} className="text-slate-900" />
          {/* Badge de notification */}
          <div className="absolute -top-3 -right-3 bg-[#FF2E63] text-white font-bold w-8 h-8 flex items-center justify-center rounded-full border-2 border-white shadow-sm">
            1
          </div>
          {/* Étoiles décoratives */}
          <Star size={16} className="absolute -top-6 -left-4 text-rose-400 fill-rose-400 animate-pulse" />
          <Star size={12} className="absolute bottom-2 -right-8 text-rose-300 fill-rose-300" />
        </div>
      </div>

      <h2 className="text-2xl font-bold text-slate-900 mb-4">
        Consultez vos emails
      </h2>
      <p className="text-slate-500 text-sm leading-relaxed px-4 mb-6">
        Nous vous avons envoyé un lien à <span className="text-slate-900 font-medium">jhlauret@gmail.com</span> pour finaliser la création de votre compte.
      </p>
      
      <button className="text-slate-900 font-bold text-sm underline decoration-2 decoration-slate-200 hover:decoration-slate-900 transition">
        Modifier mon adresse email
      </button>
    </div>

    <div className="space-y-3 mt-auto">
      <button 
        onClick={onOpenMailApp}
        className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
      >
        Ouvrir ma boîte mail
      </button>
      <button className="w-full bg-white text-slate-900 font-bold py-4 rounded-xl border border-slate-200 hover:bg-gray-50 transition">
        Renvoyer l'email
      </button>
    </div>
  </div>
);

// --- Écran 2: Simulation Inbox (Style Gmail) ---
const GmailInboxScreen = ({ onEmailClick }) => {
  const emails = [
    { id: 1, sender: "Sinergia", subject: "Confirmez votre adresse e-mail", snippet: "Bonjour, Pour finaliser la création de vo...", time: "06:47", unread: true, color: "bg-slate-400" },
    { id: 2, sender: "Replit", subject: "Your Replit receipt [#1403-3560]", snippet: "Receipt from Replit [#1403-3560] Amoun...", time: "05:23", unread: false, color: "bg-indigo-500" },
    { id: 3, sender: "The Register", subject: "Top Daily Headlines: One-fifth of the j...", snippet: "Hi lauret | Log in The Register Top Daily H...", time: "04:11", unread: false, color: "bg-blue-600" },
    { id: 4, sender: "Promotions", subject: "Learning Technologies Fran...", snippet: "Nouveaux cours disponibles", time: "", badge: "99 nouveaux", icon: true, color: "bg-green-600" },
  ];

  return (
    <div className="flex flex-col h-full bg-white relative animate-in slide-in-from-right duration-300">
      {/* Header Search Bar (Gmail Style) */}
      <div className="px-4 pt-4 pb-2">
        <div className="bg-slate-100 rounded-full h-12 flex items-center px-4 shadow-sm">
          <Menu className="text-slate-600 mr-3" />
          <input type="text" placeholder="Rechercher dans les messag..." className="bg-transparent flex-grow outline-none text-slate-700 placeholder:text-slate-500 text-sm" />
          <div className="w-8 h-8 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs ml-2">J</div>
        </div>
      </div>

      <div className="px-4 py-2 text-xs font-semibold text-slate-500 tracking-wider">Principale</div>

      {/* List */}
      <div className="flex-grow overflow-y-auto">
        {emails.map((email) => (
          <div 
            key={email.id} 
            onClick={() => email.id === 1 && onEmailClick()}
            className={`flex items-start gap-4 px-4 py-3 active:bg-slate-100 ${email.unread ? 'bg-white' : 'bg-white'}`}
          >
            <div className={`w-10 h-10 rounded-full flex-shrink-0 flex items-center justify-center text-white font-bold text-lg ${email.color}`}>
              {email.icon ? <div className="text-xs">🏷️</div> : email.sender[0]}
            </div>
            <div className="flex-grow min-w-0 border-b border-slate-100 pb-3">
              <div className="flex justify-between items-baseline mb-0.5">
                <span className={`text-base truncate ${email.unread ? 'font-bold text-slate-900' : 'font-medium text-slate-700'}`}>
                  {email.sender}
                </span>
                <span className={`text-xs ${email.unread ? 'font-bold text-slate-900' : 'text-slate-500'}`}>
                  {email.time || email.badge}
                </span>
              </div>
              <div className={`text-sm truncate mb-0.5 ${email.unread ? 'font-bold text-slate-900' : 'text-slate-600'}`}>
                {email.subject}
              </div>
              <div className="flex justify-between items-center">
                <div className="text-sm text-slate-500 truncate pr-4">{email.snippet}</div>
                <Star size={16} className="text-slate-300 flex-shrink-0" />
              </div>
              {email.badge && (
                <span className="inline-block bg-green-100 text-green-800 text-[10px] font-bold px-2 py-0.5 rounded-full mt-1">
                  {email.badge}
                </span>
              )}
            </div>
          </div>
        ))}
      </div>

      {/* FAB (Floating Action Button) */}
      <div className="absolute bottom-20 right-4 bg-white text-rose-900 px-4 py-3 rounded-2xl shadow-lg border border-slate-100 flex items-center gap-2 font-semibold text-sm">
        <Pen size={18} />
        Nouveau message
      </div>

      {/* Bottom Nav Simulation */}
      <div className="h-16 border-t border-slate-200 flex items-center justify-around bg-white">
        <div className="flex flex-col items-center relative">
           <div className="bg-rose-100 px-4 py-1 rounded-full mb-1">
             <Mail size={20} className="text-slate-900" />
           </div>
           <span className="absolute -top-1 right-2 bg-red-600 text-white text-[10px] font-bold px-1 rounded-full">99+</span>
        </div>
        <div className="flex flex-col items-center opacity-50">
           <Video size={24} className="text-slate-600" />
        </div>
      </div>
    </div>
  );
};

// --- Écran 3: Détail du mail (Confirmation) ---
const EmailDetailScreen = ({ onValidate, onBack }) => (
  <div className="flex flex-col h-full bg-white animate-in slide-in-from-right duration-300">
    {/* Header Actions */}
    <div className="flex justify-between items-center px-4 py-4">
      <button onClick={onBack}><ArrowLeft className="text-slate-700" /></button>
      <div className="flex gap-4 text-slate-700">
        <Archive size={22} />
        <Trash2 size={22} />
        <Mail size={22} />
        <MoreVertical size={22} />
      </div>
    </div>

    {/* Email Header */}
    <div className="px-4 mb-6">
      <div className="flex justify-between items-start mb-4">
        <h2 className="text-xl font-medium text-slate-900 leading-tight w-5/6">
          Confirmez votre adresse e-mail
        </h2>
        <Star size={20} className="text-slate-400 mt-1" />
      </div>
      
      <div className="flex items-center gap-3">
        <div className="w-10 h-10 rounded-full bg-slate-400 flex items-center justify-center text-white font-bold">
          B
        </div>
        <div className="flex-grow">
          <div className="flex items-center gap-2">
            <span className="font-bold text-slate-900">Brigad</span>
            <span className="text-xs text-slate-500">06:47</span>
          </div>
          <div className="flex items-center gap-1 text-xs text-slate-500">
            à moi <span className="text-xs">▼</span>
          </div>
        </div>
        <div className="flex gap-3 text-slate-500">
          <CornerUpLeft size={20} />
          <MoreVertical size={20} />
        </div>
      </div>
    </div>

    {/* Email Content */}
    <div className="px-4 py-2">
      <div className="mb-8">
        <SynergiaLogo />
      </div>
      
      <p className="text-slate-600 mb-6 text-sm">Bonjour,</p>
      <p className="text-slate-600 mb-8 text-sm leading-relaxed">
        Pour finaliser la création de votre compte I veuillez confirmer votre adresse e-mail en cliquant sur le bouton ci-dessous.
      </p>

      <button 
        onClick={onValidate}
        className="bg-[#FF2E63] text-white font-bold py-3 px-6 rounded-lg text-sm shadow-sm hover:bg-rose-600 transition block mx-auto"
      >
        Valider mon adresse e-mail
      </button>

      <div className="mt-12 pt-6 border-t border-slate-100">
        <p className="text-slate-400 text-xs font-medium">Brigad</p>
      </div>
    </div>

    {/* Footer Reply */}
    <div className="mt-auto p-4 flex gap-2">
      <button className="flex-1 bg-slate-700 text-white rounded-full py-3 flex items-center justify-center gap-2 text-sm font-medium">
        <CornerUpLeft size={16} /> Répondre
      </button>
      <button className="flex-1 bg-slate-700 text-white rounded-full py-3 flex items-center justify-center gap-2 text-sm font-medium">
        <CornerRightUp size={16} /> Transférer
      </button>
      <button className="w-12 flex items-center justify-center bg-slate-700 text-white rounded-full">
        <Smile size={20} />
      </button>
    </div>
  </div>
);

// --- Écran 4: Intro Profil (Retour App Synergia) ---
const ProfileIntroScreen = () => (
  <div className="flex flex-col h-full bg-white px-6 pt-12 pb-6 animate-in fade-in duration-500">
    <div className="flex justify-end mb-4">
       <MoreVertical size={24} className="text-slate-400" />
    </div>

    <div className="flex-grow flex flex-col items-center mt-4">
      <h2 className="text-2xl font-bold text-slate-900 mb-4 text-center">
        Vous allez créer votre profil
      </h2>
      <p className="text-slate-500 text-sm leading-relaxed mb-10 text-left w-full">
        Cette étape est indispensable pour recevoir des missions. Vous pouvez quitter et reprendre quand vous le souhaitez : <span className="font-bold text-slate-700">vos réponses seront enregistrées à chaque étape.</span>
      </p>
      
      {/* Illustration Horloge */}
      <div className="relative mb-6">
        <div className="w-32 h-32 rounded-full border-4 border-slate-900 flex items-center justify-center relative">
          <Clock size={64} className="text-slate-900" strokeWidth={1.5} />
          {/* Flèche rose autour (simulation SVG) */}
          <div className="absolute -right-2 top-0">
             <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#FF2E63" strokeWidth="3" strokeLinecap="round">
               <path d="M12 2a10 10 0 0 1 10 10"></path>
               <path d="M22 12l-4-4"></path>
             </svg>
          </div>
           {/* Etoiles */}
           <Star size={16} className="absolute top-2 -left-2 text-rose-400 fill-rose-400" />
        </div>
      </div>
      
      <p className="text-slate-900 font-bold mb-8">Prévoyez 15 minutes</p>
    </div>

    <div className="mt-auto">
      <button className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95">
        D'accord
      </button>
    </div>
  </div>
);

// --- Main App Controller ---
function App() {
  const [step, setStep] = useState(1);

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      {/* Simulation Mobile Device */}
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900">
        
        {/* Status Bar */}
        <div className={`h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold z-20 relative ${step === 2 || step === 3 ? 'bg-white text-slate-500' : 'bg-white text-gray-900'}`}>
          <span>06:48</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className={`w-6 h-3 rounded-sm ${step === 2 || step === 3 ? 'bg-slate-400' : 'bg-gray-800'}`}></div>
          </div>
        </div>

        {/* Dynamic Content */}
        <div className="h-[calc(100%-3rem)] relative bg-white">
          {step === 1 && <CheckEmailScreen onOpenMailApp={() => setStep(2)} />}
          {step === 2 && <GmailInboxScreen onEmailClick={() => setStep(3)} />}
          {step === 3 && <EmailDetailScreen onValidate={() => setStep(4)} onBack={() => setStep(2)} />}
          {step === 4 && <ProfileIntroScreen />}
        </div>
        
        {/* Home Indicator */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-30"></div>
      </div>
    </div>
  );
}

export default App;

Détails clés de l'implémentation :

  1. Réalisme "Fake App" :
    • L'Inbox Gmail est reproduite fidèlement : barre de recherche gris clair arrondie, avatar coloré, texte en gras pour le non-lu, troncature du texte (truncate) pour les snippets.
    • Le bouton "Nouveau message" (FAB) flotte bien en bas à droite.
    • La navigation du bas imite celle de Gmail Android (Mail / Visio).
  2. L'Email de Validation :
    • J'ai repris le logo "Synergia Sénior" que j'avais créé pour le premier écran, assurant une cohérence visuelle.
    • Le bouton "Valider" utilise bien la couleur #FF2E63.
  3. L'Écran 4 (Intro Profil) :
    • L'icône "Horloge" est entourée d'un élément SVG personnalisé pour simuler la flèche de rotation rose vue sur votre image.
    • Le texte met bien en avant "15 minutes" et "vos réponses seront enregistrées".

Pour tester, cliquez simplement sur le bouton rose "Ouvrir ma boîte mail" à l'écran 1, puis sur le premier email "Sinergia" dans la liste, et enfin sur "Valider mon adresse".

On passe à la création effective du profil (Photo, CNI, Diplôme) ?

JSON RPC ODOO version GEMINI Categorie

C'est un excellent exemple de Gamification dans l'onboarding : l'utilisateur doit accomplir des tâches (regarder des vidéos) pour débloquer la suite.

J'ai analysé les 5 écrans. Voici la logique à implémenter :

  1. Liste des tâches (State management) : Une liste d'items qui changent d'état (icône Play grise -> icône Check verte).
  2. Le Player Vidéo (Simulé) : Une interface immersive en plein écran (noir, contrôles superposés).
  3. Logique de déblocage : Le bouton "Continuer" (rose) est caché ou désactivé tant que toutes les vidéos n'ont pas le statut "Vue".

Voici le code complet pour cette séquence "Video Learning".

Le Code (VideoOnboarding.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState, useEffect } from 'react';
import { ChevronLeft, Play, Check, ChevronRight, X, Volume2, Maximize2, Share2, Edit2, Pause } from 'lucide-react';

// --- Données Mockées (Les vidéos) ---
const VIDEO_DATA = [
  {
    id: 1,
    title: "Le fonctionnement de Synergia", // J'ai adapté "Brigad" en "Synergia" pour la cohérence
    duration: "1:30",
    presenter: "Thomas",
    thumbnailColor: "bg-blue-100",
    videoImage: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80", // Portrait homme
    subtitle: "l'application Synergia."
  },
  {
    id: 2,
    title: "La liberté d'accepter des missions",
    duration: "0:45",
    presenter: "Mathilde",
    thumbnailColor: "bg-green-100",
    videoImage: "https://images.unsplash.com/photo-1531123897727-8f129e1688ce?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80", // Portrait femme
    subtitle: "Bonjour, je suis Mathilde"
  }
];

// --- Composant : Player Vidéo Plein Écran ---
const FullScreenVideoPlayer = ({ video, onClose, onComplete }) => {
  const [progress, setProgress] = useState(0);
  const [isPlaying, setIsPlaying] = useState(true);

  // Simulation de la lecture vidéo
  useEffect(() => {
    let interval;
    if (isPlaying) {
      interval = setInterval(() => {
        setProgress((prev) => {
          if (prev >= 100) {
            clearInterval(interval);
            onComplete(video.id); // Marquer comme vu à la fin
            return 100;
          }
          return prev + 1; // Vitesse de progression simulée
        });
      }, 50); // Rapide pour la démo
    }
    return () => clearInterval(interval);
  }, [isPlaying, video.id, onComplete]);

  return (
    <div className="fixed inset-0 z-50 bg-black flex flex-col items-center justify-center animate-in fade-in duration-300">
      
      {/* Background Image (Simulation Video) */}
      <div className="absolute inset-0 opacity-80">
        <img src={video.videoImage} alt="Video content" className="w-full h-full object-cover" />
      </div>

      {/* Top Bar */}
      <div className="absolute top-0 left-0 right-0 p-6 flex justify-between items-center text-white z-20 pt-12">
        <button onClick={onClose} className="p-2 bg-black/20 backdrop-blur-md rounded-full">
          <ChevronLeft size={24} />
        </button>
        <div className="flex gap-4">
           {/* Fake Controls */}
        </div>
      </div>

      {/* Subtitles Overlay */}
      <div className="absolute bottom-32 px-6 text-center z-10 w-full">
         <p className="text-white text-xl font-bold drop-shadow-md leading-tight">
           {video.subtitle}
         </p>
      </div>

      {/* PiP Mock (Comme sur l'écran 4) - Uniquement pour la 2ème vidéo */}
      {video.id === 2 && (
        <div className="absolute bottom-24 left-6 w-20 h-36 bg-white rounded-xl overflow-hidden border-2 border-white shadow-lg z-20">
            <img 
              src={VIDEO_DATA[0].videoImage} 
              className="w-full h-full object-cover opacity-80" 
              alt="Pip" 
            />
            <div className="absolute inset-0 bg-black/10 flex items-center justify-center">
                <div className="w-6 h-6 bg-white/90 rounded-full flex items-center justify-center">
                   <Play size={10} className="fill-black text-black ml-0.5" />
                </div>
            </div>
        </div>
      )}

      {/* Bottom Controls */}
      <div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/90 to-transparent pt-20 z-20">
        {/* Timeline */}
        <div className="flex items-center gap-4 mb-4">
           {/* Actions Button (Share/Edit like screen 4) */}
           <div className="flex gap-3">
              <button className="w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center text-white">
                 <Share2 size={18} />
              </button>
              <button className="w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center text-white">
                 <Edit2 size={18} />
              </button>
           </div>
           
           <div className="flex-grow flex flex-col justify-end h-10 pb-1">
              <div className="w-full h-1 bg-white/30 rounded-full overflow-hidden">
                <div 
                  className="h-full bg-white transition-all duration-100 ease-linear" 
                  style={{ width: `${progress}%` }}
                ></div>
              </div>
              <div className="flex justify-between text-[10px] text-white font-medium mt-1">
                 <span>0:{Math.floor(progress / 100 * 37)}</span>
                 <span>0:37</span>
              </div>
           </div>
        </div>
        
        {/* Home Indicator */}
        <div className="w-32 h-1 bg-white/50 rounded-full mx-auto mt-2"></div>
      </div>
    </div>
  );
};

// --- Composant Principal : Liste ---
const VideoOnboardingScreen = () => {
  const [watchedIds, setWatchedIds] = useState([]);
  const [activeVideo, setActiveVideo] = useState(null); // Si null = liste, sinon = player

  // Marquer une vidéo comme vue
  const handleVideoComplete = (id) => {
    if (!watchedIds.includes(id)) {
      setWatchedIds(prev => [...prev, id]);
    }
    // On ferme le player automatiquement à la fin ou on attend que l'user ferme ?
    // Pour l'UX fluide, on peut laisser l'user fermer, mais ici pour la démo je ferme :
    setTimeout(() => setActiveVideo(null), 500);
  };

  const allWatched = VIDEO_DATA.every(v => watchedIds.includes(v.id));

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      {/* Mobile Frame */}
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900 flex flex-col">
        
        {/* Status Bar */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900 bg-white z-10">
          <span>06:48</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className="w-6 h-3 bg-gray-800 rounded-sm"></div>
          </div>
        </div>

        {/* --- Screen Content (List) --- */}
        <div className="flex-grow flex flex-col px-6 pt-4 pb-6">
          
          {/* Header & Progress Bar */}
          <div className="mb-8">
            <button className="mb-4 -ml-2 p-2 rounded-full hover:bg-gray-100">
               <ChevronLeft size={28} className="text-slate-800" />
            </button>
            
            {/* Progress Segments */}
            <div className="flex gap-2 mb-6 h-1.5">
               <div className="flex-1 bg-black rounded-full"></div>
               <div className="flex-1 bg-gray-200 rounded-full"></div>
            </div>

            <h1 className="text-2xl font-bold text-slate-900 mb-2">
              Découvrez Synergia et ses avantages pour les indépendants
            </h1>
            <p className="text-slate-500 text-sm leading-relaxed">
              Visionnez chaque vidéo entièrement pour passer à l'étape suivante.
            </p>
          </div>

          {/* List Items */}
          <div className="space-y-6">
            {VIDEO_DATA.map((video) => {
              const isWatched = watchedIds.includes(video.id);
              
              return (
                <div 
                  key={video.id}
                  onClick={() => setActiveVideo(video)}
                  className="flex items-center justify-between group cursor-pointer"
                >
                  <div className="flex items-start gap-4">
                    {/* Icon Container */}
                    <div className={`
                      w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 transition-colors duration-300
                      ${isWatched ? 'bg-transparent' : 'bg-transparent'}
                    `}>
                       {isWatched ? (
                         // Checked Icon (Green circle with check)
                         <div className="w-8 h-8 rounded-full border-2 border-emerald-500 flex items-center justify-center">
                            <Check size={16} className="text-emerald-500" strokeWidth={3} />
                         </div>
                       ) : (
                         // Play Icon (Outline circle with play)
                         <div className="w-8 h-8 rounded-full border-2 border-slate-900 flex items-center justify-center">
                            <Play size={14} className="text-slate-900 fill-slate-900 ml-0.5" />
                         </div>
                       )}
                    </div>

                    {/* Text Info */}
                    <div className="pt-1">
                      <h3 className="font-bold text-slate-900 text-base mb-0.5">
                        {video.title}
                      </h3>
                      <p className={`text-sm font-medium transition-colors duration-300 ${
                        isWatched ? 'text-emerald-500' : 'text-slate-500'
                      }`}>
                        {isWatched ? 'Vidéo vue' : 'Vidéo à regarder'}
                      </p>
                    </div>
                  </div>

                  <ChevronRight size={20} className="text-slate-300" />
                </div>
              );
            })}
          </div>

          {/* Bottom Action - Continuer (Only if all watched) */}
          {allWatched && (
             <div className="mt-auto animate-in fade-in slide-in-from-bottom-4 duration-500 pt-4">
               <button 
                 className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
               >
                 Continuer
               </button>
             </div>
          )}

        </div>

        {/* Video Player Overlay (Conditional Render) */}
        {activeVideo && (
           <FullScreenVideoPlayer 
              video={activeVideo} 
              onClose={() => setActiveVideo(null)}
              onComplete={handleVideoComplete}
           />
        )}

        {/* Home Indicator */}
        {!activeVideo && (
            <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-10"></div>
        )}

      </div>
    </div>
  );
};

export default VideoOnboardingScreen;

Explication des fonctionnalités :

  1. Gestion des états (State) :
    • watchedIds : Tableau qui stocke les ID des vidéos terminées.
    • L'interface de liste vérifie cet état pour afficher soit l'icône "Play" noire avec le texte gris "Vidéo à regarder", soit l'icône "Check" verte avec le texte vert "Vidéo vue".
  2. Le Player Vidéo :
    • C'est un composant full-screen (fixed inset-0) qui s'affiche par-dessus tout.
    • J'ai simulé une vidéo avec une image de fond (img) et un timer (useEffect + setInterval) qui remplit la barre de progression en bas.
    • Astuce : Pour tester rapidement, la vidéo "se lit" en accéléré. Une fois la barre à 100%, elle se ferme et marque la tâche comme accomplie.
  3. Le PiP (Picture in Picture) :
    • Regardez dans le code du Player, j'ai ajouté une condition : {video.id === 2 && (...)}.
    • Si c'est la 2ème vidéo, j'affiche un petit rectangle en bas à gauche (bottom-24 left-6) contenant l'image de la première vidéo. Cela correspond exactement à votre écran n°4.
  4. Le Bouton Continuer :
    • Il utilise un rendu conditionnel : {allWatched && (...)}. Il n'apparaît dans le DOM que lorsque toutes les vidéos de la liste sont dans watchedIds, correspondant à l'écran n°5.

Prêt pour la suite ? (Peut-être la map ou le détail d'une mission ?)

JSON RPC ODOO version GEMINI Categorie

1- Planche 1

C'est parti pour cette nouvelle planche consacrée à la Création du Profil.

On y voit une transition intéressante entre un écran d'accueil de processus (écran 1), une modale de recherche d'adresse (écran 2), et la suite du flux "Wizard" (écrans 3 et 4).

Voici les points clés techniques :

  1. L'Écran "Cover" : Une liste d'étapes où seule la première est active (texte noir + flèche), les autres sont grisées.
  2. La Recherche d'adresse : C'est une modale (ou une page full-screen) qui simule l'autocomplétion (Google Places).
  3. Persistance des données : L'adresse sélectionnée à l'étape 2 se retrouve pré-remplie à l'étape 3.
  4. Barre de progression : Elle évolue entre l'écran 3 et 4.

Voici le code complet pour cette séquence.

Le Code (ProfileCreationFlow.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import { ChevronLeft, ArrowRight, MoreVertical, X, Search, MapPin, Info } from 'lucide-react';

// --- Composant : Écran d'Introduction (Cover) ---
const IntroScreen = ({ onStart }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 pb-6 animate-in fade-in duration-500">
    {/* Header */}
    <div className="flex justify-between items-center mb-8">
      <button className="p-2 -ml-2 rounded-full hover:bg-gray-100">
        <ChevronLeft size={28} className="text-slate-800" />
      </button>
      <button className="p-2 -mr-2 rounded-full hover:bg-gray-100">
        <MoreVertical size={24} className="text-slate-800" />
      </button>
    </div>

    {/* Illustration (Simulation main tenant téléphone) */}
    <div className="flex justify-center mb-8">
      <div className="relative w-40 h-40">
         {/* Cercle de fond décoratif (optionnel) */}
         <div className="absolute inset-0 bg-rose-50 rounded-full scale-90"></div>
         {/* SVG Illustration simplifiée */}
         <img 
           src="https://illustrations.popsy.co/amber/smartphone-addiction.svg" 
           alt="Hand holding phone"
           className="relative z-10 w-full h-full object-contain"
         />
      </div>
    </div>

    {/* Titre */}
    <h1 className="text-2xl font-bold text-slate-900 mb-8">
      Créez votre profil
    </h1>

    {/* Liste des étapes */}
    <div className="space-y-6 mb-8">
      {/* Étape Active */}
      <div className="flex items-center gap-3 text-slate-900">
        <ArrowRight size={20} strokeWidth={2.5} />
        <span className="font-bold text-lg">Précisez vos critères de mission</span>
      </div>

      {/* Étapes Inactives */}
      <div className="flex flex-col gap-5 pl-8 border-l border-gray-100 ml-2.5 py-1">
        <span className="text-slate-400 font-medium">Renseignez vos expériences</span>
        <span className="text-slate-400 font-medium">Complétez vos informations</span>
        <span className="text-slate-400 font-medium">Validez votre profil avec notre équipe</span>
      </div>
    </div>

    {/* Bouton Démarrer */}
    <div className="mt-auto">
      <button 
        onClick={onStart}
        className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
      >
        Démarrer
      </button>
    </div>
  </div>
);

// --- Composant : Recherche Adresse (Modal) ---
const AddressSearchScreen = ({ onSelectAddress, onClose }) => {
  const [query, setQuery] = useState("137"); // Simule la frappe utilisateur

  // Mock results pour "137"
  const predictions = [
    { main: "137 Rue de l'Université", secondary: "Paris, France" },
    { main: "137 Boulevard de Sébastopol", secondary: "Paris, France" },
    { main: "137 Rue du Temple", secondary: "Paris, France" },
    { main: "137 Rue du Faubourg du Temple", secondary: "Paris, France" },
    { main: "137 Rue Oberkampf", secondary: "Paris, France" },
  ];

  return (
    <div className="absolute inset-0 bg-white z-50 flex flex-col animate-in slide-in-from-bottom duration-300">
      {/* Header Modal */}
      <div className="flex items-center justify-between px-4 py-4 border-b border-gray-100">
        <div className="w-8"></div> {/* Spacer to center title */}
        <h3 className="font-bold text-slate-900">Sélectionnez une adresse</h3>
        <button onClick={onClose} className="p-2 rounded-full hover:bg-gray-100">
          <X size={24} className="text-slate-500" />
        </button>
      </div>

      {/* Search Input */}
      <div className="p-4">
        <div className="flex items-center border border-slate-300 rounded-lg px-3 py-3 focus-within:border-slate-900 focus-within:ring-1 focus-within:ring-slate-900 transition-all">
          <Search size={20} className="text-slate-400 mr-3" />
          <input 
            type="text" 
            autoFocus
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            className="flex-grow outline-none text-slate-900 font-medium"
          />
          {query.length > 0 && <X size={18} className="text-slate-400 cursor-pointer" onClick={() => setQuery('')} />}
        </div>
      </div>

      {/* Predictions List */}
      <div className="flex-grow overflow-y-auto">
        {predictions.map((pred, index) => (
          <button 
            key={index}
            onClick={() => onSelectAddress(pred.main + ", " + pred.secondary)}
            className="w-full flex items-start gap-4 px-6 py-4 border-b border-gray-50 hover:bg-gray-50 text-left transition"
          >
            <div className="mt-1 min-w-[20px]">
               <MapPin size={20} className="text-slate-400 fill-slate-100" />
            </div>
            <div>
              <div className="font-bold text-slate-900 text-sm">{pred.main}</div>
              <div className="text-slate-500 text-xs mt-0.5">{pred.secondary}</div>
            </div>
          </button>
        ))}
      </div>

      {/* Clavier Simulé (Fake Keyboard Placeholder) */}
      <div className="h-[35vh] bg-slate-900 text-white flex flex-col justify-end pb-4">
          <div className="flex justify-center gap-1 mb-2 px-1">
             {[1,2,3,4,5,6,7,8,9,0].map(n => (
               <div key={n} className="bg-slate-600 h-10 w-8 rounded text-center leading-10 font-bold">{n}</div>
             ))}
          </div>
          <div className="flex justify-center gap-1 mb-2 px-4">
             <div className="bg-slate-600 h-10 flex-grow rounded flex items-center justify-center text-sm">espace</div>
             <div className="bg-teal-400 h-10 w-20 rounded flex items-center justify-center text-slate-900 font-bold">Go</div>
          </div>
      </div>
    </div>
  );
};

// --- Composant : Écran Wizard Générique (Barre de progression) ---
const WizardStep = ({ progress, title, children, onBack, onContinue, canContinue = true }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 pb-6 animate-in slide-in-from-right duration-300">
    {/* Navigation & Progress */}
    <div className="flex items-center mb-6">
      <button onClick={onBack} className="p-2 -ml-2 rounded-full hover:bg-gray-100">
        <ChevronLeft size={28} className="text-slate-800" />
      </button>
      <div className="flex-grow ml-4 h-1.5 bg-gray-100 rounded-full overflow-hidden mr-4">
         <div className="h-full bg-slate-900 rounded-full transition-all duration-300" style={{ width: `${progress}%` }}></div>
      </div>
    </div>

    {/* Title */}
    <h2 className="text-2xl font-bold text-slate-900 mb-6 leading-tight">
      {title}
    </h2>

    {/* Dynamic Content */}
    <div className="flex-grow overflow-y-auto no-scrollbar">
      {children}
    </div>

    {/* Footer Button */}
    <div className="mt-4 pt-2">
      <button 
        onClick={onContinue}
        disabled={!canContinue}
        className={`w-full font-bold py-4 rounded-xl transition active:scale-95 ${
          canContinue 
          ? 'bg-[#FF2E63] text-white shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552]' 
          : 'bg-gray-200 text-gray-400 cursor-not-allowed'
        }`}
      >
        Continuer
      </button>
    </div>
  </div>
);

// --- Main Controller ---
const ProfileCreationFlow = () => {
  const [step, setStep] = useState('intro'); // intro, search, address_confirm, job_confirm
  const [address, setAddress] = useState('');
  const [selectedJob, setSelectedJob] = useState('');

  // Navigation Logic
  const goBack = () => {
    if (step === 'address_confirm') setStep('intro'); // Or search? usually back goes to previous logical step
    if (step === 'job_confirm') setStep('address_confirm');
  };

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      {/* Mobile Frame */}
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900">
        
        {/* Status Bar */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900 bg-white z-10">
          <span>{step === 'search' ? '06:54' : '06:53'}</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className="w-6 h-3 bg-gray-800 rounded-sm"></div>
          </div>
        </div>

        {/* Content Area */}
        <div className="h-[calc(100%-3rem)] relative">
          
          {/* 1. Intro Screen */}
          {step === 'intro' && (
            <IntroScreen onStart={() => setStep('search')} />
          )}

          {/* 2. Address Search (Overlay) */}
          {step === 'search' && (
            <AddressSearchScreen 
              onClose={() => setStep('intro')}
              onSelectAddress={(addr) => {
                setAddress(addr);
                setStep('address_confirm');
              }}
            />
          )}

          {/* 3. Address Confirmation */}
          {step === 'address_confirm' && (
            <WizardStep 
              progress={25}
              title="Quelle est l'adresse de votre domicile ?"
              onBack={() => setStep('intro')}
              onContinue={() => setStep('job_confirm')}
              canContinue={address.length > 0}
            >
              <div 
                className="w-full border border-gray-200 bg-gray-50 rounded-lg px-4 py-4 text-slate-800 font-medium cursor-text"
                onClick={() => setStep('search')} // Allow reopening search
              >
                {address || "Sélectionnez votre adresse..."}
              </div>
            </WizardStep>
          )}

          {/* 4. Job Selection (Recap context) */}
          {step === 'job_confirm' && (
             <WizardStep
                progress={50}
                title="Pour quel métier souhaitez-vous recevoir des missions ?"
                onBack={goBack}
                onContinue={() => alert("Fin de la séquence démo")}
                canContinue={!!selectedJob}
             >
                <div className="flex flex-col gap-3">
                  {["Aide-soignant(e)", "Éducateur spécialisé", "Infirmier(ère)", "Moniteur(rice) éducateur(e)", "Psychomotricien(ne)"].map(job => (
                    <button
                      key={job}
                      onClick={() => setSelectedJob(job)}
                      className={`
                        text-left px-6 py-4 rounded-full border font-medium transition-all
                        ${selectedJob === job 
                          ? 'bg-slate-900 text-white border-slate-900' 
                          : 'bg-white text-slate-700 border-slate-200 hover:border-slate-400'}
                      `}
                    >
                      {job}
                    </button>
                  ))}

                  <div className="mt-8 mb-4 flex items-center justify-between text-slate-900 font-bold text-sm">
                    <span>Métiers indisponibles pour le moment</span>
                    <Info size={16} className="text-slate-400" />
                  </div>

                  {["AES", "Auxiliaire de puériculture", "Auxiliaire de vie", "Auxiliaire de vie diplomé"].map(job => (
                    <div key={job} className="px-6 py-4 rounded-full bg-slate-100 text-slate-400 font-medium text-sm border border-transparent">
                      {job}
                    </div>
                  ))}
                </div>
             </WizardStep>
          )}

        </div>

        {/* Home Indicator */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-20"></div>

      </div>
    </div>
  );
};

export default ProfileCreationFlow;

Ce que j'ai soigné particulièrement :

  1. L'Écran de Recherche (Clavier simulé) :
    • J'ai ajouté un conteneur en bas de l'écran 2 (bg-slate-900) qui imite visuellement un clavier sombre iOS/Android. Cela permet de coller à votre capture d'écran où le clavier occupe 30% de l'écran.
    • L'input a l'attribut autoFocus pour que l'utilisateur soit directement en mode saisie.
  2. Navigation Fluide :
    • Écran 1 -> Écran 2 : Clic sur "Démarrer" -> Ouvre la recherche.
    • Écran 2 -> Écran 3 : Clic sur une adresse -> Ferme la recherche et affiche l'écran de confirmation avec l'adresse remplie.
    • Réédition : Si vous êtes sur l'écran 3 et cliquez sur l'input d'adresse, cela rouvre la recherche (UX classique).
  3. Détails Visuels :
    • Liste Intro : J'ai utilisé une bordure gauche (border-l border-gray-100) pour aligner visuellement les étapes inactives sous la première étape active.
    • Icônes Maps : Utilisation de l'icône MapPin grise pour chaque résultat de recherche, comme sur Google Maps.

C'est une séquence solide pour la création de profil !

JSON RPC ODOO version GEMINI Categorie Categorie

2-planche 2

C'est une séquence cruciale pour l'engagement utilisateur : la demande de permissions.

L'approche ici est excellente (c'est un "best practice" UX) : on utilise un écran de pré-autorisation (le fond pêche) pour expliquer la valeur ("Ne passez pas à côté de votre prochaine mission") avant de déclencher la pop-up système bloquante.

Voici l'analyse pour le code :

  1. Écran Langues : Un switch "Oui/Non" qui agit comme un accordéon. Si "Oui", la liste des tags apparaît.
  2. Écran Notification (Marketing) : Un fond spécifique (bg-[#FFE5D9]), des cartes simulant des notifications iOS réalistes.
  3. La Modale Système : Une simulation de l'alerte native iOS/Android qui se superpose à l'écran précédent.

Voici le code complet pour cette séquence.

Le Code (PermissionsFlow.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import { ChevronLeft, Plus, Check, Bell, ChevronDown } from 'lucide-react';

// --- Composant : Carte de Notification Simulée ---
const FakeNotification = ({ title, body, time, price, distance }) => (
  <div className="bg-white rounded-2xl p-4 shadow-sm mb-3 flex items-start gap-3 relative">
    {/* Logo App (B = Synergia) */}
    <div className="w-10 h-10 rounded-full bg-[#FF2E63] flex items-center justify-center flex-shrink-0 text-white font-bold text-xl">
      S
    </div>
    
    <div className="flex-grow min-w-0">
      <div className="flex justify-between items-baseline mb-0.5">
        <h4 className="font-bold text-slate-900 text-sm">Nouvelle mission disponible</h4>
        <span className="text-slate-400 text-xs">{time}</span>
      </div>
      <p className="text-slate-600 text-sm leading-snug">
        <span className="font-semibold text-slate-800">{title}</span> cherche une Infirmière pour <span className="font-bold text-slate-900">{price}</span> {distance && `(${distance})`}.
      </p>
    </div>

    {/* Chevron expand simulated */}
    <div className="absolute top-4 right-4 bg-gray-100 rounded-full p-1">
        <ChevronDown size={14} className="text-gray-500" />
    </div>
  </div>
);

// --- Écran 1 & 2 : Langues ---
const LanguageScreen = ({ onContinue }) => {
  const [hasOtherLanguages, setHasOtherLanguages] = useState(null); // null, true, false
  const [selectedLanguages, setSelectedLanguages] = useState([]);

  const languagesList = [
    "Português", "Nederlands", "English", "Deutsch", "Italiano", "Español", "Autre"
  ];

  const toggleLanguage = (lang) => {
    if (selectedLanguages.includes(lang)) {
      setSelectedLanguages(prev => prev.filter(l => l !== lang));
    } else {
      setSelectedLanguages(prev => [...prev, lang]);
    }
  };

  return (
    <div className="flex flex-col h-full bg-white px-6 pt-6 animate-in slide-in-from-right duration-300">
      {/* Header */}
      <div className="flex items-center mb-6">
        <button className="p-2 -ml-2 rounded-full hover:bg-gray-100">
          <ChevronLeft size={28} className="text-slate-800" />
        </button>
        <div className="flex-grow ml-4 h-1.5 bg-gray-100 rounded-full overflow-hidden mr-4">
           <div className="h-full bg-slate-900 rounded-full w-3/4"></div>
        </div>
      </div>

      <h2 className="text-2xl font-bold text-slate-900 mb-8 leading-tight">
        Maîtrisez-vous d'autres langues que le français ?
      </h2>

      {/* Yes / No Toggles */}
      <div className="flex gap-4 mb-8">
        {/* OUI */}
        <button 
          onClick={() => setHasOtherLanguages(true)}
          className={`flex-1 flex items-center gap-3 p-4 rounded-xl border transition-all ${
            hasOtherLanguages === true 
            ? 'border-black ring-1 ring-black bg-white' 
            : 'border-slate-200 bg-white hover:border-slate-300'
          }`}
        >
          <div className={`w-5 h-5 rounded-full border flex items-center justify-center ${
            hasOtherLanguages === true ? 'border-black' : 'border-slate-300'
          }`}>
             {hasOtherLanguages === true && <div className="w-3 h-3 bg-black rounded-full"></div>}
          </div>
          <span className="font-medium text-slate-900">Oui</span>
        </button>

        {/* NON */}
        <button 
          onClick={() => {
            setHasOtherLanguages(false);
            onContinue(); // Auto continue if No? Or wait for button. Usually wait.
          }}
          className={`flex-1 flex items-center gap-3 p-4 rounded-xl border transition-all ${
            hasOtherLanguages === false 
            ? 'border-black ring-1 ring-black bg-white' 
            : 'border-slate-200 bg-white hover:border-slate-300'
          }`}
        >
           <div className={`w-5 h-5 rounded-full border flex items-center justify-center ${
            hasOtherLanguages === false ? 'border-black' : 'border-slate-300'
          }`}>
             {hasOtherLanguages === false && <div className="w-3 h-3 bg-black rounded-full"></div>}
          </div>
          <span className="font-medium text-slate-900">Non</span>
        </button>
      </div>

      {/* Language List (Conditionnel) */}
      {hasOtherLanguages === true && (
        <div className="flex flex-col gap-3 animate-in fade-in slide-in-from-bottom-2 duration-300 mb-20 overflow-y-auto">
          {languagesList.map(lang => {
            const isSelected = selectedLanguages.includes(lang);
            return (
              <button
                key={lang}
                onClick={() => toggleLanguage(lang)}
                className={`
                  flex items-center gap-3 px-4 py-3 rounded-full border transition-all w-fit
                  ${isSelected 
                    ? 'bg-slate-900 text-white border-slate-900' 
                    : 'bg-white text-slate-600 border-slate-200 hover:border-slate-300'}
                `}
              >
                 {isSelected 
                   ? <Check size={18} /> 
                   : <div className="w-4 h-4 rounded-full border border-slate-400 flex items-center justify-center text-[10px] leading-none">+</div>
                 }
                 <span className="font-medium">{lang}</span>
              </button>
            )
          })}
        </div>
      )}

      {/* Footer Button (Only if selection made) */}
      {hasOtherLanguages !== null && (
        <div className="mt-auto pb-6">
           <button 
             onClick={onContinue}
             className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
           >
             Continuer
           </button>
        </div>
      )}
    </div>
  );
};

// --- Écran 3 & 4 : Notifications ---
const NotificationScreen = ({ onShowModal, showModal }) => {
  return (
    <div className="flex flex-col h-full bg-[#FFE5D9] px-6 pt-6 relative overflow-hidden animate-in fade-in duration-500">
      {/* Header */}
      <div className="flex items-center mb-8">
        <button className="p-2 -ml-2 rounded-full hover:bg-black/5">
          <ChevronLeft size={28} className="text-slate-800" />
        </button>
      </div>

      <div className="flex-grow">
        <h2 className="text-2xl font-bold text-slate-900 mb-8 text-center px-4">
          Ne passez pas à côté de votre prochaine mission !
        </h2>

        {/* Fake Notifications Stack */}
        <div className="space-y-4">
          <FakeNotification 
            title="Croix-Rouge Française"
            time="1min"
            price="533 €"
            distance="à 0,8km de chez vous"
          />
           <FakeNotification 
            title="EHPAD Louise Michel"
            time="1min"
            price="502 €"
            distance="à 0,3km de chez vous"
          />
        </div>
      </div>

      {/* Footer Actions */}
      <div className="mt-auto pb-8 space-y-4">
        <button 
          onClick={onShowModal}
          className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-lg hover:bg-[#e62552] transition active:scale-95"
        >
          Activer les notifications
        </button>
        
        <button className="w-full text-slate-900 font-bold underline decoration-2 decoration-slate-900/20 hover:decoration-slate-900 transition">
          Peut-être plus tard
        </button>
      </div>

      {/* --- Écran 4 : Overlay System Modal --- */}
      {showModal && (
        <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-[2px] animate-in fade-in duration-200 px-8">
          
          {/* iOS Style Alert */}
          <div className="bg-white/95 backdrop-blur-xl rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
             
             {/* Content */}
             <div className="p-6 flex flex-col items-center text-center">
                <div className="mb-4">
                   <Bell size={32} className="text-slate-900 fill-slate-900" />
                </div>
                <h3 className="text-lg font-semibold text-slate-900 mb-2 leading-tight">
                  Autoriser "Synergia" à vous envoyer des notifications ?
                </h3>
                {/* Simulated native text often doesn't have body, but keeping simple */}
             </div>

             {/* Actions (Vertical Stack style iOS newer) */}
             <div className="flex flex-col border-t border-gray-200/50">
               <button 
                  onClick={() => alert("Permissions accordées !")}
                  className="py-3.5 bg-gray-600 text-white font-semibold text-[17px] active:bg-gray-700 m-4 mb-2 rounded-full shadow-sm"
               >
                 Autoriser
               </button>
               <button 
                  onClick={() => alert("Permissions refusées")}
                  className="py-3.5 bg-gray-600 text-white font-semibold text-[17px] active:bg-gray-700 m-4 mt-0 rounded-full shadow-sm"
               >
                 Ne pas autoriser
               </button>
             </div>
          </div>

        </div>
      )}
    </div>
  );
};

// --- Main App Controller ---
const App = () => {
  const [step, setStep] = useState('languages'); // 'languages', 'notifications'
  const [showPermissionModal, setShowPermissionModal] = useState(false);

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      {/* Mobile Frame */}
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900">
        
        {/* Status Bar */}
        <div className={`h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold z-10 ${step === 'notifications' ? 'bg-[#FFE5D9] text-slate-900' : 'bg-white text-slate-900'}`}>
          <span>06:55</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className="w-6 h-3 bg-slate-900 rounded-sm"></div>
          </div>
        </div>

        {/* Content */}
        <div className="h-[calc(100%-3rem)] relative">
          
          {step === 'languages' && (
            <LanguageScreen onContinue={() => setStep('notifications')} />
          )}

          {step === 'notifications' && (
            <NotificationScreen 
              showModal={showPermissionModal}
              onShowModal={() => setShowPermissionModal(true)} 
            />
          )}

        </div>
        
        {/* Home Indicator */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-20"></div>

      </div>
    </div>
  );
};

export default App;

Détails de l'intégration :

  1. La couleur "Pêche" : J'ai utilisé #FFE5D9 pour le fond de l'écran notification. C'est chaleureux et correspond aux screenshots, contrastant bien avec les notifications blanches.
  2. Radio Buttons Customisés (Oui/Non) :
    • Au lieu de boutons HTML standard, j'ai créé des div stylisées.
    • L'état "Sélectionné" ajoute une bordure noire épaisse (border-black ring-1) et remplit le petit cercle intérieur, imitant parfaitement le design de l'écran 2.
  3. Tags de Langues :
    • J'ai utilisé un petit cercle avec un + à l'intérieur (rounded-full border).
    • Au clic, le tag devient noir (bg-slate-900) et le + se transforme en coche (Check icon de Lucide), offrant un feedback visuel immédiat.
  4. Simulation Notification Système :
    • Pour la modale finale (Écran 4), j'ai utilisé backdrop-blur sur l'arrière-plan pour flouter l'écran "pêche" en dessous.
    • Les boutons de la modale sont gris foncé (bg-gray-600), ce qui est un choix stylistique spécifique à votre maquette (iOS utilise habituellement du bleu ou du rouge, mais ici c'est du custom dark).

Ceci termine la séquence d'onboarding ! C'était un plaisir de reproduire ces écrans. L'application a maintenant un flux complet d'inscription.

JSON RPC ODOO version GEMINI Categorie Categorie

3- Planche3

C'est une étape complexe qui demande de gérer plusieurs Modales (fenêtres superposées) avec des styles très différents :

  1. Date Picker (Style Android Material) : En-tête vert/teal, grille calendaire, boutons d'action.
  2. Recherche Ville : Avec clavier simulé et liste de résultats (type "Bagnol").
  3. Sélecteur de Type : Une liste exhaustive en plein écran.

Voici le code complet intégrant ces interactions. J'ai créé un composant DatePicker flexible qui peut s'adapter pour afficher Juin ou Novembre selon votre sélection.

Le Code (ExperienceDetails.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import { ChevronLeft, Calendar, HelpCircle, X, Search, MapPin, ChevronRight } from 'lucide-react';

// --- Composant 1 : Date Picker (Style Material Design) ---
const DatePickerModal = ({ isOpen, onClose, onConfirm, initialDate, title }) => {
  if (!isOpen) return null;

  // Configuration statique pour simuler les deux écrans (Juin et Nov)
  const config = initialDate === 'start' 
    ? { 
        month: 'Juin 2025', 
        dayName: 'Mar.', 
        dayNum: '10', 
        startDayOffset: 1, // Le mois commence un Dimanche (index 0) ou Lundi ? Simulation visuelle
        daysInMonth: 30,
        selected: 10 
      }
    : { 
        month: 'Novembre 2025', 
        dayName: 'Ven.', 
        dayNum: '28', 
        startDayOffset: 6, 
        daysInMonth: 30, 
        selected: 28 
      };

  const days = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
  const grid = Array.from({ length: config.startDayOffset + config.daysInMonth });

  return (
    <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
      <div className="bg-white rounded-lg shadow-2xl w-[85%] max-w-sm overflow-hidden">
        
        {/* Header Teal */}
        <div className="bg-[#0F766E] p-6 text-white">
          <div className="text-sm font-medium opacity-70 mb-1">2025</div>
          <div className="text-3xl font-bold">
            {config.dayName} {config.dayNum} {config.month.split(' ')[0].toLowerCase()}.
          </div>
        </div>

        {/* Body Calendar */}
        <div className="p-4">
          <div className="flex justify-between items-center mb-4 px-2">
            <ChevronLeft size={20} className="text-gray-500" />
            <span className="font-semibold text-sm text-gray-700">{config.month}</span>
            <ChevronRight size={20} className="text-gray-500" />
          </div>

          {/* Days Header */}
          <div className="grid grid-cols-7 mb-2">
            {days.map(d => (
              <div key={d} className="text-center text-xs text-gray-400 font-medium">{d}</div>
            ))}
          </div>

          {/* Days Grid */}
          <div className="grid grid-cols-7 gap-y-2">
            {grid.map((_, i) => {
              const dayNum = i - config.startDayOffset + 1;
              if (dayNum <= 0) return <div key={i}></div>;
              
              const isSelected = dayNum === config.selected;
              return (
                <div key={i} className="flex justify-center">
                  <button 
                    className={`w-8 h-8 rounded-full text-xs font-medium flex items-center justify-center transition-colors
                      ${isSelected ? 'bg-[#0F766E] text-white' : 'text-gray-700 hover:bg-gray-100'}
                    `}
                  >
                    {dayNum}
                  </button>
                </div>
              );
            })}
          </div>
        </div>

        {/* Footer Actions */}
        <div className="flex justify-end gap-6 p-4 pt-2 text-[#0F766E] font-bold text-sm">
          <button onClick={onClose}>ANNULER</button>
          <button onClick={() => onConfirm(`0${config.dayNum}/05/2025`)}>CONFIRMER</button>
        </div>
      </div>
    </div>
  );
};

// --- Composant 2 : Recherche Ville (Avec Clavier) ---
const CitySearchModal = ({ onClose, onSelect }) => {
  const [query, setQuery] = useState('Bagnol');
  
  const results = [
    { name: "Bagnolet", dep: "France" },
    { name: "Bagnoles-de-l'Orne", dep: "France" },
    { name: "Bagnols-sur-Cèze", dep: "France" },
    { name: "Bagnols", dep: "France" },
    { name: "Bagnols-en-Forêt", dep: "France" },
  ];

  return (
    <div className="absolute inset-0 bg-white z-50 flex flex-col animate-in slide-in-from-bottom duration-300">
      <div className="p-4 border-b border-gray-100">
        <h3 className="font-bold text-center mb-4 text-slate-900">Sélectionnez une adresse</h3>
        <div className="flex items-center border border-slate-300 rounded-lg px-3 py-2">
          <Search size={20} className="text-slate-400 mr-2" />
          <input 
            value={query} 
            onChange={e => setQuery(e.target.value)}
            className="flex-grow outline-none text-slate-900" 
            autoFocus 
          />
          <X size={20} className="text-slate-400 cursor-pointer" onClick={onClose} />
        </div>
      </div>

      <div className="flex-grow overflow-y-auto">
        {results.map((r, i) => (
          <div key={i} onClick={() => onSelect(r.name)} className="flex items-start gap-3 px-6 py-4 border-b border-gray-50 active:bg-gray-50 cursor-pointer">
            <MapPin size={18} className="text-slate-400 mt-1" />
            <div>
              <div className="font-bold text-slate-800">{r.name}</div>
              <div className="text-xs text-slate-500">{r.dep}</div>
            </div>
          </div>
        ))}
      </div>

      {/* Clavier simulé (Sombre) */}
      <div className="bg-[#1F2937] p-2 pb-6">
         {/* Barre de suggestion */}
         <div className="flex gap-2 mb-2 overflow-x-auto no-scrollbar py-2">
            {["Bagnol", "Bagnols", "Bagnolet"].map(s => (
                <span key={s} className="px-4 py-2 bg-slate-700 text-white rounded text-sm whitespace-nowrap">{s}</span>
            ))}
             <div className="ml-auto bg-slate-600 p-2 rounded-full"><div className="w-4 h-4 bg-white rounded-full opacity-50"></div></div>
         </div>
         {/* Touches (Visuel simplifié) */}
         <div className="grid grid-cols-10 gap-1 mb-2 px-1">
            {['a','z','e','r','t','y','u','i','o','p'].map(k => <div key={k} className="bg-slate-600 h-10 rounded text-white flex items-center justify-center font-bold text-lg">{k}</div>)}
         </div>
         <div className="grid grid-cols-10 gap-1 mb-2 px-4">
            {['q','s','d','f','g','h','j','k','l','m'].map(k => <div key={k} className="bg-slate-600 h-10 rounded text-white flex items-center justify-center font-bold text-lg">{k}</div>)}
         </div>
         <div className="flex gap-2 px-1">
             <div className="bg-slate-700 w-12 rounded flex items-center justify-center text-white">⇧</div>
             <div className="flex-grow flex gap-1 justify-center">
                 {['w','x','c','v','b','n'].map(k => <div key={k} className="bg-slate-600 h-10 w-8 rounded text-white flex items-center justify-center font-bold text-lg">{k}</div>)}
             </div>
             <div className="bg-slate-700 w-12 rounded flex items-center justify-center text-white">⌫</div>
         </div>
         <div className="flex gap-2 px-1 mt-2">
            <div className="bg-slate-700 w-16 h-10 rounded text-white flex items-center justify-center text-sm">?123</div>
            <div className="bg-slate-600 flex-grow h-10 rounded"></div>
            <div className="bg-teal-400 w-16 h-10 rounded flex items-center justify-center text-black font-bold">✓</div>
         </div>
      </div>
    </div>
  );
};

// --- Composant 3 : Selecteur de Type ---
const EstablishmentTypeModal = ({ onClose, onSelect }) => {
  const types = [
    "Aide à domicile",
    "Centres d'accueil et d'hébergement pour adultes et familles en difficulté",
    "Centres d'accueil et d'hébergement pour demandeurs d'asile",
    "Centres d'accueil et d'hébergement pour enfants en difficulté",
    "Centres de prévention en addictologie",
    "Centres éducatifs",
    "Établissements pour adultes en situation de handicap",
    "Établissements pour enfants et adolescents en situation de handicap",
    "Établissements pour personnes âgées",
    "Établissements sociaux avec prise en charge sanitaire",
    "Hôpitaux et Cliniques (incl. HAD)",
    "Soins à domicile",
    "Structures pour petite enfance (crèches)"
  ];

  return (
    <div className="absolute inset-0 bg-white z-50 flex flex-col animate-in slide-in-from-bottom duration-300">
      <div className="p-6 border-b border-gray-50 flex justify-between items-center">
        <h3 className="font-bold text-lg text-slate-900">Sélectionnez un type d'établissement</h3>
        <button onClick={onClose}><X size={24} className="text-slate-400" /></button>
      </div>
      <div className="flex-grow overflow-y-auto px-6 py-2 pb-12">
        {types.map((type, i) => (
          <div 
            key={i} 
            onClick={() => onSelect(type)}
            className="py-4 border-b border-gray-50 text-slate-700 text-sm font-medium hover:text-[#FF2E63] cursor-pointer"
          >
            {type}
          </div>
        ))}
      </div>
    </div>
  );
};

// --- Composant Principal : Formulaire ---
const App = () => {
  // Gestion des états pour les modales
  const [activeModal, setActiveModal] = useState(null); // 'startDate', 'endDate', 'city', 'type', null
  
  // Données du formulaire
  const [formData, setFormData] = useState({
    startDate: '',
    endDate: '',
    city: '',
    type: ''
  });

  const updateField = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    setActiveModal(null);
  };

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900 flex flex-col">
        
        {/* Status Bar */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900 bg-white z-10 flex-shrink-0">
          <span>06:57</span>
          <div className="flex gap-1 items-center"><span>5G</span><div className="w-6 h-3 bg-gray-800 rounded-sm"></div></div>
        </div>

        {/* --- Header Formulaire --- */}
        <div className="flex items-center justify-between px-6 py-4 border-b border-gray-50">
           <button className="p-2 -ml-2 rounded-full hover:bg-gray-100"><ChevronLeft size={28} className="text-slate-800" /></button>
           <span className="font-bold text-slate-900">Ajouter une nouvelle expérience</span>
           <HelpCircle size={24} className="text-slate-400" />
        </div>

        {/* --- Corps du Formulaire --- */}
        <div className="p-6 space-y-6 overflow-y-auto pb-24">
            
            {/* Date Inputs */}
            <div>
              <label className="block text-sm font-bold text-slate-900 mb-2">Date</label>
              <div className="flex gap-4">
                <div className="flex-1 cursor-pointer" onClick={() => setActiveModal('startDate')}>
                  <div className={`w-full border rounded-lg px-4 py-3 text-sm flex items-center justify-between ${formData.startDate ? 'border-slate-800 text-slate-900' : 'border-gray-200 text-gray-400'}`}>
                     <span>{formData.startDate || "MM/AAAA"}</span>
                     <Calendar size={16} />
                  </div>
                  <span className="text-xs text-slate-400 mt-1 block">Début</span>
                </div>
                <div className="flex-1 cursor-pointer" onClick={() => setActiveModal('endDate')}>
                   <div className={`w-full border rounded-lg px-4 py-3 text-sm flex items-center justify-between ${formData.endDate ? 'border-slate-800 text-slate-900' : 'border-gray-200 text-gray-400'}`}>
                     <span>{formData.endDate || "MM/AAAA"}</span>
                     <Calendar size={16} />
                   </div>
                   <span className="text-xs text-slate-400 mt-1 block">Fin</span>
                </div>
              </div>
            </div>

            {/* Checkbox */}
            <div className="flex items-center gap-3">
              <div className="w-5 h-5 border border-gray-300 rounded"></div>
              <span className="text-slate-700 text-sm">Je suis toujours en poste</span>
            </div>

            {/* Intitulé (Static for demo) */}
            <div>
              <label className="block text-sm font-bold text-slate-900 mb-2">Intitulé du poste</label>
              <div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-400">Sélectionner...</div>
            </div>

            {/* Etablissement (Static for demo) */}
            <div>
               <label className="block text-sm font-bold text-slate-900 mb-2">Nom de l'établissement</label>
               <input className="w-full border border-gray-200 rounded-lg px-4 py-3 outline-none" />
            </div>

            {/* Ville (Trigger Modal) */}
            <div onClick={() => setActiveModal('city')}>
               <label className="block text-sm font-bold text-slate-900 mb-2">Ville</label>
               <div className={`w-full border rounded-lg px-4 py-3 text-sm ${formData.city ? 'text-slate-900' : 'text-gray-400'}`}>
                   {formData.city || "Saisir une ville..."}
               </div>
            </div>

            {/* Type Etablissement (Trigger Modal) */}
            <div onClick={() => setActiveModal('type')}>
               <label className="block text-sm font-bold text-slate-900 mb-2">Type d'établissement</label>
               <div className="relative">
                   <div className={`w-full border rounded-lg px-4 py-3 text-sm pr-8 truncate ${formData.type ? 'text-slate-900' : 'text-gray-400'}`}>
                       {formData.type || "Sélectionner..."}
                   </div>
                   <ChevronRight size={18} className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 rotate-90" />
               </div>
            </div>

             {/* Description */}
            <div>
               <label className="block text-sm font-bold text-slate-900 mb-2">Description</label>
               <textarea rows={3} placeholder="Que souhaitez-vous mettre en avant..." className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm resize-none"></textarea>
            </div>
             {/* Certificat */}
            <div>
               <label className="block text-sm font-bold text-slate-900 mb-2">Certificat de travail</label>
               <div className="border border-dashed border-gray-300 rounded-lg h-12"></div>
            </div>
        </div>

        {/* Fixed Button */}
        <div className="absolute bottom-0 w-full bg-white border-t border-gray-100 p-4 pb-8 z-10">
            <button className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-lg">Enregistrer l'expérience</button>
        </div>

        {/* --- MODALES --- */}

        {/* Date Picker Start (Juin style) */}
        <DatePickerModal 
            isOpen={activeModal === 'startDate'} 
            onClose={() => setActiveModal(null)} 
            onConfirm={(date) => updateField('startDate', date)}
            initialDate="start"
        />

        {/* Date Picker End (Novembre style) */}
        <DatePickerModal 
            isOpen={activeModal === 'endDate'} 
            onClose={() => setActiveModal(null)} 
            onConfirm={(date) => updateField('endDate', date)}
            initialDate="end"
        />

        {/* City Search */}
        {activeModal === 'city' && (
            <CitySearchModal 
                onClose={() => setActiveModal(null)}
                onSelect={(city) => updateField('city', city)}
            />
        )}

        {/* Type Selector */}
        {activeModal === 'type' && (
            <EstablishmentTypeModal 
                onClose={() => setActiveModal(null)}
                onSelect={(type) => updateField('type', type)}
            />
        )}

        {/* Home Indicator */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-20"></div>

      </div>
    </div>
  );
};

export default App;

Analyse des Fonctionnalités Reproduites

  1. Date Picker (Android Style) :
    • Le composant DatePickerModal reproduit fidèlement le design Android Material.
    • En-tête Teal (bg-[#0F766E]) : Affiche l'année en petit et la date en gros (ex: "Mar. 10 juin").
    • Logique Visuelle : J'ai hardcodé la configuration pour Juin (commençant un Lundi) et Novembre (commençant un Samedi) pour matcher exactement les captures d'écran 1 et 2.
    • Selection : Le cercle vert autour du jour sélectionné (10 ou 28).
  2. Recherche Ville (avec Clavier) :
    • La modale s'ouvre avec une animation slide-in-bottom.
    • Le Clavier est visuellement simulé en bas avec un style sombre (bg-[#1F2937]), incluant la barre de suggestions (Bagnol, Bagnols...). C'est du pur CSS/HTML pour le rendu visuel.
  3. Sélecteur de Type :
    • Une liste longue, scrollable, avec le bouton de fermeture X en haut à droite.
    • Au clic sur un élément, la modale se ferme et le champ du formulaire est rempli.
  4. Intégration Globale :
    • Le formulaire principal réagit : quand vous sélectionnez une date ou une ville dans les modales, le champ correspondant dans le formulaire se met à jour visuellement (le texte passe de gris à noir).

Vous avez maintenant un flux de saisie d'expérience complet et interactif !

JSON RPC ODOO version GEMINI Categorie Categorie
4- Planche_4


C'est une étape complexe qui demande de gérer plusieurs Modales (fenêtres superposées) avec des styles très différents :

  1. Date Picker (Style Android Material) : En-tête vert/teal, grille calendaire, boutons d'action.
  2. Recherche Ville : Avec clavier simulé et liste de résultats (type "Bagnol").
  3. Sélecteur de Type : Une liste exhaustive en plein écran.

Voici le code complet intégrant ces interactions. J'ai créé un composant DatePicker flexible qui peut s'adapter pour afficher Juin ou Novembre selon votre sélection.

Le Code (ExperienceDetails.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import { ChevronLeft, Calendar, HelpCircle, X, Search, MapPin, ChevronRight } from 'lucide-react';

// --- Composant 1 : Date Picker (Style Material Design) ---
const DatePickerModal = ({ isOpen, onClose, onConfirm, initialDate, title }) => {
  if (!isOpen) return null;

  // Configuration statique pour simuler les deux écrans (Juin et Nov)
  const config = initialDate === 'start' 
    ? { 
        month: 'Juin 2025', 
        dayName: 'Mar.', 
        dayNum: '10', 
        startDayOffset: 1, // Le mois commence un Dimanche (index 0) ou Lundi ? Simulation visuelle
        daysInMonth: 30,
        selected: 10 
      }
    : { 
        month: 'Novembre 2025', 
        dayName: 'Ven.', 
        dayNum: '28', 
        startDayOffset: 6, 
        daysInMonth: 30, 
        selected: 28 
      };

  const days = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
  const grid = Array.from({ length: config.startDayOffset + config.daysInMonth });

  return (
    <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
      <div className="bg-white rounded-lg shadow-2xl w-[85%] max-w-sm overflow-hidden">
        
        {/* Header Teal */}
        <div className="bg-[#0F766E] p-6 text-white">
          <div className="text-sm font-medium opacity-70 mb-1">2025</div>
          <div className="text-3xl font-bold">
            {config.dayName} {config.dayNum} {config.month.split(' ')[0].toLowerCase()}.
          </div>
        </div>

        {/* Body Calendar */}
        <div className="p-4">
          <div className="flex justify-between items-center mb-4 px-2">
            <ChevronLeft size={20} className="text-gray-500" />
            <span className="font-semibold text-sm text-gray-700">{config.month}</span>
            <ChevronRight size={20} className="text-gray-500" />
          </div>

          {/* Days Header */}
          <div className="grid grid-cols-7 mb-2">
            {days.map(d => (
              <div key={d} className="text-center text-xs text-gray-400 font-medium">{d}</div>
            ))}
          </div>

          {/* Days Grid */}
          <div className="grid grid-cols-7 gap-y-2">
            {grid.map((_, i) => {
              const dayNum = i - config.startDayOffset + 1;
              if (dayNum <= 0) return <div key={i}></div>;
              
              const isSelected = dayNum === config.selected;
              return (
                <div key={i} className="flex justify-center">
                  <button 
                    className={`w-8 h-8 rounded-full text-xs font-medium flex items-center justify-center transition-colors
                      ${isSelected ? 'bg-[#0F766E] text-white' : 'text-gray-700 hover:bg-gray-100'}
                    `}
                  >
                    {dayNum}
                  </button>
                </div>
              );
            })}
          </div>
        </div>

        {/* Footer Actions */}
        <div className="flex justify-end gap-6 p-4 pt-2 text-[#0F766E] font-bold text-sm">
          <button onClick={onClose}>ANNULER</button>
          <button onClick={() => onConfirm(`0${config.dayNum}/05/2025`)}>CONFIRMER</button>
        </div>
      </div>
    </div>
  );
};

// --- Composant 2 : Recherche Ville (Avec Clavier) ---
const CitySearchModal = ({ onClose, onSelect }) => {
  const [query, setQuery] = useState('Bagnol');
  
  const results = [
    { name: "Bagnolet", dep: "France" },
    { name: "Bagnoles-de-l'Orne", dep: "France" },
    { name: "Bagnols-sur-Cèze", dep: "France" },
    { name: "Bagnols", dep: "France" },
    { name: "Bagnols-en-Forêt", dep: "France" },
  ];

  return (
    <div className="absolute inset-0 bg-white z-50 flex flex-col animate-in slide-in-from-bottom duration-300">
      <div className="p-4 border-b border-gray-100">
        <h3 className="font-bold text-center mb-4 text-slate-900">Sélectionnez une adresse</h3>
        <div className="flex items-center border border-slate-300 rounded-lg px-3 py-2">
          <Search size={20} className="text-slate-400 mr-2" />
          <input 
            value={query} 
            onChange={e => setQuery(e.target.value)}
            className="flex-grow outline-none text-slate-900" 
            autoFocus 
          />
          <X size={20} className="text-slate-400 cursor-pointer" onClick={onClose} />
        </div>
      </div>

      <div className="flex-grow overflow-y-auto">
        {results.map((r, i) => (
          <div key={i} onClick={() => onSelect(r.name)} className="flex items-start gap-3 px-6 py-4 border-b border-gray-50 active:bg-gray-50 cursor-pointer">
            <MapPin size={18} className="text-slate-400 mt-1" />
            <div>
              <div className="font-bold text-slate-800">{r.name}</div>
              <div className="text-xs text-slate-500">{r.dep}</div>
            </div>
          </div>
        ))}
      </div>

      {/* Clavier simulé (Sombre) */}
      <div className="bg-[#1F2937] p-2 pb-6">
         {/* Barre de suggestion */}
         <div className="flex gap-2 mb-2 overflow-x-auto no-scrollbar py-2">
            {["Bagnol", "Bagnols", "Bagnolet"].map(s => (
                <span key={s} className="px-4 py-2 bg-slate-700 text-white rounded text-sm whitespace-nowrap">{s}</span>
            ))}
             <div className="ml-auto bg-slate-600 p-2 rounded-full"><div className="w-4 h-4 bg-white rounded-full opacity-50"></div></div>
         </div>
         {/* Touches (Visuel simplifié) */}
         <div className="grid grid-cols-10 gap-1 mb-2 px-1">
            {['a','z','e','r','t','y','u','i','o','p'].map(k => <div key={k} className="bg-slate-600 h-10 rounded text-white flex items-center justify-center font-bold text-lg">{k}</div>)}
         </div>
         <div className="grid grid-cols-10 gap-1 mb-2 px-4">
            {['q','s','d','f','g','h','j','k','l','m'].map(k => <div key={k} className="bg-slate-600 h-10 rounded text-white flex items-center justify-center font-bold text-lg">{k}</div>)}
         </div>
         <div className="flex gap-2 px-1">
             <div className="bg-slate-700 w-12 rounded flex items-center justify-center text-white">⇧</div>
             <div className="flex-grow flex gap-1 justify-center">
                 {['w','x','c','v','b','n'].map(k => <div key={k} className="bg-slate-600 h-10 w-8 rounded text-white flex items-center justify-center font-bold text-lg">{k}</div>)}
             </div>
             <div className="bg-slate-700 w-12 rounded flex items-center justify-center text-white">⌫</div>
         </div>
         <div className="flex gap-2 px-1 mt-2">
            <div className="bg-slate-700 w-16 h-10 rounded text-white flex items-center justify-center text-sm">?123</div>
            <div className="bg-slate-600 flex-grow h-10 rounded"></div>
            <div className="bg-teal-400 w-16 h-10 rounded flex items-center justify-center text-black font-bold">✓</div>
         </div>
      </div>
    </div>
  );
};

// --- Composant 3 : Selecteur de Type ---
const EstablishmentTypeModal = ({ onClose, onSelect }) => {
  const types = [
    "Aide à domicile",
    "Centres d'accueil et d'hébergement pour adultes et familles en difficulté",
    "Centres d'accueil et d'hébergement pour demandeurs d'asile",
    "Centres d'accueil et d'hébergement pour enfants en difficulté",
    "Centres de prévention en addictologie",
    "Centres éducatifs",
    "Établissements pour adultes en situation de handicap",
    "Établissements pour enfants et adolescents en situation de handicap",
    "Établissements pour personnes âgées",
    "Établissements sociaux avec prise en charge sanitaire",
    "Hôpitaux et Cliniques (incl. HAD)",
    "Soins à domicile",
    "Structures pour petite enfance (crèches)"
  ];

  return (
    <div className="absolute inset-0 bg-white z-50 flex flex-col animate-in slide-in-from-bottom duration-300">
      <div className="p-6 border-b border-gray-50 flex justify-between items-center">
        <h3 className="font-bold text-lg text-slate-900">Sélectionnez un type d'établissement</h3>
        <button onClick={onClose}><X size={24} className="text-slate-400" /></button>
      </div>
      <div className="flex-grow overflow-y-auto px-6 py-2 pb-12">
        {types.map((type, i) => (
          <div 
            key={i} 
            onClick={() => onSelect(type)}
            className="py-4 border-b border-gray-50 text-slate-700 text-sm font-medium hover:text-[#FF2E63] cursor-pointer"
          >
            {type}
          </div>
        ))}
      </div>
    </div>
  );
};

// --- Composant Principal : Formulaire ---
const App = () => {
  // Gestion des états pour les modales
  const [activeModal, setActiveModal] = useState(null); // 'startDate', 'endDate', 'city', 'type', null
  
  // Données du formulaire
  const [formData, setFormData] = useState({
    startDate: '',
    endDate: '',
    city: '',
    type: ''
  });

  const updateField = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    setActiveModal(null);
  };

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900 flex flex-col">
        
        {/* Status Bar */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900 bg-white z-10 flex-shrink-0">
          <span>06:57</span>
          <div className="flex gap-1 items-center"><span>5G</span><div className="w-6 h-3 bg-gray-800 rounded-sm"></div></div>
        </div>

        {/* --- Header Formulaire --- */}
        <div className="flex items-center justify-between px-6 py-4 border-b border-gray-50">
           <button className="p-2 -ml-2 rounded-full hover:bg-gray-100"><ChevronLeft size={28} className="text-slate-800" /></button>
           <span className="font-bold text-slate-900">Ajouter une nouvelle expérience</span>
           <HelpCircle size={24} className="text-slate-400" />
        </div>

        {/* --- Corps du Formulaire --- */}
        <div className="p-6 space-y-6 overflow-y-auto pb-24">
            
            {/* Date Inputs */}
            <div>
              <label className="block text-sm font-bold text-slate-900 mb-2">Date</label>
              <div className="flex gap-4">
                <div className="flex-1 cursor-pointer" onClick={() => setActiveModal('startDate')}>
                  <div className={`w-full border rounded-lg px-4 py-3 text-sm flex items-center justify-between ${formData.startDate ? 'border-slate-800 text-slate-900' : 'border-gray-200 text-gray-400'}`}>
                     <span>{formData.startDate || "MM/AAAA"}</span>
                     <Calendar size={16} />
                  </div>
                  <span className="text-xs text-slate-400 mt-1 block">Début</span>
                </div>
                <div className="flex-1 cursor-pointer" onClick={() => setActiveModal('endDate')}>
                   <div className={`w-full border rounded-lg px-4 py-3 text-sm flex items-center justify-between ${formData.endDate ? 'border-slate-800 text-slate-900' : 'border-gray-200 text-gray-400'}`}>
                     <span>{formData.endDate || "MM/AAAA"}</span>
                     <Calendar size={16} />
                   </div>
                   <span className="text-xs text-slate-400 mt-1 block">Fin</span>
                </div>
              </div>
            </div>

            {/* Checkbox */}
            <div className="flex items-center gap-3">
              <div className="w-5 h-5 border border-gray-300 rounded"></div>
              <span className="text-slate-700 text-sm">Je suis toujours en poste</span>
            </div>

            {/* Intitulé (Static for demo) */}
            <div>
              <label className="block text-sm font-bold text-slate-900 mb-2">Intitulé du poste</label>
              <div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-400">Sélectionner...</div>
            </div>

            {/* Etablissement (Static for demo) */}
            <div>
               <label className="block text-sm font-bold text-slate-900 mb-2">Nom de l'établissement</label>
               <input className="w-full border border-gray-200 rounded-lg px-4 py-3 outline-none" />
            </div>

            {/* Ville (Trigger Modal) */}
            <div onClick={() => setActiveModal('city')}>
               <label className="block text-sm font-bold text-slate-900 mb-2">Ville</label>
               <div className={`w-full border rounded-lg px-4 py-3 text-sm ${formData.city ? 'text-slate-900' : 'text-gray-400'}`}>
                   {formData.city || "Saisir une ville..."}
               </div>
            </div>

            {/* Type Etablissement (Trigger Modal) */}
            <div onClick={() => setActiveModal('type')}>
               <label className="block text-sm font-bold text-slate-900 mb-2">Type d'établissement</label>
               <div className="relative">
                   <div className={`w-full border rounded-lg px-4 py-3 text-sm pr-8 truncate ${formData.type ? 'text-slate-900' : 'text-gray-400'}`}>
                       {formData.type || "Sélectionner..."}
                   </div>
                   <ChevronRight size={18} className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 rotate-90" />
               </div>
            </div>

             {/* Description */}
            <div>
               <label className="block text-sm font-bold text-slate-900 mb-2">Description</label>
               <textarea rows={3} placeholder="Que souhaitez-vous mettre en avant..." className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm resize-none"></textarea>
            </div>
             {/* Certificat */}
            <div>
               <label className="block text-sm font-bold text-slate-900 mb-2">Certificat de travail</label>
               <div className="border border-dashed border-gray-300 rounded-lg h-12"></div>
            </div>
        </div>

        {/* Fixed Button */}
        <div className="absolute bottom-0 w-full bg-white border-t border-gray-100 p-4 pb-8 z-10">
            <button className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-lg">Enregistrer l'expérience</button>
        </div>

        {/* --- MODALES --- */}

        {/* Date Picker Start (Juin style) */}
        <DatePickerModal 
            isOpen={activeModal === 'startDate'} 
            onClose={() => setActiveModal(null)} 
            onConfirm={(date) => updateField('startDate', date)}
            initialDate="start"
        />

        {/* Date Picker End (Novembre style) */}
        <DatePickerModal 
            isOpen={activeModal === 'endDate'} 
            onClose={() => setActiveModal(null)} 
            onConfirm={(date) => updateField('endDate', date)}
            initialDate="end"
        />

        {/* City Search */}
        {activeModal === 'city' && (
            <CitySearchModal 
                onClose={() => setActiveModal(null)}
                onSelect={(city) => updateField('city', city)}
            />
        )}

        {/* Type Selector */}
        {activeModal === 'type' && (
            <EstablishmentTypeModal 
                onClose={() => setActiveModal(null)}
                onSelect={(type) => updateField('type', type)}
            />
        )}

        {/* Home Indicator */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-20"></div>

      </div>
    </div>
  );
};

export default App;

Analyse des Fonctionnalités Reproduites

  1. Date Picker (Android Style) :
    • Le composant DatePickerModal reproduit fidèlement le design Android Material.
    • En-tête Teal (bg-[#0F766E]) : Affiche l'année en petit et la date en gros (ex: "Mar. 10 juin").
    • Logique Visuelle : J'ai hardcodé la configuration pour Juin (commençant un Lundi) et Novembre (commençant un Samedi) pour matcher exactement les captures d'écran 1 et 2.
    • Selection : Le cercle vert autour du jour sélectionné (10 ou 28).
  2. Recherche Ville (avec Clavier) :
    • La modale s'ouvre avec une animation slide-in-bottom.
    • Le Clavier est visuellement simulé en bas avec un style sombre (bg-[#1F2937]), incluant la barre de suggestions (Bagnol, Bagnols...). C'est du pur CSS/HTML pour le rendu visuel.
  3. Sélecteur de Type :
    • Une liste longue, scrollable, avec le bouton de fermeture X en haut à droite.
    • Au clic sur un élément, la modale se ferme et le champ du formulaire est rempli.
  4. Intégration Globale :
    • Le formulaire principal réagit : quand vous sélectionnez une date ou une ville dans les modales, le champ correspondant dans le formulaire se met à jour visuellement (le texte passe de gris à noir).

Vous avez maintenant un flux de saisie d'expérience complet et interactif !

JSON RPC ODOO version GEMINICategorie Categorie

C'est la dernière ligne droite du parcours : la Gestion de la liste des expériences et la Validation finale.

Cette planche montre une interaction classique de type "Master-Detail" avec une modale de confirmation.

Voici les défis UX/UI relevés ici :

  1. Liste de cartes (Screen 1) : Affichage résumé des expériences ajoutées (Titre, Lieu, Date) avec un bouton d'ajout rapide +.
  2. La Modale "Bottom Sheet" (Screen 2 & 4) : Elle se superpose à la liste (assombrissement de l'arrière-plan) pour demander confirmation avant de quitter. Elle contient une illustration vectorielle.
  3. Le Formulaire (Screen 3 & 5) : Il doit gérer deux états : Édition (champs pré-remplis comme "Cadre de santé") et Création (champs vides).

Voici le code complet pour gérer ce flux de validation.

Le Code (ExperienceListFlow.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState } from 'react';
import { ChevronLeft, ChevronRight, Plus, X, Calendar, HelpCircle, Check, Trash2 } from 'lucide-react';

// --- Composant : Carte d'Expérience (Item de liste) ---
const ExperienceCard = ({ data, onClick }) => (
  <div 
    onClick={onClick}
    className="bg-white border border-gray-200 rounded-xl p-4 flex items-center justify-between shadow-sm active:bg-gray-50 transition cursor-pointer"
  >
    <div>
      <h3 className="font-bold text-slate-900 text-sm mb-1">{data.title}</h3>
      <p className="text-slate-500 text-xs font-medium">{data.company}</p>
      <p className="text-slate-500 text-xs mb-2">{data.location}</p>
      <p className="text-slate-400 text-[10px]">{data.date}</p>
    </div>
    <ChevronRight size={20} className="text-slate-300" />
  </div>
);

// --- Composant : Modale de Confirmation (Bottom Sheet) ---
const CompletionModal = ({ isOpen, onClose, onAddMore, onContinue }) => {
  if (!isOpen) return null;

  return (
    <div className="absolute inset-0 z-50 flex flex-col justify-end bg-black/50 backdrop-blur-[1px] animate-in fade-in duration-300">
      <div className="bg-white rounded-t-3xl p-6 pb-10 animate-in slide-in-from-bottom duration-300 relative">
        
        {/* Close Button */}
        <button 
          onClick={onClose}
          className="absolute top-4 right-4 p-2 bg-gray-100 rounded-full hover:bg-gray-200"
        >
          <X size={20} className="text-slate-500" />
        </button>

        {/* Illustration (SVG Placeholder) */}
        <div className="flex justify-center mb-6 mt-2">
            <div className="relative w-32 h-32">
                {/* Cercle décoratif */}
                <div className="absolute inset-0 bg-red-50 rounded-full scale-90"></div>
                {/* Personnage vectoriel simplifié */}
                <svg viewBox="0 0 200 200" className="w-full h-full relative z-10">
                    <circle cx="100" cy="80" r="30" fill="#fecaca" /> {/* Tête */}
                    <rect x="70" y="110" width="60" height="80" rx="20" fill="#fff" stroke="#1e293b" strokeWidth="2"/> {/* Corps */}
                    <path d="M130 130 L160 100 L170 110 L140 140" fill="#ff2e63" /> {/* Bras avec dossier */}
                    <rect x="150" y="90" width="30" height="40" fill="#ff2e63" rx="2" /> {/* Dossier */}
                    <path d="M70 130 L40 160" stroke="#1e293b" strokeWidth="2" strokeLinecap="round" /> {/* Bras gauche */}
                    <circle cx="100" cy="80" r="30" fill="none" stroke="#1e293b" strokeWidth="2" />
                    <path d="M90 75 L90 85" stroke="#1e293b" strokeWidth="2" /> {/* Yeux */}
                    <path d="M110 75 L110 85" stroke="#1e293b" strokeWidth="2" />
                    <path d="M95 95 Q100 100 105 95" fill="none" stroke="#1e293b" strokeWidth="2" /> {/* Sourire */}
                </svg>
            </div>
        </div>

        {/* Text */}
        <div className="text-center mb-8">
          <h3 className="text-xl font-bold text-slate-900 mb-2">Avez-vous terminé ?</h3>
          <p className="text-slate-500 text-sm">
            Vous pouvez revenir et modifier à tout moment.
          </p>
        </div>

        {/* Actions Stack */}
        <div className="space-y-3">
          <button 
            onClick={onAddMore}
            className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
          >
            Ajouter d'autres expériences
          </button>
          
          <button 
            onClick={onContinue}
            className="w-full bg-white text-slate-900 font-bold py-4 rounded-xl border border-gray-200 shadow-sm hover:bg-gray-50 transition active:scale-95"
          >
            Continuer
          </button>
        </div>

      </div>
    </div>
  );
};

// --- Composant : Formulaire Expérience (Réutilisé & Adapté) ---
const ExperienceForm = ({ initialData, onSave, onBack }) => {
  // Si initialData existe, on est en mode édition
  const isEditMode = !!initialData;
  const [formData, setFormData] = useState(initialData || {
    startDate: '', endDate: '', current: false, title: '', establishment: '', city: '', type: '', description: ''
  });

  return (
    <div className="flex flex-col h-full bg-white animate-in slide-in-from-right duration-300">
      {/* Header Form */}
      <div className="flex items-center justify-between px-6 py-4 border-b border-gray-50">
        <button onClick={onBack} className="p-2 -ml-2 rounded-full hover:bg-gray-100">
          <ChevronLeft size={28} className="text-slate-800" />
        </button>
        <span className="font-bold text-slate-900">
          {isEditMode ? "Modifier l'expérience" : "Ajouter une nouvelle expérience"}
        </span>
        <HelpCircle size={24} className="text-slate-400" />
      </div>

      <div className="p-6 space-y-6 overflow-y-auto pb-24">
        {/* Date Row */}
        <div>
          <label className="block text-sm font-bold text-slate-900 mb-2">Date</label>
          <div className="flex gap-4">
             <div className="flex-1 relative">
                <input 
                  defaultValue={formData.startDate}
                  className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm outline-none focus:border-slate-900" 
                  placeholder="MM/AAAA"
                />
                <span className="text-xs text-slate-400 mt-1 block">Début</span>
             </div>
             <div className="flex-1">
                <input 
                  defaultValue={formData.endDate}
                  className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm outline-none focus:border-slate-900" 
                  placeholder="MM/AAAA"
                />
                <span className="text-xs text-slate-400 mt-1 block">Fin</span>
             </div>
          </div>
        </div>

        {/* Checkbox */}
        <div className="flex items-center gap-3">
           <div className={`w-5 h-5 border rounded flex items-center justify-center ${formData.current ? 'bg-black border-black' : 'border-gray-300'}`}>
              {formData.current && <Check size={14} className="text-white" />}
           </div>
           <span className="text-slate-700 text-sm">Je suis toujours en poste</span>
        </div>

        {/* Fields */}
        <div>
           <label className="block text-sm font-bold text-slate-900 mb-2">Intitulé du poste</label>
           <div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm flex justify-between items-center">
              <input defaultValue={formData.title} className="w-full outline-none" placeholder="Sélectionner..." />
           </div>
        </div>
        
        <div>
           <label className="block text-sm font-bold text-slate-900 mb-2">Nom de l'établissement</label>
           <input defaultValue={formData.establishment} className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm outline-none focus:border-slate-900" />
        </div>

        <div>
           <label className="block text-sm font-bold text-slate-900 mb-2">Ville</label>
           <input defaultValue={formData.city} className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm outline-none focus:border-slate-900" />
        </div>

        {/* Description */}
        <div>
           <label className="block text-sm font-bold text-slate-900 mb-2">Description</label>
           <textarea className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm resize-none" rows={3} placeholder="Que souhaitez-vous mettre en avant..."></textarea>
        </div>
      </div>

      {/* Footer CTA */}
      <div className="absolute bottom-0 w-full bg-white border-t border-gray-100 p-4 pb-8 z-10">
        <button 
          onClick={onSave}
          className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-lg hover:bg-[#e62552] transition active:scale-95"
        >
          Enregistrer l'expérience
        </button>
      </div>
    </div>
  );
};

// --- Composant : Liste Principale ---
const ExperienceListScreen = ({ experiences, onAddClick, onEditClick, onValidate }) => {
  return (
    <div className="flex flex-col h-full bg-white px-6 pt-6 animate-in fade-in duration-500">
      {/* Header */}
      <div className="flex items-center mb-6">
        <button className="p-2 -ml-2 rounded-full hover:bg-gray-100">
          <ChevronLeft size={28} className="text-slate-800" />
        </button>
        <div className="flex-grow text-center pr-8 font-bold text-lg opacity-0">Hidden Title</div>
        <span className="font-bold text-lg text-slate-900 absolute left-1/2 -translate-x-1/2">
          Compléter mes expériences
        </span>
      </div>

      <p className="text-slate-500 text-sm mb-6 leading-relaxed">
        Mettez en avant votre parcours et valorisez votre profil auprès des établissements.
      </p>

      {/* List */}
      <div className="space-y-4 overflow-y-auto pb-24">
        {experiences.map((exp) => (
          <ExperienceCard 
            key={exp.id} 
            data={exp} 
            onClick={() => onEditClick(exp)} 
          />
        ))}

        {/* Add Button */}
        <button 
          onClick={onAddClick}
          className="w-full border border-gray-200 rounded-xl p-4 flex items-center justify-center text-slate-800 hover:bg-gray-50 hover:border-gray-300 transition gap-2 h-16"
        >
          <Plus size={24} strokeWidth={1.5} />
        </button>
      </div>

      {/* Footer Validation */}
      <div className="absolute bottom-0 left-0 right-0 p-6 pb-8 bg-white border-t border-gray-50">
        <button 
          onClick={onValidate}
          className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
        >
          Valider
        </button>
      </div>
    </div>
  );
};

// --- Main App Controller ---
const App = () => {
  const [view, setView] = useState('list'); // 'list', 'form'
  const [showModal, setShowModal] = useState(false);
  const [editItem, setEditItem] = useState(null); // Data passed to form

  // Données factices initiales (Comme sur l'écran 1)
  const [experiences, setExperiences] = useState([
    {
      id: 1,
      title: "Cadre de santé",
      company: "Someflu",
      location: "Bagnolet, France",
      date: "Mai 2025 - Juin 2025",
      startDate: "05/2025",
      endDate: "06/2025",
      establishment: "Someflu",
      city: "Bagnolet, France"
    }
  ]);

  // Actions
  const handleEditClick = (item) => {
    setEditItem(item);
    setView('form');
  };

  const handleAddClick = () => {
    setEditItem(null); // Mode création (Formulaire vide)
    setView('form');
  };

  const handleSaveForm = () => {
    // Ici on sauvegarderait les données...
    // Puis retour à la liste avec la modale de confirmation
    setView('list');
    setTimeout(() => setShowModal(true), 300); // Petit délai pour le réalisme
  };

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900">
        
        {/* Status Bar */}
        <div className="h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold text-gray-900 bg-white z-20 relative">
          <span>06:59</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className="w-6 h-3 bg-gray-800 rounded-sm"></div>
          </div>
        </div>

        {/* Content Area */}
        <div className="h-[calc(100%-3rem)] relative bg-white">
          
          {view === 'list' && (
            <ExperienceListScreen 
              experiences={experiences}
              onAddClick={handleAddClick}
              onEditClick={handleEditClick}
              onValidate={() => setShowModal(true)}
            />
          )}

          {view === 'form' && (
            <ExperienceForm 
              initialData={editItem}
              onBack={() => setView('list')}
              onSave={handleSaveForm}
            />
          )}

          {/* Modal Overlay (Rendered conditionally on top of list) */}
          <CompletionModal 
            isOpen={showModal}
            onClose={() => setShowModal(false)}
            onAddMore={() => {
              setShowModal(false);
              handleAddClick(); // Retourne au formulaire vide
            }}
            onContinue={() => {
              alert("Parcours terminé ! Redirection vers l'accueil...");
              setShowModal(false);
            }}
          />

        </div>
        
        {/* Home Indicator */}
        <div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-black rounded-full z-30"></div>

      </div>
    </div>
  );
};

export default App;

Explication du flux UX :

  1. L'Écran Liste (view === 'list') :
    • C'est l'écran par défaut.
    • Il affiche la carte "Cadre de santé" que j'ai stylisée avec une bordure légère et une flèche à droite, comme sur la capture 1.
    • Le bouton + est une boîte simple avec une icône centrée.
    • Le bouton "Valider" en bas déclenche la modale.
  2. La Modale de Confirmation (CompletionModal) :
    • C'est le composant clé des écrans 2 et 4.
    • Elle utilise absolute inset-0 et z-50 pour couvrir toute l'interface (sauf la barre de statut) avec un fond semi-transparent.
    • L'illustration est un SVG personnalisé ("Bonhomme avec presse-papier") pour correspondre au style graphique sans nécessiter d'image externe.
    • Le bouton "Ajouter d'autres expériences" (Rose) redirige vers le formulaire vide.
  3. Le Formulaire (ExperienceForm) :
    • Il est intelligent : si je clique sur le bouton +, initialData est nul, donc le formulaire est vide (Écran 5).
    • Si je clique sur la carte "Cadre de santé", initialData est rempli, et le titre du header change (simulant l'écran 3).

Ceci conclut l'ensemble de votre cinématique ! Vous disposez maintenant de tous les composants React pour assembler l'application complète. 🚀

JSON RPC ODOO version GEMINICategorie Categorie

C'est la suite logique du parcours : la Complétion du Dossier Administratif.

Cette étape introduit une nouvelle structure de liste de tâches ("Task List") avec des statuts (vide = à faire, check = fait) et une navigation profonde vers un lecteur vidéo vertical (similaire aux "Stories" ou TikTok) pour l'éducation utilisateur.

Voici les défis techniques de cette planche :

  1. Liste de Tâches (Screen 2) : Une liste mixte avec des sections "Obligatoire" et "Optionnel".
  2. Navigation Imbriquée : Overview -> Liste des Tâches -> Détail Tâche -> Vidéo.
  3. Video Player "Vertical" (Screen 4) : Interface immersive plein écran avec superposition d'éléments (sous-titres, boutons d'action, et une incrustation "Picture-in-Picture").

Voici le code complet pour ce module "Documents & Engagements".

Le Code (DocumentsFlow.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState, useEffect } from 'react';
import { 
  ChevronLeft, MoreVertical, Check, ArrowRight, Circle, 
  Play, Share2, Edit2, X, ChevronRight 
} from 'lucide-react';

// --- Composant 1 : Vue d'ensemble (Checklist des étapes) ---
const OverviewScreen = ({ onNext }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 pb-6 animate-in fade-in duration-500">
    {/* Header */}
    <div className="flex justify-between items-center mb-8">
      <button className="p-2 -ml-2 rounded-full hover:bg-gray-100">
        <ChevronLeft size={28} className="text-slate-800" />
      </button>
      <button className="p-2 -mr-2 rounded-full hover:bg-gray-100">
        <MoreVertical size={24} className="text-slate-800" />
      </button>
    </div>

    {/* Illustration */}
    <div className="flex justify-center mb-8">
      <div className="relative w-40 h-40">
         <div className="absolute inset-0 bg-rose-50 rounded-full scale-90"></div>
         <img 
           src="https://illustrations.popsy.co/amber/smartphone-addiction.svg" 
           alt="Hand holding phone"
           className="relative z-10 w-full h-full object-contain"
         />
      </div>
    </div>

    <h1 className="text-2xl font-bold text-slate-900 mb-8">
      Créez votre profil
    </h1>

    <div className="space-y-6 mb-8">
      {/* Étape 1 : Terminée */}
      <div className="flex items-center gap-3 text-emerald-500">
        <Check size={20} strokeWidth={3} />
        <span className="font-medium text-lg line-through decoration-emerald-500/50 text-slate-400">Précisez vos critères de mission</span>
      </div>

      {/* Étape 2 : Terminée */}
      <div className="flex items-center gap-3 text-emerald-500">
        <Check size={20} strokeWidth={3} />
        <span className="font-medium text-lg line-through decoration-emerald-500/50 text-slate-400">Renseignez vos expériences</span>
      </div>

      {/* Étape 3 : Active */}
      <div className="flex items-center gap-3 text-slate-900">
        <ArrowRight size={20} strokeWidth={2.5} />
        <span className="font-bold text-lg">Complétez vos informations</span>
      </div>

      {/* Étape 4 : Future */}
      <div className="flex flex-col gap-5 pl-8 border-l border-gray-100 ml-2.5 py-1">
        <span className="text-slate-400 font-medium">Validez votre profil avec notre équipe</span>
      </div>
    </div>

    <div className="mt-auto">
      <button 
        onClick={onNext}
        className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
      >
        Compléter
      </button>
    </div>
  </div>
);

// --- Composant 2 : Liste des Tâches (Documents) ---
const TaskListScreen = ({ onTaskClick, completedTasks }) => {
  const tasks = [
    { id: 'engagements', label: "Vos engagements sur Synergia" },
    { id: 'identity', label: "Document d'identité" },
    { id: 'photo', label: "Photo de profil" },
    { id: 'rpps', label: "Identifiant RPPS" },
    { id: 'status', label: "Statut d'indépendant" },
    { id: 'diploma', label: "Diplôme" },
  ];

  return (
    <div className="flex flex-col h-full bg-white px-6 pt-6 pb-6 animate-in slide-in-from-right duration-300">
      {/* Header with Progress Bar */}
      <div className="mb-6">
        <button className="p-2 -ml-2 mb-4 rounded-full hover:bg-gray-100">
          <ChevronLeft size={28} className="text-slate-800" />
        </button>
        {/* Progress Bar Top */}
        <div className="h-1.5 w-full bg-gray-100 rounded-full overflow-hidden">
           <div className="h-full bg-slate-900 w-3/4 rounded-full"></div>
        </div>
      </div>

      <h2 className="text-2xl font-bold text-slate-900 mb-2 leading-tight">
        Remplissez ces dernières tâches avant de valider votre profil
      </h2>

      <div className="flex-grow overflow-y-auto -mx-6 px-6 pt-4">
        {/* Section Obligatoire */}
        <div className="text-[10px] font-bold text-slate-400 tracking-wider uppercase mb-4 mt-2">
          Obligatoire pour continuer
        </div>

        <div className="space-y-6 mb-8">
          {tasks.map((task) => {
            const isDone = completedTasks.includes(task.id);
            return (
              <div 
                key={task.id}
                onClick={() => task.id === 'engagements' && onTaskClick(task.id)}
                className="flex items-center justify-between cursor-pointer group"
              >
                <div className="flex items-center gap-4">
                  {isDone ? (
                     <div className="w-6 h-6 rounded-full bg-emerald-500 flex items-center justify-center text-white">
                        <Check size={14} strokeWidth={3} />
                     </div>
                  ) : (
                     <div className="w-6 h-6 rounded-full border-2 border-slate-300 group-hover:border-slate-800 transition"></div>
                  )}
                  <span className={`font-medium text-lg ${isDone ? 'text-slate-900' : 'text-slate-800'}`}>
                    {task.label}
                  </span>
                </div>
                <ChevronRight size={20} className="text-slate-300" />
              </div>
            );
          })}
        </div>

        {/* Section Optionnel */}
        <div className="text-[10px] font-bold text-slate-400 tracking-wider uppercase mb-4 border-t border-gray-100 pt-6">
          Optionnel à cette étape
        </div>
        
        <div className="flex items-center justify-between cursor-pointer group mb-8">
            <div className="flex items-center gap-4">
                <div className="w-6 h-6 rounded-full border-2 border-slate-300 group-hover:border-slate-800 transition"></div>
                <span className="font-medium text-lg text-slate-800">CV (Curriculum vitæ)</span>
            </div>
            <ChevronRight size={20} className="text-slate-300" />
        </div>
      </div>

      {/* Footer Button */}
      <div className="pt-4 mt-auto">
         <button className="w-full bg-gray-100 text-gray-400 font-bold py-4 rounded-xl cursor-not-allowed">
            Continuer
         </button>
      </div>
    </div>
  );
};

// --- Composant 3 : Détail Tâche (Intro Vidéo) ---
const TaskDetailScreen = ({ onBack, onStartVideo }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 animate-in slide-in-from-right duration-300">
    {/* Header */}
    <div className="flex items-center mb-6">
        <button onClick={onBack} className="p-2 -ml-2 rounded-full hover:bg-gray-100">
          <ChevronLeft size={28} className="text-slate-800" />
        </button>
        <span className="font-bold text-slate-900 ml-4">Vos engagements sur Synergia</span>
    </div>

    <div className="border-t border-gray-100 pt-6">
       <h2 className="text-xl font-bold text-slate-900 mb-2">Découvrez vos responsabilités</h2>
       <p className="text-slate-500 text-sm mb-8">
         Visionnez la vidéo entièrement pour valider votre profil.
       </p>

       {/* Video Item */}
       <div 
         onClick={onStartVideo}
         className="flex items-center justify-between cursor-pointer py-2 hover:bg-gray-50 -mx-2 px-2 rounded-lg transition"
       >
          <div className="flex items-center gap-4">
             {/* Play Icon Circle */}
             <div className="w-8 h-8 rounded-full border-2 border-slate-900 flex items-center justify-center">
                <Play size={12} className="fill-slate-900 text-slate-900 ml-0.5" />
             </div>
             <div>
                <div className="font-bold text-slate-900">Frais et déclaration URSSAF</div>
                <div className="text-xs font-medium text-slate-500">Vidéo à regarder</div>
             </div>
          </div>
          <ChevronRight size={20} className="text-slate-300" />
       </div>
    </div>
  </div>
);

// --- Composant 4 : Lecteur Vidéo Vertical (Immersif) ---
const FullScreenVideo = ({ onClose, onComplete }) => {
  const [progress, setProgress] = useState(0);

  // Simuler la lecture
  useEffect(() => {
    const timer = setInterval(() => {
      setProgress(old => {
        if (old >= 100) {
          clearInterval(timer);
          // Auto close after completion just for demo feel
          // setTimeout(onComplete, 500);
          return 100;
        }
        return old + 0.5;
      });
    }, 30);
    return () => clearInterval(timer);
  }, []);

  return (
    <div className="fixed inset-0 bg-black z-50 flex flex-col animate-in fade-in duration-500">
       
       {/* Fake Video Content (Image) */}
       <div className="absolute inset-0">
          <img 
            src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?ixlib=rb-4.0.3&auto=format&fit=crop&w=688&q=80" 
            alt="Video Speaker"
            className="w-full h-full object-cover opacity-90"
          />
          {/* Gradient overlay for text readability */}
          <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-black/20"></div>
       </div>

       {/* Header Controls */}
       <div className="absolute top-12 left-0 right-0 px-4 flex justify-between items-center text-white z-10">
          <div className="flex items-center gap-2">
             <div className="w-8 h-8 rounded-full overflow-hidden border border-white/50">
               <img src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&w=100&q=80" className="w-full h-full object-cover"/>
             </div>
             <span className="font-semibold text-sm">Brigad • Synergia</span>
          </div>
          <button onClick={onClose}><X size={24} /></button>
       </div>

       {/* PiP Overlay (Bottom Left) */}
       <div className="absolute bottom-24 left-4 z-20">
          <div className="w-24 h-40 rounded-xl overflow-hidden border-2 border-white shadow-2xl relative bg-black">
             <img 
               src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&w=200&q=80" 
               className="w-full h-full object-cover"
             />
             {/* Fake Phone UI Overlay inside PiP */}
             <div className="absolute bottom-2 left-1/2 -translate-x-1/2 w-8 h-1 bg-white/50 rounded-full"></div>
          </div>
       </div>

       {/* Subtitles */}
       <div className="absolute bottom-32 right-6 left-32 text-right z-10">
          <p className="text-white text-xl font-bold leading-tight drop-shadow-md">
            en quelques clics.
          </p>
       </div>

       {/* Bottom Controls */}
       <div className="absolute bottom-8 left-4 right-4 z-10">
          <div className="flex items-center gap-4">
             {/* Social Buttons */}
             <button className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-md flex items-center justify-center text-white">
                <Share2 size={18} />
             </button>
             <button className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-md flex items-center justify-center text-white">
                <Edit2 size={18} />
             </button>

             {/* Progress Bar */}
             <div className="flex-grow h-1 bg-white/30 rounded-full overflow-hidden">
                <div className="h-full bg-white transition-all duration-100 ease-linear" style={{ width: `${progress}%` }}></div>
             </div>
          </div>
          
          {/* Finish Button (Appears at end) */}
          {progress >= 100 && (
            <button 
              onClick={onComplete}
              className="absolute bottom-16 left-0 right-0 mx-auto w-40 bg-[#FF2E63] text-white font-bold py-2 rounded-full shadow-lg animate-in zoom-in"
            >
              Terminer
            </button>
          )}
       </div>
    </div>
  );
};

// --- Main App Controller ---
const App = () => {
  const [step, setStep] = useState('overview'); // overview, list, detail, video
  const [completedTasks, setCompletedTasks] = useState([]);

  const completeEngagementTask = () => {
     setCompletedTasks(prev => [...prev, 'engagements']);
     setStep('list');
  };

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900 flex flex-col">
        
        {/* Status Bar */}
        <div className={`h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold z-20 relative ${step === 'video' ? 'bg-black text-white' : 'bg-white text-gray-900'}`}>
          <span>06:59</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className={`w-6 h-3 rounded-sm ${step === 'video' ? 'bg-white' : 'bg-gray-800'}`}></div>
          </div>
        </div>

        {/* Content */}
        <div className="h-[calc(100%-3rem)] relative bg-white">
          
          {step === 'overview' && (
             <OverviewScreen onNext={() => setStep('list')} />
          )}

          {step === 'list' && (
             <TaskListScreen 
                completedTasks={completedTasks}
                onTaskClick={() => setStep('detail')} 
             />
          )}

          {step === 'detail' && (
             <TaskDetailScreen 
               onBack={() => setStep('list')}
               onStartVideo={() => setStep('video')}
             />
          )}

          {step === 'video' && (
             <FullScreenVideo 
               onClose={() => setStep('detail')}
               onComplete={completeEngagementTask}
             />
          )}

        </div>
        
        {/* Home Indicator */}
        <div className={`absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 rounded-full z-30 ${step === 'video' ? 'bg-white' : 'bg-black'}`}></div>

      </div>
    </div>
  );
};

export default App;

Analyse des Fonctionnalités Implémentées

  1. Liste de Tâches (Screen 2) :
    • Structure visuelle : J'ai utilisé une mise en page très aérée. Les items sont cliquables.
    • Section Headers : Les titres "OBLIGATOIRE POUR CONTINUER" utilisent une taille de police minuscule (text-[10px]), du gras et de l'uppercase (uppercase tracking-wider) pour reproduire fidèlement le style "légal/administratif".
    • État du bouton : Le bouton "Continuer" en bas est grisé (bg-gray-100 text-gray-400) pour indiquer qu'il faut finir les tâches obligatoires.
  2. Navigation en Profondeur :
    • Le flux est géré par l'état step.
    • L'utilisateur doit cliquer sur "Vos engagements..." pour accéder à l'écran 3, puis sur l'item "Frais et déclaration" pour lancer la vidéo. C'est un pattern UX classique pour éviter de surcharger l'utilisateur.
  3. Video Player "TikTok Style" (Screen 4) :
    • Immersion : Le fond est noir, l'image couvre tout (object-cover).
    • Picture in Picture (PiP) : J'ai recréé l'effet de l'écran 4 avec une div positionnée en absolu (bottom-24 left-4), contenant une image et une bordure blanche.
    • Contrôles : Les boutons de partage et d'édition sont ronds avec un fond flouté (backdrop-blur-md), typique des interfaces vidéo modernes.
    • Logique de fin : Un bouton "Terminer" apparaît quand la barre de progression atteint 100%, permettant de valider la tâche et de revenir à la liste avec une coche verte.

C'est un module complet qui simule parfaitement la fin du parcours d'inscription !

JSON RPC ODOO version GEMINI Categorie Categorie

C'est une séquence très spécifique appelée KYC (Know Your Customer). Elle est souvent déléguée à des prestataires externes (comme Persona ou Onfido), ce qui explique le changement subtil de design (boutons gris, logos spécifiques) et les interactions natives (demande de caméra).

Voici les défis techniques de cette planche :

  1. L'écran Intro (Screen 1) : Beaucoup de texte légal en petit caractères et un footer "Secured by Persona".
  2. La Modale Système (Screen 4) : Reproduction fidèle de l'alerte de permission Android (fond sombre, boutons gris empilés).
  3. L'Interface Caméra (Screen 5) : Une interface immersive (fond noir) avec un gabarit de visée (overlay) spécifique pour scanner une carte d'identité (coins arrondis, zone transparente).

Voici le code complet pour ce module "Vérification d'Identité".

Le Code (IdentityVerificationFlow.jsx)

codeJsx

downloadcontent_copy

expand_less
import React, { useState, useEffect } from 'react';
import { 
  X, ChevronLeft, ChevronRight, Camera, Flashlight, AlertCircle, 
  CreditCard, FileText, Globe, Zap 
} from 'lucide-react';

// --- Composant : Footer "Secured by Persona" ---
const PersonaFooter = () => (
  <div className="flex justify-center items-center gap-2 py-6 opacity-60 grayscale">
    <span className="text-[10px] font-bold tracking-widest uppercase text-slate-400">Secured with</span>
    <div className="font-bold text-slate-800 text-sm flex items-center gap-1">
      <div className="w-3 h-3 bg-slate-800 rounded-sm"></div> {/* Fake Logo */}
      Persona
    </div>
  </div>
);

// --- Écran 1 : Intro KYC ---
const IntroScreen = ({ onStart, onClose }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 animate-in fade-in duration-500">
    <div className="flex justify-end mb-4">
      <button onClick={onClose}><X size={24} className="text-slate-400" /></button>
    </div>

    <h1 className="text-2xl font-bold text-slate-900 mb-2">
      Vous allez vérifier votre identité
    </h1>
    <p className="text-slate-500 text-sm mb-8">
      Nous avons besoin d'informations pour confirmer votre identité.
    </p>

    {/* Illustration Vectorielle Custom (Cartes ID) */}
    <div className="flex justify-center mb-8 relative">
        <div className="w-64 h-48 relative">
            {/* Background Blob */}
            <div className="absolute inset-0 bg-[#FF2E63] rounded-full opacity-10 scale-90 blur-xl"></div>
            
            {/* Illustration SVG simulée en CSS/JSX */}
            <div className="relative w-full h-full flex items-center justify-center">
                {/* Carte Arrière */}
                <div className="absolute top-4 right-8 w-32 h-24 bg-white border-2 border-slate-900 rounded-lg transform rotate-12 shadow-sm flex flex-col p-2">
                    <div className="w-8 h-8 rounded-full border border-slate-900 mb-2"></div>
                    <div className="w-16 h-2 bg-slate-200 rounded mb-1"></div>
                    <div className="w-10 h-2 bg-slate-200 rounded"></div>
                </div>
                {/* Carte Avant */}
                <div className="absolute top-12 left-8 w-40 h-28 bg-white border-2 border-[#FF2E63] rounded-lg transform -rotate-6 shadow-lg flex flex-col p-3 z-10">
                    <div className="flex gap-3">
                        <div className="w-12 h-12 rounded bg-rose-100 border border-rose-200 flex items-center justify-center">
                            <div className="w-6 h-6 rounded-full bg-[#FF2E63] opacity-20"></div>
                        </div>
                        <div className="flex-1 space-y-2 py-1">
                            <div className="w-full h-2 bg-slate-900 rounded-full opacity-80"></div>
                            <div className="w-2/3 h-2 bg-slate-300 rounded-full"></div>
                            <div className="w-1/2 h-2 bg-slate-300 rounded-full"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    {/* Legal Text */}
    <div className="mt-auto mb-6">
      <p className="text-[10px] text-slate-400 leading-relaxed text-justify mb-6">
        En cliquant sur le bouton ci-dessous, vous consentez à ce que Persona, notre fournisseur, collecte, utilise et fasse appel à ses prestataires de services pour traiter vos informations biométriques afin de vérifier votre identité, d'identifier les fraudes et d'améliorer la plateforme. <span className="text-[#FF2E63] underline">Politique de confidentialité</span>. Vos données biométriques seront conservées pendant 3 ans au maximum.
      </p>

      <button 
        onClick={onStart}
        className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-[0_4px_14px_rgba(255,46,99,0.4)] hover:bg-[#e62552] transition active:scale-95"
      >
        Commencer
      </button>

      <PersonaFooter />
    </div>
  </div>
);

// --- Écran 2 : Nationalité ---
const NationalityScreen = ({ onNext, onBack }) => (
  <div className="flex flex-col h-full bg-white px-6 pt-6 animate-in slide-in-from-right duration-300">
    <div className="flex justify-between items-center mb-6">
      <button onClick={onBack}><ChevronLeft size={28} className="text-slate-800" /></button>
      <button><X size={24} className="text-slate-400" /></button>
    </div>

    <h2 className="text-xl font-bold text-slate-900 mb-8">
      Quelle est votre nationalité ?
    </h2>

    <div className="relative">
      <select className="w-full border border-slate-300 rounded-lg px-4 py-4 text-slate-900 font-medium appearance-none bg-white outline-none focus:border-slate-900 transition">
        <option>France</option>
        <option>Belgique</option>
        <option>Suisse</option>
      </select>
      <div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none">
        <span className="text-slate-400 text-xs">▼</span>
      </div>
    </div>

    <div className="mt-auto pb-8">
      <button 
        onClick={onNext}
        className="w-full bg-[#FF2E63] text-white font-bold py-4 rounded-xl shadow-lg hover:bg-[#e62552] transition active:scale-95"
      >
        Valider
      </button>
    </div>
  </div>
);

// --- Écran 3 : Choix du Document ---
const DocumentTypeScreen = ({ onSelect, onBack }) => {
  const docs = [
    { id: 'id_card', label: "Carte nationale d'identité", icon: <CreditCard size={20} /> },
    { id: 'passport', label: "Passeport", icon: <Globe size={20} /> },
    { id: 'residence', label: "Titre de séjour", icon: <FileText size={20} /> },
  ];

  return (
    <div className="flex flex-col h-full bg-white px-6 pt-6 animate-in slide-in-from-right duration-300">
      <div className="flex justify-between items-center mb-6">
        <button onClick={onBack}><ChevronLeft size={28} className="text-slate-800" /></button>
        <button><X size={24} className="text-slate-400" /></button>
      </div>

      <h2 className="text-xl font-bold text-slate-900 mb-8">
        Quel document d'identité souhaitez-vous fournir ?
      </h2>

      <div className="space-y-4">
        {docs.map((doc) => (
          <button 
            key={doc.id}
            onClick={onSelect}
            className="w-full flex items-center justify-between p-4 rounded-xl border border-transparent hover:bg-gray-50 hover:border-gray-200 transition group"
          >
            <div className="flex items-center gap-4">
              <div className="text-slate-400 group-hover:text-slate-900 transition">{doc.icon}</div>
              <span className="font-medium text-slate-700 group-hover:text-slate-900">{doc.label}</span>
            </div>
            <ChevronRight size={20} className="text-slate-300 group-hover:text-slate-900" />
          </button>
        ))}
      </div>
    </div>
  );
};

// --- Écran 4 : Modale Permission Caméra ---
const CameraPermissionScreen = ({ onAllow }) => (
  <div className="h-full bg-black flex items-center justify-center px-6 animate-in fade-in duration-300 relative">
    {/* Background Status Bar Simulation */}
    <div className="absolute top-0 w-full h-12 flex justify-between items-end px-6 pb-2 text-xs font-semibold text-white opacity-50">
        <span>07:02</span>
    </div>

    {/* The Modal */}
    <div className="bg-white w-full max-w-sm rounded-2xl p-6 text-center shadow-2xl animate-in zoom-in-95 duration-200">
      <div className="flex justify-center mb-4">
        <Camera size={32} className="text-slate-600" />
      </div>
      
      <h3 className="text-lg font-medium text-slate-900 mb-6 px-4 leading-snug">
        Autoriser <span className="font-bold">Synergia</span> à prendre des photos et enregistrer des vidéos ?
      </h3>

      <div className="space-y-2">
        <button 
          onClick={onAllow}
          className="w-full bg-slate-600 text-white font-medium py-3 rounded-full active:bg-slate-700 transition"
        >
          Lorsque vous utilisez l'appli
        </button>
        <button className="w-full bg-slate-600 text-white font-medium py-3 rounded-full active:bg-slate-700 transition">
          Uniquement cette fois-ci
        </button>
        <button className="w-full bg-slate-600 text-white font-medium py-3 rounded-full active:bg-slate-700 transition">
          Ne pas autoriser
        </button>
      </div>
    </div>

    {/* Flash Icon Background decoration */}
    <div className="absolute bottom-32 right-12 opacity-30">
       <Zap size={24} className="text-white" />
    </div>
  </div>
);

// --- Écran 5 : Interface Caméra (Scan) ---
const CameraScanScreen = ({ onBack }) => (
  <div className="h-full bg-black flex flex-col relative animate-in fade-in duration-500">
    
    {/* Top Controls */}
    <div className="flex justify-between items-center p-6 pt-12 text-white">
      <button onClick={onBack}><ChevronLeft size={28} /></button>
      <button><X size={24} /></button>
    </div>

    {/* Camera View Area */}
    <div className="flex-grow relative flex flex-col items-center justify-center px-4">
      
      {/* Simulation Flux Vidéo (Image floue) */}
      <div className="absolute inset-0 z-0 opacity-40">
         <img src="https://images.unsplash.com/photo-1550989460-0adf9ea622e2?q=80&w=1000&auto=format&fit=crop" className="w-full h-full object-cover blur-sm" />
      </div>

      {/* Overlay Frame */}
      <div className="relative z-10 w-full aspect-[1.58] border-2 border-white/30 rounded-xl overflow-hidden mb-8">
        {/* Pink Corners (Signature Visuelle Persona) */}
        <div className="absolute top-0 left-0 w-8 h-8 border-t-4 border-l-4 border-[#FF2E63] rounded-tl-lg"></div>
        <div className="absolute top-0 right-0 w-8 h-8 border-t-4 border-r-4 border-[#FF2E63] rounded-tr-lg"></div>
        <div className="absolute bottom-0 left-0 w-8 h-8 border-b-4 border-l-4 border-[#FF2E63] rounded-bl-lg"></div>
        <div className="absolute bottom-0 right-0 w-8 h-8 border-b-4 border-r-4 border-[#FF2E63] rounded-br-lg"></div>

        {/* Scan Animation Line */}
        <div className="absolute top-0 left-0 w-full h-1 bg-[#FF2E63]/50 shadow-[0_0_15px_rgba(255,46,99,0.8)] animate-[scan_2s_ease-in-out_infinite]"></div>

        {/* Silhouette Overlay */}
        <div className="absolute inset-0 flex items-center justify-start pl-8 opacity-50">
             <div className="w-20 h-24 border-2 border-white rounded-full flex items-center justify-center">
                 <div className="w-12 h-12 bg-white rounded-full mb-4"></div>
                 <div className="absolute bottom-6 w-16 h-8 bg-white rounded-t-full"></div>
             </div>
             <div className="ml-4 text-white text-[10px] tracking-widest opacity-80">
                 {'<<<<<<<<<<<<<<<<<<'} <br/> {'<<<<<<<<<<<<<<<<<<'}
             </div>
        </div>
      </div>

      {/* Helper Box */}
      <div className="bg-white rounded-lg p-3 flex items-center gap-3 w-full max-w-xs shadow-lg z-10 mb-8">
         <div className="w-8 h-6 border border-slate-900 rounded bg-rose-50 flex items-center justify-center">
            <div className="w-2 h-2 rounded-full bg-slate-900"></div>
         </div>
         <p className="text-xs font-semibold text-slate-900 leading-tight">
           Placez le recto de votre pièce d'identité dans le cadre
         </p>
      </div>

      <div className="flex items-center gap-1 text-white text-xs font-medium opacity-80 mb-4">
         <AlertCircle size={14} /> Conseils
      </div>
    </div>

    {/* Bottom Controls */}
    <div className="h-32 bg-black/50 backdrop-blur-sm flex items-center justify-around px-12 pb-4 z-20">
       <button className="text-white opacity-0"><Zap size={24} /></button> {/* Spacer */}
       
       {/* Shutter Button */}
       <button className="w-16 h-16 rounded-full border-4 border-white flex items-center justify-center active:scale-95 transition">
          <div className="w-14 h-14 bg-white rounded-full"></div>
       </button>

       <button className="text-white"><Zap size={24} /></button> {/* Flash */}
    </div>
  </div>
);

// --- Main Controller ---
const App = () => {
  const [step, setStep] = useState('intro'); // intro, nationality, doctype, permission, camera

  return (
    <div className="min-h-screen bg-gray-200 flex items-center justify-center font-sans py-8">
      
      {/* Custom Keyframe for Scanner */}
      <style>{`
        @keyframes scan {
          0% { top: 0%; opacity: 0; }
          10% { opacity: 1; }
          90% { opacity: 1; }
          100% { top: 100%; opacity: 0; }
        }
      `}</style>

      <div className="w-full max-w-md h-[844px] bg-white md:rounded-3xl shadow-2xl overflow-hidden relative border-8 border-gray-900 flex flex-col">
        
        {/* Status Bar (Dynamic Color) */}
        <div className={`h-12 w-full flex justify-between items-end px-6 pb-2 text-xs font-semibold z-20 absolute top-0 left-0 ${step === 'camera' || step === 'permission' ? 'text-white' : 'text-slate-900'}`}>
          <span>07:02</span>
          <div className="flex gap-1 items-center">
            <span>5G</span>
            <div className={`w-6 h-3 rounded-sm ${step === 'camera' || step === 'permission' ? 'bg-white' : 'bg-slate-900'}`}></div>
          </div>
        </div>

        {/* Content */}
        <div className="h-full relative bg-white">
          
          {step === 'intro' && (
             <div className="h-full pt-12">
               <IntroScreen 
                 onClose={() => alert("Quitter")} 
                 onStart={() => setStep('nationality')} 
               />
             </div>
          )}

          {step === 'nationality' && (
             <div className="h-full pt-12">
                <NationalityScreen 
                  onBack={() => setStep('intro')}
                  onNext={() => setStep('doctype')}
                />
             </div>
          )}

          {step === 'doctype' && (
             <div className="h-full pt-12">
                <DocumentTypeScreen 
                  onBack={() => setStep('nationality')}
                  onSelect={() => setStep('permission')}
                />
             </div>
          )}

          {step === 'permission' && (
             <CameraPermissionScreen 
               onAllow={() => setStep('camera')}
             />
          )}

          {step === 'camera' && (
             <CameraScanScreen 
               onBack={() => setStep('doctype')}
             />
          )}

        </div>
        
        {/* Home Indicator */}
        <div className={`absolute bottom-1 left-1/2 transform -translate-x-1/2 w-32 h-1 rounded-full z-30 ${step === 'camera' || step === 'permission' ? 'bg-white' : 'bg-black'}`}></div>

      </div>
    </div>
  );
};

export default App;

Détails Techniques et Visuels :

  1. L'Écran Caméra (Screen 5) :
    • Le Cadre (Overlay) : J'ai utilisé 4 div positionnées en absolu dans les coins (top-0 left-0, etc.) avec des bordures partielles (border-t-4 border-l-4) pour recréer exactement les coins roses arrondis.
    • L'Animation de Scan : J'ai ajouté une style tag avec une keyframe @keyframes scan pour faire descendre une barre lumineuse rose de haut en bas, simulant la lecture optique.
    • Silhouette : J'ai recréé la forme de la tête et des chevrons <<<< en CSS pur pour guider l'utilisateur.
  2. L'Écran Permission (Screen 4) :
    • Le fond est noir (simulant que la caméra est prête derrière mais bloquée).
    • La modale utilise des boutons gris (bg-slate-600) et une forme très arrondie, typique des overlays système Android récents.
  3. L'Écran Intro (Screen 1) :
    • L'illustration des cartes d'identité est faite en CSS (rectangles avec rotations rotate-12 et -rotate-6) pour éviter d'importer des images. Cela garde l'application ultra-légère.
    • Le footer "Secured with Persona" ajoute cette touche de crédibilité bancaire/légale.

C'est la brique finale technique de votre application ! Vous avez maintenant couvert tout le spectre : du marketing (landing), à l'inscription (wizard), aux tâches administratives (vidéo) et enfin à la vérification technique (caméra). 🎥✅

JSON RPC ODOO version GEMINI Categorie Categorie

Mapping des Vues avec la DB d'ODOO ? 

 Passer du Frontend (React) au Backend (Odoo) est l'étape critique. Odoo est très structuré, et pour garder le système maintenable, il faut utiliser au maximum les objets natifs (res.partner, hr.applicant, etc.) avant de créer du "custom" avec Studio.

Voici la méthodologie que je vous recommande :

La Méthode Recommandée : Architecture Globale + Mapping par Planche


Découvrir plus

Ne faites pas que du planche par planche aveugle. Il faut d'abord définir les 3 ou 4 Objets Piliers dans Odoo, puis nous détaillerons les champs planche par planche.

Si nous créons des champs x_ partout sans logique relationnelle, votre Odoo deviendra une usine à gaz.

Étape 1 : Définition des Objets Piliers (Architecture Odoo)

Avant de mapper les écrans, voici les modules Odoo que nous allons cibler.

  1. L'Infirmier (Utilisateur) 
    →→
     Module Contacts (res.partner)
    • C'est le cœur. Tout infirmier est un "Contact".
    • Pourquoi pas hr.employee ? Parce que ce sont des freelances, pas vos salariés internes.
    • On ajoutera un tag ou un boolean x_is_nurse = True.
  2. La Mission 
    →→
     Custom Model (x_mission) via Studio
    • Je recommande de créer un nouvel objet dédié plutôt que de détourner le module "Projets" ou "Ventes", car les champs (soins, horaires, urgence) sont très spécifiques.
  3. Les Documents (KYC) 
    →→
     Module Documents (documents.document) ou Pièces jointes (ir.attachment)
    • Liés à la fiche du res.partner.
  4. L'Expérience/CV 
    →→
     Custom Model (x_nurse_experience)
    • Ce sera une table liée (One2many) à l'infirmier.