diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..50d90fd0 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# Configuration de la base de données PostgreSQL +POSTGRES_DB=strapi +POSTGRES_USER=strapi +POSTGRES_PASSWORD=strapi + +# Configuration Strapi +SEED_DB=false +STRAPI_PORT=1338 +STRAPI_HOST=strapi +STRAPI_DISABLE_TELEMETRY=true +DATABASE_SSL=false +# Import SQL Configuration +# Active l'import SQL au démarrage du conteneur +IMPORT_SQL=false + +# Force le réimport même si la base contient déjà des données +# ATTENTION: Met FORCE_IMPORT=true seulement si vous voulez ÉCRASER la base existante +FORCE_IMPORT=false + +# Configuration Next.js +NEXTJS_PORT=3000 diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml new file mode 100644 index 00000000..aa50a5a0 --- /dev/null +++ b/.github/workflows/ci-cd.yaml @@ -0,0 +1,62 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' # Assuming Node.js 20, adjust if needed + + - name: Install Yarn + run: npm install -g yarn + + - name: Install root dependencies + run: yarn install --frozen-lockfile + + - name: Install Next.js dependencies + run: yarn install --frozen-lockfile + working-directory: ./next + + - name: Build Next.js + run: yarn build + working-directory: ./next + + - name: Install Strapi dependencies + run: yarn install --frozen-lockfile + working-directory: ./strapi + + - name: Build Strapi + run: yarn build + working-directory: ./strapi + + deploy: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Deploy to server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + password: ${{ secrets.SERVER_PASSWORD }} + script: | + cd ${{ secrets.PROJECT_PATH }} + git pull origin develop + docker-compose down + docker-compose up --build -d + docker system prune -f diff --git a/.gitignore b/.gitignore index 304ad3bb..6752d2d8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ node_modules !.yarn/plugins !.yarn/releases !.yarn/sdks -!.yarn/versions \ No newline at end of file +!.yarn/versions +uploads-extracted/ +uploads.tar.gz* \ No newline at end of file diff --git a/README.md b/README.md index 86278076..0cea9ccb 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,276 @@ -# LaunchPad - Official Strapi Demo +# Site Webtinix - Moderne et Optimisé ![LaunchPad](./LaunchPad.jpg) -Welcome aboard **LaunchPad**, the official Strapi demo application, where we launch your content into the stratosphere at the speed of _"we-can't-even-measure-it!"_. -This repository contains the following: +Bienvenue sur le nouveau site de **Webtinix**, propulsé par Strapi et Next.js. Ce projet est un fork du dépôt officiel [Strapi LaunchPad](https://github.com/strapi/LaunchPad), adapté et optimisé pour les besoins de Webtinix. -- A Strapi project with content-types and data already onboard -- A Next.js client that's primed and ready to fetch the content from Strapi faster than you can say "blast off!" +Ce dépôt contient : -## 🌌 Get started +* Un projet Strapi avec des types de contenu et des données préchargées +* Un client Next.js prêt à récupérer et afficher le contenu depuis Strapi +* Une configuration optimisée pour PostgreSQL en production -Strap yourself in! You can get started with this project on your local machine by following the instructions below, or you can [request a private instance on our website](https://strapi.io/demo) +## 🚀 Démarrage rapide -## 1. Clone Launchpad +Vous pouvez démarrer ce projet sur votre machine locale en suivant les instructions ci-dessous. -To infinity and beyond! 🚀 Clone the repo with this command: +### 1. Cloner le projet +Clonez le dépôt avec cette commande : + +```bash +git clone https://github.com/webtinix1/wx-refonte-with-launchpad.git +cd wx-refonte-with-launchpad ``` -git clone https://github.com/strapi/launchpad.git + +### 2. Configurer PostgreSQL + +Ce projet utilise PostgreSQL comme base de données. Voici comment la configurer : + +#### Installation de PostgreSQL + +Si PostgreSQL n'est pas installé sur votre machine : + +**Windows :** +- Téléchargez PostgreSQL depuis [postgresql.org](https://www.postgresql.org/download/windows/) +- Installez-le avec l'assistant d'installation +- Notez le mot de passe que vous définissez pour l'utilisateur `postgres` + +**Linux (Ubuntu/Debian) :** +```bash +sudo apt update +sudo apt install postgresql postgresql-contrib +``` + +**macOS :** +```bash +brew install postgresql +brew services start postgresql ``` -- Navigate to your project folder by running `cd launchpad`. +#### Créer la base de données + +Connectez-vous à PostgreSQL et créez la base de données pour Strapi : + +```bash +# Connectez-vous en tant que superutilisateur postgres +psql -U postgres -## 2. Set up environment variables +# Dans le shell PostgreSQL, exécutez : +CREATE USER strapi WITH PASSWORD 'strapi'; +CREATE DATABASE strapi OWNER strapi; -Before you take off, set up the required environment variables for both Strapi and Next.js. +# Accordez tous les droits nécessaires +GRANT ALL PRIVILEGES ON DATABASE strapi TO strapi; -To create the Strapi .env file, copy the content of the `./strapi/.env.example` file into a new file named `./strapi/.env`, then modify the values to match your setup: +# Connectez-vous à la base strapi +\c strapi -```sh +# Accordez les droits sur le schéma public +GRANT ALL ON SCHEMA public TO strapi; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO strapi; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO strapi; + +# Quittez le shell PostgreSQL +\q +``` + +**Note :** Pour les environnements de production, utilisez un mot de passe fort et sécurisé ! + +### 3. Configurer les variables d'environnement + +#### Configuration de Strapi + +Créez le fichier `.env` pour Strapi : + +```bash cp ./strapi/.env.example ./strapi/.env ``` -Then do the same for the Next.js .env file, and modify it too: +Modifiez `./strapi/.env` avec vos paramètres : + +```env +HOST=0.0.0.0 +PORT=1337 +APP_KEYS="votre-clé-1,votre-clé-2" +API_TOKEN_SALT=votre-token-salt +ADMIN_JWT_SECRET=votre-admin-secret +TRANSFER_TOKEN_SALT=votre-transfer-salt +JWT_SECRET=votre-jwt-secret + +# Base de données PostgreSQL +DATABASE_CLIENT=postgres +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_NAME=strapi +DATABASE_USERNAME=strapi +DATABASE_PASSWORD=strapi +DATABASE_SSL=false +DATABASE_SCHEMA=public + +# Optimisations +DATABASE_POOL_MIN=2 +DATABASE_POOL_MAX=20 +DATABASE_CONNECTION_TIMEOUT=600000 + +# Configuration Next.js (optionnel) +CLIENT_URL=http://localhost:3000 +PREVIEW_SECRET=votre-preview-secret + +# Environnement +NODE_ENV=development +STRAPI_DISABLE_TELEMETRY=true + +# Mémoire Node.js (pour les imports volumineux) +NODE_OPTIONS=--max-old-space-size=4096 +``` + +**Important :** Générez des clés sécurisées pour `APP_KEYS`, `API_TOKEN_SALT`, `ADMIN_JWT_SECRET`, etc. Ne réutilisez jamais les valeurs par défaut en production ! + +#### Configuration de Next.js -```sh +Créez le fichier `.env` pour Next.js : + +```bash cp ./next/.env.example ./next/.env ``` -## 3. Start Strapi +Modifiez `./next/.env` selon vos besoins : + +```env +NEXT_PUBLIC_STRAPI_URL=http://localhost:1337 +STRAPI_URL=http://localhost:1337 +PREVIEW_SECRET=votre-preview-secret +``` + +### 4. Démarrer Strapi + +Installez les dépendances, importez les données initiales et démarrez le serveur : + +```bash +cd strapi +yarn install +yarn seed +yarn develop +``` + +Le panneau d'administration Strapi sera accessible sur [http://localhost:1337/admin](http://localhost:1337/admin) -Take a deep breath. It's time to power up the Strapi engines. Navigate to your ./my-projects/launchpad/strapi folder by running: +**Note :** La commande `yarn seed` importe les données de démonstration. Si vous rencontrez des erreurs liées aux droits PostgreSQL, vérifiez que vous avez bien exécuté toutes les commandes SQL de la section "Créer la base de données". -Navigate to your `./my-projects/launchpad/strapi` folder by running `cd strapi` from your command line. +### 5. Démarrer Next.js -- Run the following command in your `./launchpad/strapi` folder: +Ouvrez un nouveau terminal et démarrez le client Next.js : +```bash +cd next +yarn install +yarn build +yarn start ``` -yarn && yarn seed && yarn develop + +Ou pour le mode développement : + +```bash +yarn dev ``` -This will install dependencies, sprinkle in some data magic, and run the server. (You can run these commands separately, but why not be efficient?) +Le site sera accessible sur [http://localhost:3000](http://localhost:3000) -## 4. Start Next.js +## 📚 Fonctionnalités -We're almost ready for lift-off! Next.js is your sleek, futuristic interface for getting all that glorious content out into the world. 🚀 +### Côté Utilisateur -Open a new terminal tab or window to leave Strapi running, and navigate to your `./my-projects/launchpad/next` folder by running `cd next`. +* **Éditeur intuitif et minimaliste** : Créez du contenu avec des blocs dynamiques +* **Bibliothèque média** : Téléchargez et optimisez vos images et vidéos +* **Gestion de contenu flexible** : Adaptez la structure selon vos besoins +* **Tri et filtrage** : Gérez facilement des milliers d'entrées +* **Interface conviviale** : L'une des interfaces open-source les plus faciles à utiliser +* **Optimisé SEO** : Gérez vos métadonnées SEO simplement -- Run the following command in your `./launchpad/next` folder +### Fonctionnalités Globales +* **API personnalisable** : REST ou GraphQL générées automatiquement +* **Bibliothèque média avancée** : Stockage et gestion optimisés +* **Contrôle d'accès basé sur les rôles (RBAC)** : Droits d'accès granulaires +* **Internationalisation (i18n)** : Gestion multilingue du contenu +* **Journaux d'audit** : Traçabilité de toutes les actions +* **Transfert de données** : Import/export entre instances Strapi +* **Workflow de révision** : Collaboration sur le cycle de vie du contenu + +## 🛠️ Scripts disponibles + +### Strapi + +```bash +yarn develop # Démarrer en mode développement +yarn start # Démarrer en mode production +yarn build # Construire le projet +yarn seed # Importer les données de démonstration ``` -yarn && yarn build && yarn start + +### Next.js + +```bash +yarn dev # Démarrer en mode développement +yarn build # Construire pour la production +yarn start # Démarrer en mode production +yarn lint # Vérifier le code ``` -This installs dependencies, builds your project, and starts your server. You’re now a spacefaring content master! +## 🔧 Dépannage -## Features Overview ✨ +### Erreur "droit refusé pour le schéma public" + +Si vous rencontrez cette erreur lors de l'exécution de `yarn seed`, c'est que l'utilisateur PostgreSQL n'a pas les droits nécessaires. Exécutez les commandes suivantes : + +```bash +psql -U postgres -d strapi + +GRANT ALL ON SCHEMA public TO strapi; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO strapi; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO strapi; + +\q +``` -### User +### Erreur de connexion à PostgreSQL -
+Vérifiez que : +1. PostgreSQL est bien démarré sur votre machine +2. Les identifiants dans `.env` correspondent à ceux configurés +3. La base de données `strapi` existe bien +4. L'utilisateur `strapi` a les droits nécessaires -**An intuitive, minimal editor** The editor allows you to pull in dynamic blocks of content. It’s 100% open-source, and it’s fully extensible.
-**Media Library** Upload images, video or any files and crop and optimize their sizes, without quality loss.
-**Flexible content management** Build any type of category, section, format or flow to adapt to your needs.
-**Sort and Filter** Built-in sorting and filtering: you can manage thousands of entries without effort.
-**User-friendly interface** The most user-friendly open-source interface on the market.
-**SEO optimized** Easily manage your SEO metadata with a repeatable field and use our Media Library to add captions, notes, and custom filenames to optimize the SEO of media assets.

+## 📖 Documentation -### Global +* [Documentation Strapi](https://docs.strapi.io) +* [Documentation Next.js](https://nextjs.org/docs) +* [Forum Strapi](https://forum.strapi.io/) +* [Discord Strapi](https://discord.strapi.io) -
+## 🌐 Déploiement -[Customizable API](https://strapi.io/features/customizable-api): Automatically build out the schema, models, controllers for your API from the editor. Get REST or GraphQL API out of the box without writing a single line of code.
-[Media Library](https://strapi.io/features/media-library): The media library allows you to store your images, videos and files in your Strapi admin panel with many ways to visualize and manage them.
-[Role-Based Access Control (RBAC)](https://strapi.io/features/custom-roles-and-permissions): Role-Based Access Control is a feature available in the Administration Panel settings that let your team members have access rights only to the information they need.
-[Internationalization (i18n)](https://strapi.io/features/internationalization): Internationalization (i18n) lets you create many content versions, also called locales, in different languages and for different countries.
-[Audit Logs](https://strapi.io/blog/reasons-and-best-practices-for-using-audit-logs-in-your-application)The Audit Logs section provides a searchable and filterable display of all activities performed by users of the Strapi application
-[Data transfer](https://strapi.io/blog/importing-exporting-and-transferring-data-with-the-strapi-cli) Streams your data from one Strapi instance to another Strapi instance.
-[Review Worfklows](https://docs.strapi.io/user-docs/settings/review-workflows) Create and manage any desired review stages for your content, enabling your team to collaborate in the content creation flow from draft to publication.
+Consultez les guides de déploiement dans le dépôt : +* `wx-deployment-docker-guide.md` - Déploiement avec Docker +* `wx-fork-launchpad-guide.md` - Guide du fork LaunchPad +* `wx-dev-best-practices.md` - Bonnes pratiques de développement -## Resources +## 📝 Personnalisations -[Docs](https://docs.strapi.io) • [Demo](https://strapi.io/demo) • [Forum](https://forum.strapi.io/) • [Discord](https://discord.strapi.io) • [Youtube](https://www.youtube.com/c/Strapi/featured) • [Strapi Design System](https://design-system.strapi.io/) • [Marketplace](https://market.strapi.io/) • [Cloud Free Trial](https://cloud.strapi.io) +Ce projet contient plusieurs personnalisations par rapport au LaunchPad original : -## Todo +* Configuration PostgreSQL optimisée pour la production +* Middlewares de population personnalisés dans les routes API +* Script postinstall pour la gestion des UUID +* Support natif de PostgreSQL au lieu de SQLite -- [ ] Implement the official Strapi SEO plugin -- [ ] Implement the community Strapi preview plugin -- [ ] Create localized content for the pricing plans and products -- [ ] Populate creator fields when it'll work on Strapi 5 (article authors information are missing) +## 📄 Licence -## Customization +MIT -- The Strapi application contains a custom population middlewares in every api route. +## 👥 À propos -- The Strapi application contains a postinstall script that will regenerate an uuid for the project in order to get some anonymous usage information concerning this demo. You can disable it by removing the uuid inside the `./strapi/packages.json` file. +Développé par **Webtinix** - [Site web](https://webtinix.com) -- The Strapi application contains a patch for the @strapi/admin package. It is only necessary for the hosted demos since we automatically create the Super Admin users for them when they request this demo on our website. +Basé sur [Strapi LaunchPad](https://github.com/strapi/LaunchPad) \ No newline at end of file diff --git a/README_docker.md b/README_docker.md new file mode 100644 index 00000000..4cf8f1e5 --- /dev/null +++ b/README_docker.md @@ -0,0 +1,488 @@ +# 🚀 Déploiement Docker - Webtinix Refonte + +Guide complet pour déployer l'application Webtinix (Next.js + Strapi + PostgreSQL) en utilisant Docker et Docker Compose. + +## 📋 Prérequis + +- **Docker** (version 20.10+) +- **Docker Compose** (version 2.0+) +- **Git** (pour cloner le projet) + +## 🛠️ Configuration + +### 1. Cloner le projet + +```bash +git clone https://github.com/webtinix1/wx-refonte-with-launchpad.git +cd wx-refonte-with-launchpad +``` + +### 2. Variables d'environnement + +Copiez le fichier d'exemple : + +```bash +cp .env.example .env +``` + +Modifiez `.env` avec vos valeurs sécurisées : + +```env +# Base de données PostgreSQL +POSTGRES_DB=strapi +POSTGRES_USER=strapi +POSTGRES_PASSWORD=votre_mot_de_passe_fort +DATABASE_SSL=false + +# Strapi +SEED_DB=true # IMPORTANT: voir section "Premier démarrage" ci-dessous +STRAPI_PORT=1337 +STRAPI_HOST=strapi +STRAPI_DISABLE_TELEMETRY=true + +# Next.js +NEXTJS_PORT=3000 +``` + +**⚠️ Sécurité :** +- Utilisez des mots de passe forts (au moins 16 caractères) +- Ne commitez jamais `.env` (ajoutez-le à `.gitignore`) + +### 3. Générer des clés sécurisées (optionnel) + +Pour Strapi, générez des clés sécurisées : + +```bash +# Générer une clé aléatoire +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +## 🚀 Démarrage + +### ⚠️ IMPORTANT : Distinction Premier Démarrage vs Démarrages Suivants + +L'application nécessite une configuration différente pour le **premier démarrage** (avec import des données) et les **démarrages suivants**. + +--- + +## 🆕 Premier Démarrage (avec SEED_DB=true) + +Pour le tout premier démarrage, vous devez importer les données initiales dans Strapi. + +### Étape 1 : Configuration du docker-compose.yml + +**Commentez** le volume `strapi_uploads` dans `docker-compose.yml` : + +```yaml +strapi: + # ... autres configurations + depends_on: + postgres: + condition: service_healthy + # COMMENTEZ CETTE LIGNE POUR LE PREMIER DÉMARRAGE : + # volumes: + # - strapi_uploads:/opt/app/public/uploads + networks: + - wx-refonte-sitenetwork +``` + +**Pourquoi ?** Le volume Docker écrase les permissions nécessaires pour créer le dossier de backup lors de l'import. + +### Étape 2 : Configurer .env + +```env +SEED_DB=true +``` + +### Étape 3 : Démarrer + +```bash +# Nettoyer complètement (si ce n'est pas la première fois) +docker-compose down -v + +# Construire et démarrer +docker-compose up --build + +# Ou en arrière-plan +docker-compose up -d --build +``` + +### Étape 4 : Vérifier l'import + +Surveillez les logs pour confirmer que l'import s'est bien passé : + +```bash +docker-compose logs -f strapi +``` + +Vous devriez voir : +``` +Starting database seeding... +Starting import... +Import process has been completed successfully! +Starting Strapi... +``` + +--- + +## 🔄 Démarrages Suivants (avec SEED_DB=false) + +Une fois les données importées avec succès, vous devez modifier la configuration pour les démarrages normaux. + +### Étape 1 : Modifier .env + +```env +SEED_DB=false +``` + +### Étape 2 : Réactiver le volume dans docker-compose.yml + +**Décommentez** le volume `strapi_uploads` : + +```yaml +strapi: + # ... autres configurations + depends_on: + postgres: + condition: service_healthy + volumes: + - strapi_uploads:/opt/app/public/uploads # DÉCOMMENTEZ CETTE LIGNE + networks: + - wx-refonte-sitenetwork +``` + +**Pourquoi ?** Le volume permet maintenant de persister vos fichiers uploadés entre les redémarrages. + +### Étape 3 : Redémarrer + +```bash +# Arrêter les conteneurs (SANS supprimer les volumes) +docker-compose down + +# Relancer +docker-compose up -d +``` + +--- + +## 📊 Récapitulatif des Configurations + +| Scénario | SEED_DB | Volume strapi_uploads | Commande | +|----------|---------|----------------------|----------| +| **Premier démarrage** | `true` | ❌ Commenté | `docker-compose down -v && docker-compose up --build` | +| **Démarrages normaux** | `false` | ✅ Activé | `docker-compose up -d` | +| **Réimport complet** | `true` | ❌ Commenté | `docker-compose down -v && docker-compose up --build` | + +--- + +## 🌐 Accès aux services + +Une fois démarré : + +- **Next.js (site web)** : http://localhost:3000 +- **Strapi Admin** : http://localhost:1337/admin +- **Base de données PostgreSQL** : Accessible uniquement depuis les conteneurs (port interne 5432) + +### Premier accès à Strapi Admin + +Si les données ont été importées avec succès, utilisez les identifiants configurés dans votre export. Sinon, créez un admin : + +```bash +docker-compose exec strapi yarn strapi admin:create-user +``` + +## 📝 Commandes utiles + +### Gestion des conteneurs + +```bash +# Voir les logs de tous les services +docker-compose logs -f + +# Logs d'un service spécifique +docker-compose logs -f strapi +docker-compose logs -f postgres +docker-compose logs -f nextjs + +# Arrêter tous les services +docker-compose down + +# Redémarrer un service +docker-compose restart strapi + +# Accéder au shell d'un conteneur +docker-compose exec strapi sh +docker-compose exec postgres psql -U strapi -d strapi +``` + +### Lancer uniquement certains services + +```bash +# Lancer uniquement Strapi et PostgreSQL (sans Next.js) +docker-compose up postgres strapi + +# Lancer en arrière-plan +docker-compose up -d postgres strapi +``` + +### Gestion de Strapi + +```bash +# Exécuter le seed manuellement +docker-compose exec strapi yarn seed + +# Construire Strapi (si modifications) +docker-compose exec strapi yarn build + +# Voir la structure de la base de données +docker-compose exec postgres psql -U strapi -d strapi -c "\dt" +``` + +### Gestion de Next.js + +```bash +# Voir les logs Next.js +docker-compose logs -f nextjs + +# Rebuild Next.js après modifications +docker-compose build nextjs && docker-compose up -d nextjs +``` + +### Base de données + +```bash +# Sauvegarder la base de données +docker-compose exec postgres pg_dump -U strapi strapi > backup_$(date +%Y%m%d_%H%M%S).sql + +# Restaurer la base de données +docker-compose exec -T postgres psql -U strapi strapi < backup.sql + +# Voir les tables +docker-compose exec postgres psql -U strapi -d strapi -c "\dt" + +# Se connecter à la base +docker-compose exec postgres psql -U strapi -d strapi +``` + +## 💾 Persistance des données + +Les données sont persistées dans des volumes Docker nommés : + +- `postgres_data` : Données PostgreSQL (tables, utilisateurs, etc.) +- `strapi_uploads` : Fichiers uploadés par Strapi (images, documents, etc.) + +### Lister les volumes + +```bash +docker volume ls | grep wx-refonte +``` + +### Sauvegarde complète + +```bash +# Créer un dossier de backup +mkdir -p ./backups + +# Sauvegarder PostgreSQL +docker-compose exec postgres pg_dump -U strapi strapi > ./backups/postgres_$(date +%Y%m%d_%H%M%S).sql + +# Sauvegarder les uploads +docker run --rm \ + -v wx-refonte-site_strapi_uploads:/data \ + -v $(pwd)/backups:/backup \ + alpine tar czf /backup/uploads_$(date +%Y%m%d_%H%M%S).tar.gz -C /data . +``` + +### Restauration + +```bash +# Restaurer PostgreSQL +docker-compose exec -T postgres psql -U strapi strapi < ./backups/postgres_YYYYMMDD_HHMMSS.sql + +# Restaurer les uploads +docker run --rm \ + -v wx-refonte-site_strapi_uploads:/data \ + -v $(pwd)/backups:/backup \ + alpine sh -c "cd /data && tar xzf /backup/uploads_YYYYMMDD_HHMMSS.tar.gz" +``` + +## 🔧 Dépannage + +### Le seed échoue avec "backup folder could not be created" + +**Solution :** Vous avez oublié de commenter le volume `strapi_uploads` dans `docker-compose.yml` pour le premier démarrage. + +1. Arrêtez les conteneurs : `docker-compose down -v` +2. Commentez le volume dans `docker-compose.yml` +3. Relancez : `docker-compose up --build` + +### Les conteneurs ne démarrent pas + +1. Vérifiez les logs : + ```bash + docker-compose logs + ``` + +2. Vérifiez l'état des conteneurs : + ```bash + docker-compose ps + ``` + +3. Redémarrez avec reconstruction : + ```bash + docker-compose down + docker-compose up --build + ``` + +### Erreur de connexion à PostgreSQL + +- Vérifiez que PostgreSQL est healthy : + ```bash + docker-compose logs postgres + docker-compose ps + ``` + +- Testez la connexion : + ```bash + docker-compose exec postgres pg_isready -U strapi -d strapi + ``` + +- Attendez que PostgreSQL soit complètement démarré (health check) + +### Problèmes avec Strapi + +- Vérifiez les variables d'environnement dans `.env` +- Assurez-vous que PostgreSQL est accessible +- Pour les erreurs de seed, vérifiez les logs détaillés : + ```bash + docker-compose logs strapi | grep -i error + ``` + +### Problèmes avec Next.js + +- Vérifiez que Strapi est accessible : + ```bash + curl http://localhost:1337/api + ``` + +- Rebuild Next.js : + ```bash + docker-compose build nextjs && docker-compose up -d nextjs + ``` + +### Nettoyer complètement + +⚠️ **Attention : supprime toutes les données !** + +```bash +# Arrêter et supprimer les conteneurs + volumes +docker-compose down -v + +# Supprimer les images +docker-compose down --rmi all + +# Nettoyer le cache Docker +docker system prune -f +``` + +### Réimporter les données depuis le début + +Si vous devez recommencer l'import : + +```bash +# 1. Tout nettoyer +docker-compose down -v + +# 2. Modifier .env +echo "SEED_DB=true" >> .env + +# 3. Commenter le volume dans docker-compose.yml +# (voir section "Premier Démarrage") + +# 4. Reconstruire et démarrer +docker-compose up --build +``` + +## 📊 Monitoring + +### Ressources utilisées + +```bash +# Voir l'utilisation des ressources en temps réel +docker stats + +# Espace disque utilisé par Docker +docker system df + +# Voir les volumes et leur taille +docker system df -v +``` + +### Health checks + +PostgreSQL a un health check intégré. Pour vérifier : + +```bash +docker-compose ps +# Cherchez "healthy" dans la colonne STATUS +``` + +## 🚀 Déploiement en production + +### Variables d'environnement de production + +```env +NODE_ENV=production +SEED_DB=false # TOUJOURS false en production +POSTGRES_PASSWORD=votre_mot_de_passe_prod_tres_fort +DATABASE_SSL=true # Si votre provider PostgreSQL le supporte +STRAPI_DISABLE_TELEMETRY=true +``` + +### Checklist avant production + +- [ ] `SEED_DB=false` configuré +- [ ] Volume `strapi_uploads` activé dans docker-compose.yml +- [ ] Mots de passe forts dans `.env` +- [ ] `.env` dans `.gitignore` +- [ ] Backups automatisés configurés +- [ ] Health checks activés +- [ ] Monitoring en place + +### Optimisations + +- Utilisez des images multi-stage (déjà configuré) +- Configurez des limites de ressources dans docker-compose.yml : + ```yaml + deploy: + resources: + limits: + cpus: '1' + memory: 1G + ``` +- Utilisez un reverse proxy (nginx/Traefik) pour Next.js et Strapi +- Activez HTTPS avec Let's Encrypt + +## 📚 Ressources + +- [Documentation Docker](https://docs.docker.com) +- [Documentation Docker Compose](https://docs.docker.com/compose/) +- [Documentation Strapi](https://docs.strapi.io) +- [Documentation Next.js](https://nextjs.org/docs) + +## 🤝 Support + +Pour des problèmes spécifiques : +1. Consultez les logs détaillés : `docker-compose logs -f` +2. Vérifiez la configuration `.env` +3. Testez les connexions entre services +4. Consultez ce README pour les cas spécifiques (premier démarrage vs normal) +5. Ouvrez une issue sur le repository GitHub + +## 📋 Changelog + +### Version actuelle +- ✅ Support du seed automatique au premier démarrage +- ✅ Gestion des permissions pour l'import Strapi +- ✅ Documentation complète pour premier démarrage vs démarrages suivants +- ✅ Volumes persistants pour PostgreSQL et uploads Strapi \ No newline at end of file diff --git a/diagnose-timeout.sh b/diagnose-timeout.sh new file mode 100755 index 00000000..4b631f6c --- /dev/null +++ b/diagnose-timeout.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +# Script de diagnostic pour Gateway Timeout Traefik + Strapi + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} DIAGNOSTIC GATEWAY TIMEOUT - TRAEFIK + STRAPI${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n" + +# 1. Vérifier que les conteneurs tournent +echo -e "${YELLOW}1. État des conteneurs${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +docker ps --filter "name=wx-refonte-site" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +echo "" + +# 2. Vérifier les logs Strapi +echo -e "${YELLOW}2. Logs Strapi (dernières 50 lignes)${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +docker logs --tail 50 wx-refonte-site-strapi 2>&1 | grep -v "node_modules" | tail -20 +echo "" + +# 3. Vérifier si Strapi écoute sur le port +echo -e "${YELLOW}3. Strapi écoute-t-il sur le port 1337 ?${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +docker exec wx-refonte-site-strapi netstat -tlnp 2>/dev/null | grep 1337 || \ +docker exec wx-refonte-site-strapi ss -tlnp 2>/dev/null | grep 1337 || \ +echo -e "${RED}⚠️ Impossible de vérifier (netstat/ss non disponible)${NC}" +echo "" + +# 4. Test de connexion interne +echo -e "${YELLOW}4. Test de connexion interne (depuis le conteneur)${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +docker exec wx-refonte-site-strapi wget -q -O- http://localhost:1337/_health 2>&1 || \ +docker exec wx-refonte-site-strapi curl -s http://localhost:1337/_health 2>&1 || \ +echo -e "${RED}⚠️ Pas de réponse sur localhost:1337${NC}" +echo "" + +# 5. Vérifier les labels Traefik +echo -e "${YELLOW}5. Labels Traefik du conteneur Strapi${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +docker inspect wx-refonte-site-strapi --format='{{range $key, $value := .Config.Labels}}{{$key}}={{$value}}{{println}}{{end}}' | grep traefik +echo "" + +# 6. Vérifier le réseau +echo -e "${YELLOW}6. Réseaux du conteneur Strapi${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +docker inspect wx-refonte-site-strapi --format='{{range $net, $config := .NetworkSettings.Networks}}{{$net}}: {{$config.IPAddress}}{{println}}{{end}}' +echo "" + +# 7. Vérifier que Traefik peut voir Strapi +echo -e "${YELLOW}7. Traefik voit-il le service Strapi ?${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +if docker ps | grep -q traefik; then + TRAEFIK_CONTAINER=$(docker ps --filter "name=traefik" --format "{{.Names}}" | head -1) + echo "Conteneur Traefik: $TRAEFIK_CONTAINER" + + # Vérifier si Traefik peut pinger Strapi + docker exec $TRAEFIK_CONTAINER ping -c 2 wx-refonte-site-strapi 2>&1 || \ + echo -e "${RED}⚠️ Traefik ne peut pas joindre Strapi${NC}" +else + echo -e "${RED}⚠️ Aucun conteneur Traefik trouvé${NC}" +fi +echo "" + +# 8. Vérifier les logs Traefik +echo -e "${YELLOW}8. Logs Traefik (recherche d'erreurs Strapi)${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +if docker ps | grep -q traefik; then + TRAEFIK_CONTAINER=$(docker ps --filter "name=traefik" --format "{{.Names}}" | head -1) + docker logs --tail 100 $TRAEFIK_CONTAINER 2>&1 | grep -i "strapi\|wx-refonte\|preprod-api" | tail -10 +else + echo -e "${RED}⚠️ Aucun conteneur Traefik trouvé${NC}" +fi +echo "" + +# 9. Test DNS +echo -e "${YELLOW}9. Résolution DNS dans le réseau Docker${NC}" +echo -e "${YELLOW}─────────────────────────────────────────────────────────${NC}" +docker exec wx-refonte-site-strapi nslookup wx-refonte-site-strapi 2>&1 || \ +docker exec wx-refonte-site-strapi getent hosts wx-refonte-site-strapi 2>&1 || \ +echo -e "${YELLOW}⚠️ Outils DNS non disponibles${NC}" +echo "" + +# 10. Résumé et recommandations +echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} RÉSUMÉ ET RECOMMANDATIONS${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n" + +# Vérifier si Strapi est vraiment démarré +if docker logs wx-refonte-site-strapi 2>&1 | grep -q "Server started"; then + echo -e "${GREEN}✅ Strapi semble démarré${NC}" +else + echo -e "${RED}❌ Strapi ne semble pas démarré complètement${NC}" + echo -e "${YELLOW} → Vérifiez les logs complets: docker compose logs -f strapi${NC}" +fi + +# Vérifier le réseau Traefik +if docker network ls | grep -q traefik-platform-network; then + echo -e "${GREEN}✅ Réseau traefik-platform-network existe${NC}" + + # Vérifier que Strapi est sur ce réseau + if docker inspect wx-refonte-site-strapi --format='{{range $net, $config := .NetworkSettings.Networks}}{{$net}}{{println}}{{end}}' | grep -q traefik-platform-network; then + echo -e "${GREEN}✅ Strapi est connecté au réseau Traefik${NC}" + else + echo -e "${RED}❌ Strapi N'EST PAS sur le réseau traefik-platform-network${NC}" + echo -e "${YELLOW} → Solution: docker network connect traefik-platform-network wx-refonte-site-strapi${NC}" + fi +else + echo -e "${RED}❌ Réseau traefik-platform-network n'existe pas${NC}" + echo -e "${YELLOW} → Créez-le: docker network create traefik-platform-network${NC}" +fi + +echo "" +echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..f4ab4cef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,146 @@ +name: wx-2026 + +services: + postgres: + image: postgres:17-alpine + container_name: wx-refonte-site-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - wx-refonte-site-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + strapi: + build: + context: ./strapi + dockerfile: Dockerfile + container_name: wx-refonte-site-strapi + restart: unless-stopped + environment: + # Database Configuration + DATABASE_CLIENT: postgres + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB} + DATABASE_USERNAME: ${POSTGRES_USER} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_SSL: ${DATABASE_SSL} + + # Server Configuration + HOST: 0.0.0.0 + PORT: 1337 + NODE_ENV: production + STRAPI_DISABLE_TELEMETRY: ${STRAPI_DISABLE_TELEMETRY} + + # ========================================== + # IMPORT SQL CONFIGURATION + # ========================================== + # Active l'import SQL automatique au démarrage + IMPORT_SQL: ${IMPORT_SQL:-true} + + # Force le réimport même si la base existe déjà + # ⚠️ DANGER: Mettre à true efface toute la base existante + FORCE_IMPORT: ${FORCE_IMPORT:-false} + + # Chemin vers le fichier SQL dans le conteneur + SQL_FILE: /opt/app/data/strapi_backup.sql + + # Legacy seed (ne pas utiliser avec IMPORT_SQL) + SEED_DB: false + + depends_on: + postgres: + condition: service_healthy + + volumes: + # ========================================== + # VOLUMES IMPORTANTS + # ========================================== + + # 1. Dossier data contenant: + # - strapi_backup.sql (dump PostgreSQL) + # - uploads.tar.gz (archive des médias) + # - OU export_20250116105447.tar.gz (export Strapi complet) + - ./strapi/data:/opt/app/data:ro + + # 2. Uploads persistants (géré par Strapi) + - strapi_uploads:/opt/app/public/uploads + + networks: + - wx-refonte-site-network + - traefik-platform-network + + expose: + - 1337 + + labels: + - "traefik.enable=true" + + # Router + - "traefik.http.routers.wx-refonte-site-strapi.rule=Host(`preprod-api.webtinix.com`)" + - "traefik.http.routers.wx-refonte-site-strapi.entrypoints=websecure" + - "traefik.http.routers.wx-refonte-site-strapi.tls.certresolver=le" + + # Service + - "traefik.http.services.wx-refonte-site-strapi.loadbalancer.server.port=1337" + + # Sécurité + - "traefik.http.middlewares.wx-refonte-site-strapi-secure.headers.sslredirect=true" + - "traefik.http.middlewares.wx-refonte-site-strapi-secure.headers.stsSeconds=31536000" + - "traefik.http.middlewares.wx-refonte-site-strapi-secure.headers.stsIncludeSubdomains=true" + - "traefik.http.routers.wx-refonte-site-strapi.middlewares=wx-refonte-site-strapi-secure" + + nextjs: + build: + context: ./next + dockerfile: Dockerfile + container_name: wx-refonte-site-nextjs + restart: unless-stopped + environment: + STRAPI_URL: http://strapi:1337 + NODE_ENV: production + depends_on: + - strapi + networks: + - wx-refonte-site-network + - traefik-platform-network + expose: + - 3000 + + labels: + - "traefik.enable=true" + + # Router + - "traefik.http.routers.wx-refonte-site-nextjs.rule=Host(`preprod.webtinix.com`)" + - "traefik.http.routers.wx-refonte-site-nextjs.entrypoints=websecure" + - "traefik.http.routers.wx-refonte-site-nextjs.tls.certresolver=le" + + # Service + - "traefik.http.services.wx-refonte-site-nextjs.loadbalancer.server.port=3000" + + # Sécurité + - "traefik.http.middlewares.wx-refonte-site-nextjs-secure.headers.sslredirect=true" + - "traefik.http.middlewares.wx-refonte-site-nextjs-secure.headers.stsSeconds=31536000" + - "traefik.http.middlewares.wx-refonte-site-nextjs-secure.headers.stsIncludeSubdomains=true" + - "traefik.http.routers.wx-refonte-site-nextjs.middlewares=wx-refonte-site-nextjs-secure" + +volumes: + postgres_data: + driver: local + strapi_uploads: + driver: local + +networks: + wx-refonte-site-network: + driver: bridge + traefik-platform-network: + external: true \ No newline at end of file diff --git a/docker-compose.yml.bak b/docker-compose.yml.bak new file mode 100644 index 00000000..f9551acf --- /dev/null +++ b/docker-compose.yml.bak @@ -0,0 +1,77 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: wx-refonte-site-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - wx-refonte-site-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + strapi: + build: + context: ./strapi + dockerfile: Dockerfile + container_name: wx-refonte-site-strapi + restart: unless-stopped + environment: + DATABASE_CLIENT: postgres + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB} + DATABASE_USERNAME: ${POSTGRES_USER} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_SSL: ${DATABASE_SSL} + HOST: 0.0.0.0 + PORT: 1337 + NODE_ENV: production + STRAPI_DISABLE_TELEMETRY: ${STRAPI_DISABLE_TELEMETRY} + SEED_DB: ${SEED_DB} + depends_on: + postgres: + condition: service_healthy + # NE PAS MONTER DE VOLUME SUR /opt/app/public + # Strapi doit gérer ce dossier lui-même pour l'import + # volumes: + # - strapi_uploads:/opt/app/public/uploads + networks: + - wx-refonte-site-network + ports: + - "${STRAPI_PORT}:1337" + + nextjs: + build: + context: ./next + dockerfile: Dockerfile + container_name: wx-refonte-site-nextjs + restart: unless-stopped + environment: + # Mettre à jour les variables du projet Next.js si nécessaire + # NEXT_PUBLIC_API_URL: http://${STRAPI_HOST}:${STRAPI_PORT} + STRAPI_URL: http://${STRAPI_HOST}:${STRAPI_PORT} + NODE_ENV: production + depends_on: + - strapi + networks: + - wx-refonte-site-network + ports: + - "${NEXTJS_PORT}:3000" + +volumes: + postgres_data: + strapi_uploads: # Commenté car on ne l'utilise plus pour le moment + +networks: + wx-refonte-site-network: + driver: bridge \ No newline at end of file diff --git a/next/.env.example b/next/.env.example index 67f9aedc..139e9cd2 100644 --- a/next/.env.example +++ b/next/.env.example @@ -3,3 +3,11 @@ PORT=3000 NEXT_PUBLIC_API_URL=http://localhost:1337 PREVIEW_SECRET=tobemodified +NEXT_PUBLIC_API_URL_IMAGE=http://localhost:1337 + +IMAGE_HOSTNAME=localhost + +# Vtiger (ces variables NE doivent PAS être préfixées par NEXT_PUBLIC_ pour la sécurité) +VTIGER_URL=https://your-vtiger-instance.com +VTIGER_USERNAME=your_vtiger_username +VTIGER_ACCESS_KEY=your_vtiger_access_key \ No newline at end of file diff --git a/next/.gitignore b/next/.gitignore index 5257cf12..a0439165 100644 --- a/next/.gitignore +++ b/next/.gitignore @@ -38,4 +38,5 @@ next-env.d.ts # env vars .env -.contentlayer \ No newline at end of file +.contentlayer +yarn.lock \ No newline at end of file diff --git a/next/Dockerfile b/next/Dockerfile new file mode 100644 index 00000000..5b4e92ec --- /dev/null +++ b/next/Dockerfile @@ -0,0 +1,51 @@ +# Multi-stage Dockerfile for Next.js (Node 22) +FROM node:22-bullseye-slim AS builder +WORKDIR /app + +# Install build dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential python3 ca-certificates git curl libvips-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy package files +COPY package*.json ./ +COPY yarn.lock ./ + +# Enable Corepack and install ALL dependencies +RUN corepack enable \ + && corepack prepare yarn@stable --activate \ + && yarn install --immutable + +# Copy source and build +COPY . . +RUN yarn build + +# Production stage +FROM node:22-bullseye-slim AS runner +WORKDIR /app + +# Runtime dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates libvips42 \ + && rm -rf /var/lib/apt/lists/* + +# Enable Corepack +RUN corepack enable && corepack prepare yarn@stable --activate + +# Copy package files +COPY package*.json ./ +COPY yarn.lock ./ + +# Install production dependencies (nouvelle syntaxe Yarn v2+) +RUN yarn workspaces focus --production + +# Copy built files from builder +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/next.config.mjs ./next.config.mjs + +ENV NODE_ENV=production +EXPOSE 3000 + +CMD ["yarn", "start"] \ No newline at end of file diff --git a/next/app/Providers.tsx b/next/app/Providers.tsx new file mode 100644 index 00000000..a2980a51 --- /dev/null +++ b/next/app/Providers.tsx @@ -0,0 +1,49 @@ +// app/Providers.tsx +"use client"; + + +import { ThemeProvider } from "next-themes"; +import { PropsWithChildren, useState } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +// import { NuqsAdapter } from "nuqs/adapters/next/app"; +import { Toaster } from "@/components/ui/sonner"; + + +export const Providers = ({ children }: PropsWithChildren) => { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + gcTime: 5 * 60 * 1000, // 5 minutes + retry: (failureCount, error) => { + // Ne pas retry sur les erreurs 404 + if (error instanceof Error && error.message.includes('404')) { + return false; + } + return failureCount < 2; + }, + refetchOnWindowFocus: false, + }, + }, + }) + ); + + return ( + + + + {/* */} + {children} + {/* */} + + + + + ); +}; diff --git a/next/app/[locale]/(marketing)/[slug]/page.tsx b/next/app/[locale]/(marketing)/[slug]/page.tsx index e3ed68e5..e8859971 100644 --- a/next/app/[locale]/(marketing)/[slug]/page.tsx +++ b/next/app/[locale]/(marketing)/[slug]/page.tsx @@ -18,7 +18,7 @@ export async function generateMetadata(props: { }, }); - const seo = pageData.seo; + const seo = pageData?.seo || {}; const metadata = generateMetadataObject(seo); return metadata; } @@ -36,13 +36,14 @@ export default async function Page(props: { }, }); - const localizedSlugs = pageData.localizations?.reduce( + const localizedSlugs = pageData?.localizations?.reduce( (acc: Record, localization: any) => { acc[localization.locale] = localization.slug; return acc; }, { [params.locale]: params.slug } ); + console.log(pageData); return ( <> diff --git a/next/app/[locale]/(marketing)/page.tsx b/next/app/[locale]/(marketing)/page.tsx index ca816cb8..8dc804ca 100644 --- a/next/app/[locale]/(marketing)/page.tsx +++ b/next/app/[locale]/(marketing)/page.tsx @@ -38,13 +38,15 @@ export default async function HomePage(props: { }, }); - const localizedSlugs = pageData.localizations?.reduce( + const localizedSlugs = pageData?.localizations?.reduce( (acc: Record, localization: any) => { acc[localization.locale] = ''; return acc; }, { [params.locale]: '' } ); + + return ( <> diff --git a/next/app/[locale]/(marketing)/services/[slug]/page.tsx b/next/app/[locale]/(marketing)/services/[slug]/page.tsx new file mode 100644 index 00000000..3b226121 --- /dev/null +++ b/next/app/[locale]/(marketing)/services/[slug]/page.tsx @@ -0,0 +1,53 @@ +import { Metadata } from 'next'; +import { redirect } from 'next/navigation'; + +import { Container } from '@/components/container'; +import { AmbientColor } from '@/components/decorations/ambient-color'; +import DynamicZoneManager from '@/components/dynamic-zone/manager'; +import { SingleProduct } from '@/components/products/single-product'; +import { generateMetadataObject } from '@/lib/shared/metadata'; +import { fetchCollectionType } from '@/lib/strapi'; +import type { Product } from '@/types/types'; + +export async function generateMetadata(props: { + params: Promise<{ locale: string; slug: string }>; +}): Promise { + const params = await props.params; + + const [pageData] = await fetchCollectionType('products', { + filters: { slug: { $eq: params.slug } }, + }); + + const seo = pageData; + const metadata = generateMetadataObject(seo); + return metadata; +} + +export default async function SingleProductPage(props: { + params: Promise<{ slug: string; locale: string }>; +}) { + const params = await props.params; + + const [pageData] = await fetchCollectionType('products', { + filters: { slug: { $eq: params.slug } }, + }); + + if (!pageData) { + redirect('/products'); + } + + return ( +
+ + + + {pageData?.dynamic_zone && ( + + )} + +
+ ); +} diff --git a/next/app/[locale]/(marketing)/services/page.tsx b/next/app/[locale]/(marketing)/services/page.tsx new file mode 100644 index 00000000..82209db9 --- /dev/null +++ b/next/app/[locale]/(marketing)/services/page.tsx @@ -0,0 +1,54 @@ +import { Metadata } from 'next'; + +import ClientSlugHandler from '../ClientSlugHandler'; +import PageContent from '@/lib/shared/PageContent'; +import { generateMetadataObject } from '@/lib/shared/metadata'; +import { fetchCollectionType } from '@/lib/strapi'; + +export async function generateMetadata(props: { + params: Promise<{ locale: string; slug: string }>; +}): Promise { + const params = await props.params; + const [pageData] = await fetchCollectionType('pages', { + filters: { + slug: { + $eq: 'services', + }, + locale: params.locale, + }, + }); + + const seo = pageData?.seo || {}; + const metadata = generateMetadataObject(seo); + return metadata; +} + +export default async function Page(props: { + params: Promise<{ locale: string; slug: string }>; +}) { + const params = await props.params; + const [pageData] = await fetchCollectionType('pages', { + filters: { + slug: { + $eq: 'services', + }, + locale: params.locale, + }, + }); + + const localizedSlugs = pageData?.localizations?.reduce( + (acc: Record, localization: any) => { + acc[localization.locale] = localization.slug; + return acc; + }, + { [params.locale]: 'services' } + ); +// console.log(pageData); + + return ( + <> + + + + ); +} diff --git a/next/app/[locale]/layout.tsx b/next/app/[locale]/layout.tsx index 18b042d0..c3635ca2 100644 --- a/next/app/[locale]/layout.tsx +++ b/next/app/[locale]/layout.tsx @@ -51,17 +51,41 @@ export default async function LocaleLayout(props: {
- - {children} + {/* Navbar full width */} + + + {/* + Main content with max-width constraint + max-w-6xl → 1152px (recommandé pour sites compacts) + max-w-7xl → 1280px (standard, équilibré) ✅ + max-w-[1440px] → 1440px (pour sites larges) + max-w-[1600px] → 1600px (très large) + */} +
+ {/*
*/} +
+ {children} +
+
+ + {/* Footer full width */}
- - {isDraftMode && } + + {/* + {isDraftMode && } */}
- + ); -} +} \ No newline at end of file diff --git a/next/app/api/vtiger-contact/route.ts b/next/app/api/vtiger-contact/route.ts new file mode 100644 index 00000000..82608a4b --- /dev/null +++ b/next/app/api/vtiger-contact/route.ts @@ -0,0 +1,82 @@ +// app/api/vtiger-contact/route.ts +/** + * Route API pour gérer les soumissions de formulaires vers Vtiger + * Ce fichier doit être créé dans: app/api/vtiger-contact/route.ts + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { vtigerService } from '@/src/services/api/vtiger-service'; + +export async function POST(request: NextRequest) { + try { + const { formData, moduleType, mapping } = await request.json(); + + if (!formData) { + return NextResponse.json( + { success: false, error: 'Données du formulaire manquantes' }, + { status: 400 } + ); + } + + // Mapper les données selon la configuration Strapi + const vtigerData = mapping + ? vtigerService.mapFormDataToVtiger(formData, mapping) + : formData; + + // Créer le contact dans Vtiger + const result = await vtigerService.createContact( + vtigerData, + moduleType || 'Leads' + ); + + return NextResponse.json({ + success: true, + data: result, + message: 'Formulaire envoyé avec succès', + }); + } catch (error) { + console.error('API Error:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Erreur lors de la soumission', + }, + { status: 500 } + ); + } +} + +export async function PUT(request: NextRequest) { + try { + const { id, formData, mapping } = await request.json(); + + if (!id) { + return NextResponse.json( + { success: false, error: 'ID manquant' }, + { status: 400 } + ); + } + + // Mapper les données + const vtigerData = mapping + ? vtigerService.mapFormDataToVtiger(formData, mapping) + : formData; + + const result = await vtigerService.updateContact(id, vtigerData); + + return NextResponse.json({ + success: true, + data: result, + message: 'Formulaire mis à jour avec succès', + }); + } catch (error) { + console.error('API Error:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour', + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/next/app/favicon.ico b/next/app/favicon.ico index 718d6fea..c9105f0b 100644 Binary files a/next/app/favicon.ico and b/next/app/favicon.ico differ diff --git a/next/app/globals.css b/next/app/globals.css index 6c74b365..d2d014ab 100644 --- a/next/app/globals.css +++ b/next/app/globals.css @@ -1,24 +1,529 @@ -@import './prism.css'; -@tailwind base; -@tailwind components; -@tailwind utilities; +@import './prism.css' layer(base); +@import 'tailwindcss'; +/* + ---break--- +*/ +@custom-variant dark (&:is(.dark *)); -:root { - --foreground-rgb: 255, 255, 255; +@theme { + --color-charcoal: #08090a; + --color-lightblack: #1c1c1c; + --color-muted: var(--neutral-200); + + --shadow-derek: + 0px 0px 0px 1px rgb(0 0 0 / 0.06), 0px 1px 1px -0.5px rgb(0 0 0 / 0.06), + 0px 3px 3px -1.5px rgb(0 0 0 / 0.06), 0px 6px 6px -3px rgb(0 0 0 / 0.06), + 0px 12px 12px -6px rgb(0 0 0 / 0.06), 0px 24px 24px -12px rgb(0 0 0 / 0.06); + --shadow-aceternity: + 0px 2px 3px -1px rgba(0, 0, 0, 0.1), 0px 1px 0px 0px rgba(25, 28, 33, 0.02), + 0px 0px 0px 1px rgba(25, 28, 33, 0.08); + + --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops)); + --background-image-gradient-conic: conic-gradient( + from 180deg at 50% 50%, + var(--tw-gradient-stops) + ); + + --animate-move: move 5s linear infinite; + --animate-spin-circle: spin-circle 3s linear infinite; + + @keyframes move { + 0% { + transform: translateX(-200px); + } + 100% { + transform: translateX(200px); + } + } + @keyframes spin-circle { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); } } -body { - color: rgb(var(--foreground-rgb)); +@utility text-balance { + text-wrap: balance; } @layer utilities { - .text-balance { - text-wrap: balance; + :root { + --foreground-rgb: 27, 27, 27; + } + + @media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + } + } + + body { + color: rgb(var(--foreground-rgb)); + } +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); + + /* custom */ + --color-tertiare: var(--tertiare); +} + +:root { + /* Backgrounds - Tons clairs et neutres */ + --background: oklch(0.99 0.002 0); /* Blanc cassé très léger */ + --foreground: oklch(0.25 0.01 264); /* #1B1B1B approximatif en OKLCH */ + + /* Cards - Blanc pur avec bon contraste */ + --card: oklch(1 0 0); /* Blanc pur */ + --card-foreground: oklch(0.25 0.01 264); /* Texte sombre */ + + /* Popover */ + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.25 0.01 264); + + /* Primary - Bleu violet (inchangé) */ + --primary: oklch(0.3886 0.1774 261.82); + --primary-foreground: oklch(0.99 0 0); /* Blanc pour contraste */ + + /* Secondary - Orange/Corail (inchangé) */ + --secondary: oklch(0.646 0.222 41.116); + --secondary-foreground: oklch(0.99 0 0); /* Blanc pour contraste */ + + /* Tertiaire - Lavande très clair */ + --tertiare: oklch(0.97 0.014 254.6); + --tertiare-foreground: oklch(0.25 0.01 264); /* Texte sombre */ + + /* Muted - Gris clair pour backgrounds secondaires */ + --muted: oklch(0.96 0.005 264); + --muted-foreground: oklch(0.50 0.01 264); /* Gris moyen pour texte */ + + /* Accent - Nuance de primary plus claire */ + --accent: oklch(0.95 0.02 214); + --accent-foreground: oklch(0.25 0.01 264); + + /* Destructive - Rouge avec bon contraste */ + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.99 0 0); + + /* Borders et inputs */ + --border: oklch(0.90 0.005 264); /* Bordures légères */ + --input: oklch(0.95 0.005 264); + --ring: oklch(0.39 0.21 214); /* Même que primary pour cohérence */ + + /* Charts - Palette harmonisée avec primary */ + --chart-1: oklch(0.81 0.1 252); + --chart-2: oklch(0.62 0.19 260); + --chart-3: oklch(0.55 0.22 263); + --chart-4: oklch(0.49 0.22 264); + --chart-5: oklch(0.42 0.18 266); + + /* Sidebar */ + --sidebar: oklch(0.99 0.002 264); + --sidebar-foreground: oklch(0.25 0.01 264); + --sidebar-primary: oklch(0.39 0.21 214); + --sidebar-primary-foreground: oklch(0.99 0 0); + --sidebar-accent: oklch(0.96 0.005 264); + --sidebar-accent-foreground: oklch(0.25 0.01 264); + --sidebar-border: oklch(0.90 0.005 264); + --sidebar-ring: oklch(0.39 0.21 214); + + /* Fonts - Outfit comme police principale */ + --font-sans: + 'Outfit', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace; + + --radius: 0.625rem; + + /* Shadows */ + --shadow-x: 0; + --shadow-y: 1px; + --shadow-blur: 3px; + --shadow-spread: 0px; + --shadow-opacity: 0.1; + --shadow-color: oklch(0.25 0.01 264); + --shadow-2xs: 0 1px 2px 0px hsl(264 10% 25% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(264 10% 25% / 0.08); + --shadow-sm: + 0 1px 3px 0px hsl(264 10% 25% / 0.1), 0 1px 2px -1px hsl(264 10% 25% / 0.1); + --shadow: 0 1px 3px 0px hsl(264 10% 25% / 0.1), 0 1px 2px -1px hsl(264 10% 25% / 0.1); + --shadow-md: + 0 2px 4px 0px hsl(264 10% 25% / 0.1), 0 2px 4px -1px hsl(264 10% 25% / 0.1); + --shadow-lg: + 0 4px 6px 0px hsl(264 10% 25% / 0.1), 0 4px 6px -2px hsl(264 10% 25% / 0.1); + --shadow-xl: + 0 8px 10px 0px hsl(264 10% 25% / 0.1), 0 8px 10px -2px hsl(264 10% 25% / 0.1); + --shadow-2xl: 0 12px 24px 0px hsl(264 10% 25% / 0.15); + + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +.dark { + /* Backgrounds - Tons sombres avec légère teinte */ + --background: oklch(0.145 0.01 264); /* Fond très sombre avec nuance */ + --foreground: oklch(0.98 0.005 264); /* Blanc cassé pour le texte */ + + /* Cards - Légèrement plus clair que le background */ + --card: oklch(0.20 0.01 264); + --card-foreground: oklch(0.98 0.005 264); + + /* Popover */ + --popover: oklch(0.22 0.01 264); + --popover-foreground: oklch(0.98 0.005 264); + + /* Primary - Version plus claire et saturée pour le mode sombre */ + --primary: oklch(0.50 0.22 265); + --primary-foreground: oklch(0.99 0 0); + + /* Secondary - Garde sa vivacité */ + --secondary: oklch(0.646 0.222 41.116); + --secondary-foreground: oklch(0.99 0 0); + + /* Tertiaire */ + --tertiare: oklch(0.35 0.03 254.6); + --tertiare-foreground: oklch(0.98 0.005 264); + + /* Muted */ + --muted: oklch(0.25 0.01 264); + --muted-foreground: oklch(0.65 0.01 264); + + /* Accent */ + --accent: oklch(0.30 0.02 265); + --accent-foreground: oklch(0.98 0.005 264); + + /* Destructive */ + --destructive: oklch(0.60 0.22 27); + --destructive-foreground: oklch(0.99 0 0); + + /* Borders et inputs */ + --border: oklch(0.28 0.01 264); + --input: oklch(0.30 0.01 264); + --ring: oklch(0.50 0.22 265); + + /* Charts */ + --chart-1: oklch(0.81 0.1 252); + --chart-2: oklch(0.62 0.19 260); + --chart-3: oklch(0.55 0.22 263); + --chart-4: oklch(0.49 0.22 264); + --chart-5: oklch(0.42 0.18 266); + + /* Sidebar */ + --sidebar: oklch(0.18 0.01 264); + --sidebar-foreground: oklch(0.98 0.005 264); + --sidebar-primary: oklch(0.50 0.22 265); + --sidebar-primary-foreground: oklch(0.99 0 0); + --sidebar-accent: oklch(0.25 0.01 264); + --sidebar-accent-foreground: oklch(0.98 0.005 264); + --sidebar-border: oklch(0.28 0.01 264); + --sidebar-ring: oklch(0.50 0.22 265); + + /* Fonts */ + --font-sans: + 'Outfit', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace; + + --radius: 0.625rem; + + /* Shadows - Plus subtiles en mode sombre */ + --shadow-x: 0; + --shadow-y: 1px; + --shadow-blur: 3px; + --shadow-spread: 0px; + --shadow-opacity: 0.3; + --shadow-color: oklch(0 0 0); + --shadow-2xs: 0 1px 2px 0px hsl(0 0% 0% / 0.3); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.4); + --shadow-sm: + 0 1px 3px 0px hsl(0 0% 0% / 0.4), 0 1px 2px -1px hsl(0 0% 0% / 0.4); + --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.4), 0 1px 2px -1px hsl(0 0% 0% / 0.4); + --shadow-md: + 0 2px 4px 0px hsl(0 0% 0% / 0.4), 0 2px 4px -1px hsl(0 0% 0% / 0.4); + --shadow-lg: + 0 4px 6px 0px hsl(0 0% 0% / 0.5), 0 4px 6px -2px hsl(0 0% 0% / 0.5); + --shadow-xl: + 0 8px 10px 0px hsl(0 0% 0% / 0.5), 0 8px 10px -2px hsl(0 0% 0% / 0.5); + --shadow-2xl: 0 12px 24px 0px hsl(0 0% 0% / 0.6); +} + +/* + ---break--- +*/ + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + font-family: var(--font-sans); + } + + /* text de base de l'application */ + .font-size-base { + @apply font-extralight leading-[100%] tracking-[0%] text-[12px] sm:text-[13px] md:text-[14px] lg:text-[15px] xl:text-[16px]; + } + + /* Theme-aware scrollbar styles améliorés */ + .scrollbar { + /* Support moderne pour tous les navigateurs */ + scrollbar-width: thin; + scrollbar-color: var(--ring) var(--muted); + + /* Améliore le comportement de scroll */ + scroll-behavior: smooth; + + } + + .scrollbar::-webkit-scrollbar { + width: 0.375rem; /* Légèrement plus large pour une meilleure UX */ + height: 0.375rem; + } + + .scrollbar::-webkit-scrollbar-track { + background-color: var(--muted); + border-radius: 9999px; + margin: 0.25rem 0; /* Espace en haut et en bas */ + } + + .scrollbar::-webkit-scrollbar-thumb { + background-color: var(--ring); + border-radius: 9999px; + border: 1px solid var(--muted); + transition: all 0.2s ease; + } + + .scrollbar::-webkit-scrollbar-thumb:hover { + background-color: var(--muted-foreground); + transform: scale(1.05); + } + + .scrollbar::-webkit-scrollbar-thumb:active { + background-color: var(--primary); + } + + .scrollbar::-webkit-scrollbar-corner { + background-color: var(--muted); + } + + /* Variante fine pour les cas où on veut une scrollbar plus discrète */ + .scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: var(--ring) transparent; + } + + .scrollbar-thin::-webkit-scrollbar { + width: 0.25rem; + height: 0.25rem; + } + + .scrollbar-thin::-webkit-scrollbar-track { + background-color: transparent; + } + + .scrollbar-thin::-webkit-scrollbar-thumb { + background-color: var(--ring); + border-radius: 9999px; + opacity: 0.7; + transition: all 0.2s ease; + } + + .scrollbar-thin::-webkit-scrollbar-thumb:hover { + opacity: 1; + background-color: var(--muted-foreground); + } + + /* Animation pour un scroll plus fluide */ + @media (prefers-reduced-motion: no-preference) { + .scrollbar { + scroll-behavior: smooth; + } + } + + /* Amélioration pour les appareils tactiles */ + @media (hover: none) and (pointer: coarse) { + .scrollbar::-webkit-scrollbar { + width: 0.5rem; + } + + .scrollbar::-webkit-scrollbar-thumb { + background-color: var(--ring); + opacity: 0.8; + } + } + + /* Styles pour le focus et l'accessibilité */ + .scrollbar:focus-within::-webkit-scrollbar-thumb { + background-color: var(--primary); + outline: 2px solid var(--ring); + outline-offset: 2px; + } + + /* Classe utilitaire pour masquer la scrollbar si nécessaire */ + .scrollbar-hide { + scrollbar-width: none; + -ms-overflow-style: none; + } + + .scrollbar-hide::-webkit-scrollbar { + display: none; + } + + /* responsive scrollbar */ + +/* Variante adaptative - S'adapte automatiquement selon la taille d'écran */ +.scrollbar-responsive { + scrollbar-width: thin; + scrollbar-color: var(--ring) var(--muted); + scroll-behavior: smooth; + +} + +.scrollbar-responsive::-webkit-scrollbar { + width: 0.375rem; + height: 0.375rem; +} + +@media (min-width: 768px) { + .scrollbar-responsive::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; + } +} + +@media (min-width: 1024px) { + .scrollbar-responsive::-webkit-scrollbar { + width: 0.625rem; + height: 0.625rem; + } +} + +.scrollbar-responsive::-webkit-scrollbar-track { + background-color: var(--muted); + border-radius: 9999px; + margin: 0.25rem 0; +} + +.scrollbar-responsive::-webkit-scrollbar-thumb { + background-color: var(--ring); + border-radius: 9999px; + border: 1px solid var(--muted); + transition: all 0.2s ease; +} + +@media (min-width: 768px) { + .scrollbar-responsive::-webkit-scrollbar-thumb { + border-width: 1.5px; } } + +@media (min-width: 1024px) { + .scrollbar-responsive::-webkit-scrollbar-thumb { + border-width: 2px; + } +} + +.scrollbar-responsive::-webkit-scrollbar-thumb:hover { + background-color: var(--muted-foreground); + transform: scale(1.05); +} + +.scrollbar-responsive::-webkit-scrollbar-thumb:active { + background-color: var(--muted-foreground); +} + +.scrollbar-responsive::-webkit-scrollbar-corner { + background-color: var(--muted); +} + +h3 { + @apply scroll-m-20 font-semibold tracking-[0%] text-[24px] leading-[24px] sm:text-[28px] sm:leading-[28px] md:text-[32px] md:leading-[32px] lg:text-[36px] lg:leading-[36px] xl:text-[40px] xl:leading-[40px]; +} + +#expertise-items:last-child { + border: none; +} + +/* END layer */ +} + diff --git a/next/app/layout.tsx b/next/app/layout.tsx index 2c3f1bd8..266c69b0 100644 --- a/next/app/layout.tsx +++ b/next/app/layout.tsx @@ -1,11 +1,22 @@ -import type { Viewport } from 'next'; - -import { Locale, i18n } from '@/i18n.config'; - import './globals.css'; +import type { Viewport } from 'next'; +import { Outfit } from 'next/font/google'; + import { SlugProvider } from './context/SlugContext'; import { Preview } from '@/components/preview'; +import { Locale, i18n } from '@/i18n.config'; +import { cn } from '@/lib/utils'; +import { Providers } from './Providers'; +import { TailwindIndicator } from '@/lib/utils/TailwindIndicator'; +import { Toaster } from "sonner" + +// Configuration de la police Outfit +const outfit = Outfit({ + subsets: ['latin'], + display: 'swap', + variable: '--font-sans', +}); export const viewport: Viewport = { themeColor: [ @@ -24,11 +35,20 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - - {children} + + + + + {children} + + + ); -} +} \ No newline at end of file diff --git a/next/app/prism.css b/next/app/prism.css index dcd8a135..a9b8406d 100644 --- a/next/app/prism.css +++ b/next/app/prism.css @@ -1,5 +1,5 @@ pre[class*='language-'] { - color: theme('colors.zinc.100'); + color: var(--color-zinc-100); } .token.tag, @@ -8,7 +8,7 @@ pre[class*='language-'] { .token.selector .class, .token.selector.class, .token.function { - color: theme('colors.blue.400'); + color: var(--color-blue-400); } .token.attr-name, @@ -16,32 +16,32 @@ pre[class*='language-'] { .token.rule, .token.pseudo-class, .token.important { - color: theme('colors.slate.300'); + color: var(--color-slate-300); } .token.module { - color: theme('colors.emerald.400'); + color: var(--color-emerald-400); } .token.attr-value, .token.class, .token.string, .token.property { - color: theme('colors.teal.300'); + color: var(--color-teal-300); } .token.punctuation, .token.attr-equals { - color: theme('colors.zinc.500'); + color: var(--color-zinc-500); } .token.unit, .language-css .token.function { - color: theme('colors.sky.200'); + color: var(--color-sky-200); } .token.comment, .token.operator, .token.combinator { - color: theme('colors.zinc.400'); + color: var(--color-zinc-400); } diff --git a/next/components.json b/next/components.json index 926abbba..b7b9791c 100644 --- a/next/components.json +++ b/next/components.json @@ -1,20 +1,22 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "style": "new-york", "rsc": true, "tsx": true, "tailwind": { - "config": "tailwind.config.ts", + "config": "", "css": "app/globals.css", "baseColor": "neutral", - "cssVariables": false, + "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", - "examples": "@/components/examples", - "blocks": "@/components/blocks" - } + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} } diff --git a/next/components/InfiniteSlider.tsx b/next/components/InfiniteSlider.tsx new file mode 100644 index 00000000..04e3f61f --- /dev/null +++ b/next/components/InfiniteSlider.tsx @@ -0,0 +1,90 @@ +"use client"; + +import React, { useRef, useState } from "react"; +import { motion, useAnimationFrame, useMotionValue, useTransform } from "framer-motion"; + +interface InfiniteSliderProps { + children: React.ReactNode[]; + direction?: "horizontal" | "vertical"; + speed?: number; + speedOnHover?: number; + className?: string; + itemsVisibleOnscreen?: number; +} + +export const InfiniteSlider = ({ + children, + direction = "horizontal", + speed = 50, + speedOnHover = 20, + className = "", + itemsVisibleOnscreen = 5, +}: InfiniteSliderProps) => { + const containerRef = useRef(null); + const contentRef = useRef(null); + + // État pour la vitesse actuelle et la position + const [currentSpeed, setCurrentSpeed] = useState(speed); + const baseTranslation = useMotionValue(0); + + // On duplique les enfants pour assurer la continuité + const duplicatedChildren = [...children, ...children]; + + useAnimationFrame((time, delta) => { + if (!contentRef.current) return; + + // Calcul du déplacement basé sur le temps (delta) pour la fluidité + const moveBy = (currentSpeed * delta) / 1000; + let newTranslation = baseTranslation.get() - moveBy; + + // Calcul de la taille d'une seule série d'éléments + const contentSize = direction === "horizontal" + ? contentRef.current.scrollWidth / 2 + : contentRef.current.scrollHeight / 2; + + // Reset de la position quand on a dépassé la moitié (boucle infinie) + if (Math.abs(newTranslation) >= contentSize) { + newTranslation = 0; + } + + baseTranslation.set(newTranslation); + }); + + // Transforme la valeur brute en style CSS + const x = useTransform(baseTranslation, (v) => (direction === "horizontal" ? v : 0)); + const y = useTransform(baseTranslation, (v) => (direction === "vertical" ? v : 0)); + + return ( +
setCurrentSpeed(speedOnHover)} + onMouseLeave={() => setCurrentSpeed(speed)} + > + + {duplicatedChildren.map((child, index) => ( +
+ {child} +
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/next/components/beam/index.tsx b/next/components/beam/index.tsx index 77b19df8..0b553080 100644 --- a/next/components/beam/index.tsx +++ b/next/components/beam/index.tsx @@ -58,7 +58,7 @@ const Beam = ({
{article?.image ? ( - ) : ( @@ -60,7 +61,7 @@ export async function BlogLayout({
{children}
- {/* { value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search articles" - className="text-sm min-w-full sm:min-w-96 p-2 rounded-md bg-neutral-800 border-none focus:ring-0 focus:outline-none outline-none text-neutral-200 placeholder-neutral-400" + className="text-sm min-w-full sm:min-w-96 p-2 rounded-md bg-neutral-800 border-none focus:ring-0 focus:outline-hidden outline-hidden text-neutral-200 placeholder-neutral-400" />
diff --git a/next/components/blur-image.tsx b/next/components/blur-image.tsx index accc6f70..eb122e4d 100644 --- a/next/components/blur-image.tsx +++ b/next/components/blur-image.tsx @@ -13,7 +13,7 @@ export const BlurImage = (props: React.ComponentProps) => { setLoading(false)} diff --git a/next/components/draft-mode-banner.tsx b/next/components/draft-mode-banner.tsx index 64782d43..6b8a60ec 100644 --- a/next/components/draft-mode-banner.tsx +++ b/next/components/draft-mode-banner.tsx @@ -28,7 +28,7 @@ export function DraftModeBanner() { diff --git a/next/components/dynamic-zone/avis-clients.tsx b/next/components/dynamic-zone/avis-clients.tsx new file mode 100644 index 00000000..b372d349 --- /dev/null +++ b/next/components/dynamic-zone/avis-clients.tsx @@ -0,0 +1,74 @@ +import { BlocksRenderer } from '@strapi/blocks-react-renderer'; +import Image from 'next/image'; +import { Typography } from '../ui/typography'; +import { strapiImage } from '@/lib/strapi/strapiImage'; + +export type AvisClientProps = { + avis_clients: avis_clients[]; +}; + +type avis_clients = { + client_name: string; + entreprise_name: string; + id: number | string; + pseudo_client: string; + client_photo: client_photo; + description: any[]; + option_projet: option_projet[]; +}; + +type client_photo = { + url: string; +}; + +type option_projet = { + options: string; +}; + +export function AvisClients({ avis_clients }: AvisClientProps) { + return ( +
+
+ {avis_clients.map((el, index) => ( +
+ quote + +
+ {children} + }} + /> +
+ +
+ +
+
+ {el.client_name} +
+ +
+ + {el.client_name} + + + {el.pseudo_client} + +
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/next/components/dynamic-zone/avis-form.tsx b/next/components/dynamic-zone/avis-form.tsx new file mode 100644 index 00000000..f589215e --- /dev/null +++ b/next/components/dynamic-zone/avis-form.tsx @@ -0,0 +1,174 @@ +"use client" +import React, { useState } from 'react' +import { Typography } from '../ui/typography' +import { Card } from '../ui/card' +import { Loader2, Star } from "lucide-react" +import { Button } from '../ui/button' +import { Input } from '../ui/input' +import { Textarea } from '../ui/textarea' +import { toast } from 'sonner' + +interface AvisFormProps { + heading: string + sub_heading: string + note_experience_text: string + note_placeholder: string + input_avis: InputAVI[] + button: Button + textarea_placeholder: string +} + +interface Button { + text: string +} + +interface InputAVI { + type: string + name: string + placeholder: string +} + +export function AvisForm(props: AvisFormProps) { + const [note, setNote] = useState(0) + const [isSubmitting, setIsSubmitting] = useState(false) + const [formData, setFormData] = useState>({}) + const backendRoute = process.env.NEXT_PUBLIC_API_URL + + const handleStarClick = (starValue: number) => { + setNote(starValue) + } + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsSubmitting(true) + + // Préparation des données pour Strapi + const payload = { + data: { + ...formData, + note, + description: [ + { + type: "paragraph", + children: [ + { type: "text", text: formData.description || "" } + ], + }, + ], + } + } + + try { + const response = await fetch(`${backendRoute}/api/avis-clients`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }) + + if (response.ok) { + setNote(0) + setFormData({}) + toast.success('Merci pour votre avis !') + } else { + const errorData = await response.json(); + console.error('Erreur Strapi:', errorData); + toast.error('Une erreur est survenue lors de l\'enregistrement') + } + } catch (error) { + console.error('Erreur réseau:', error) + toast.error('Erreur de connexion au serveur') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ {props.heading} + {props.sub_heading} +
+ + +
+ {props.note_experience_text} + +
+ {[1, 2, 3, 4, 5].map((starValue) => ( + + ))} +
+ +
+ {props.input_avis.map((input, index) => + index === 0 ? ( +