Compare commits

...

27 Commits

Author SHA1 Message Date
681d5da4d1 Amélioration qualité images avec rendu CSS optimisé
- Amélioration rendu CSS des images dans PlaceCard
- Ajout propriétés imageRendering pour meilleure qualité
- Ajout loading='lazy' et fetchPriority='high'
- Script Node.js pour télécharger images haute résolution
- Guide README pour télécharger images Google Maps en HD
- APK rebuild
2025-11-24 13:17:54 +01:00
65ccd2daaf Météo dynamique pour Fakarava avec heure locale
- Intégration API Open-Meteo pour météo en temps réel
- Coordonnées Fakarava (Rotoava): -16.3167, -145.6167
- Affichage heure locale (fuseau horaire Pacific/Tahiti)
- Rafraîchissement automatique toutes les heures
- Gestion des erreurs avec valeurs par défaut
- Icônes dynamiques selon conditions météo
- APK rebuild
2025-11-24 13:10:08 +01:00
7e6b670847 Photo Google Maps pour Tumoana Markets
- Remplacement image temporaire par photo Google Maps
- Image authentique de Tumoana Markets
- APK rebuild
2025-11-23 20:26:40 +01:00
14b7405961 Mise à jour: Tumoana Markets et Snack Elda avec photos Google Maps
- Remplacement Magasin Rotoava → Tumoana Markets
- Photo Google Maps pour Snack Elda
- Mise à jour des coordonnées GPS
- Mise à jour des liens Google Maps
- APK rebuild
2025-11-23 20:22:31 +01:00
980a3dbe0c Intégration des photos réelles Google Maps pour 4 établissements
- Photos Google Maps téléchargées:
   restaurant-requin-dormeur.jpg → Snack du Requin Dormeur
   restaurant-kori-kori.jpg → Snack Kori Kori
   plage-cocotier-pk9.jpg → Plage PK 9 (Cocotier Couché)
   phare-topaka.jpg → Ancien Phare de Topaka

- Images temporaires Unsplash pour les autres lieux
- APK rebuild avec nouvelles photos authentiques
2025-11-23 20:14:48 +01:00
e693713267 Mise à jour des images avec correspondance spécifique aux activités
- Images renommées pour correspondre aux lieux spécifiques
- restaurant-requin-dormeur.jpg → Snack du Requin Dormeur
- restaurant-kori-kori.jpg → Snack Kori Kori
- plage-cocotier-pk9.jpg → Plage PK 9 (Cocotier Couché)
- phare-topaka.jpg → Ancien Phare de Topaka
- passe-nord-snorkeling.jpg → Passe Nord (Garuae)
- sables-roses.jpg → Sables Roses (Passe Sud)
- plage-pension.jpg → Plage de la Pension
- epicerie-rotoava.jpg → Magasin Rotoava
- snack-chez-elda.jpg → Snack Chez Elda
- epicerie-locale.jpg → Autres Petites Épiceries
- APK rebuild avec nouvelles images
2025-11-23 20:04:25 +01:00
3ff1b82b85 Permettre le commit de l'APK pour téléchargement distant 2025-11-23 19:56:01 +01:00
f42b09ebab Build APK avec images incluses (6.3M)
- APK généré avec toutes les images
- Restaurants, plages et épiceries avec photos
- Prêt pour test beta
2025-11-23 19:55:47 +01:00
908bf5f0c2 Ajout d'images pour les cartes de lieux (restaurants, plages, épiceries)
- Téléchargement d'images depuis Unsplash
- Images stockées dans public/images/
- Mise à jour de places.json avec chemins locaux
- Composant PlaceCard affiche maintenant les images dans l'en-tête
- Fallback automatique vers placeholder si image manquante
2025-11-23 19:52:37 +01:00
fa4ef6320a Fix: Logo visible en mode sombre avec dark:invert 2025-11-23 19:40:02 +01:00
45a96f618b Suppression de l'APK admin obsolète 2025-11-23 19:29:09 +01:00
bbfddefc76 Build APK simplifié réussi - Version finale sans admin/WiFi/bungalow 2025-11-23 19:27:15 +01:00
b10d9c515b Simplification complète de l'app - Suppression admin/WiFi/bungalow
- Suppression de toute la partie admin (routes, composants, API)
- Suppression du WiFi et du numéro de bungalow
- Simplification de l'accueil (logo, météo, message statique)
- App 100% statique maintenant
- Redirection simple vers /accueil
- Nettoyage des hooks et types inutilisés
2025-11-23 19:22:34 +01:00
29d6ab3d80 Rebuild APK client avec la logique de détection corrigée 2025-11-23 17:23:36 +01:00
429eb07291 Amélioration de la détection APK client vs admin
- Détection basée sur Capacitor Android + adminPassword
- APK client (sans adminPassword) → redirige vers /accueil
- APK admin (avec adminPassword) → redirige vers /admin/login
- Web avec /admin → admin
- Sinon → client
2025-11-23 17:21:22 +01:00
092d537180 Ajout du script build-apk-admin.sh pour générer l'APK admin
- Script dédié pour l'APK admin (compagnon-admin-debug.apk)
- Configuration Capacitor pour com.pensionmarama.admin
- Exclusion temporaire des routes API pendant le build
- Génération de l'APK admin avec les corrections pour /admin/login
2025-11-23 17:18:34 +01:00
61c1f47409 Corrections pour le build APK admin
- Ajout de updatedAt manquant dans la création de client
- Correction de l'import ThemeProvider (next-themes)
- Correction du size icon dans ThemeToggle
- Exclusion temporaire des routes API pendant le build
- Build APK admin réussi
2025-11-23 17:13:47 +01:00
0247f6ed9d Ajout du message informatif pour le mode hors ligne dans ClientList 2025-11-23 17:01:52 +01:00
ba1433b192 Correction de l'accès à /admin/login dans l'APK admin
- Détection automatique de l'app admin (Capacitor)
- Redirection automatique vers /admin/login si non connecté
- Gestion gracieuse des erreurs API en mode statique
- Message informatif si API non disponible
- Support du mode statique pour l'APK admin
2025-11-23 17:01:31 +01:00
0e485aacee Ajout du mode clair/sombre (dark mode)
- Installation de next-themes
- Création du composant ThemeToggle avec icônes soleil/lune
- Intégration du ThemeProvider dans le layout
- Ajout du toggle dans la navigation mobile et le header admin
- Adaptation des couleurs pour le dark mode (tropical chic)
- Mise à jour des composants UI (Card, Button) pour le dark mode
- Adaptation des composants principaux (Layout, WifiCard, etc.)
2025-11-23 16:53:27 +01:00
106f15205c Amélioration du composant QRCodeDisplay avec zone de texte
- Zone de texte sélectionnable pour le lien
- Boutons Copier et Sélectionner
- Alerte de confirmation
- Instructions claires pour l'utilisateur
2025-11-23 11:33:08 +01:00
dd41da5b0a Solution simplifiée pour copie du lien - zone de texte sélectionnable
- Zone de texte en lecture seule avec le lien
- Sélection automatique au clic
- Bouton 'Sélectionner' explicite
- Instructions claires pour l'utilisateur
- Plus simple et plus fiable que l'alerte
2025-11-23 11:12:30 +01:00
8b0ba77b63 Ajout d'une alerte de confirmation après copie du lien
- Alerte explicite avec le lien copié
- Feedback clair pour l'utilisateur
- Meilleur fallback pour anciens navigateurs
- Durée du feedback augmentée à 3 secondes
2025-11-23 11:07:06 +01:00
83c209117e Amélioration du bouton de copie du lien unique
- Ajout d'un feedback visuel (bouton devient vert + 'Copié !')
- Icônes Copy et Check pour plus de clarté
- Fallback pour navigateurs anciens (document.execCommand)
- Meilleur affichage du lien (plus grand, en monospace)
- Message explicatif pour l'utilisateur
2025-11-23 11:03:22 +01:00
ea2d620c7a Ajout interface admin déployable sur serveur
- API routes pour gestion clients (CRUD complet)
- Système de stockage JSON persistant (data/clients.json)
- Configuration Next.js serveur (next.config.server.js)
- Script de build pour déploiement (scripts/build-server.sh)
- Documentation complète de déploiement (ADMIN_DEPLOY.md)

Fonctionnalités admin:
- Création/modification/suppression de clients
- Génération automatique de tokens uniques
- QR codes pour configuration clients
- Authentification par mot de passe
- Backend Node.js avec API REST

Déploiement prévu: marama.syoul.fr
2025-11-23 10:57:01 +01:00
43b733b439 Amélioration du script APK et ajout du guide complet
- Script build-apk-simple.sh mis à jour avec auto-installation Java 21 + Android SDK
- GUIDE_APK.md créé avec documentation complète
- Instructions pour distribution aux bêta-testeurs
- APK prêt: dist/compagnon-lagon-beta.apk (4,5 MB)
2025-11-23 10:41:28 +01:00
621e478705 APK Android généré avec succès
- Script build-apk-simple.sh créé pour build sans Docker
- Installation SDKMAN + Java 21 requise par Capacitor
- Configuration Android SDK via cmdline-tools
- APK généré: dist/compagnon-lagon-beta.apk
- Export statique Next.js configuré (next.config.export.js)
- Taille APK: ~5MB prêt pour distribution

Pour rebuild: ./scripts/build-apk-simple.sh
2025-11-23 10:38:42 +01:00
287 changed files with 12247 additions and 1074 deletions

16
.gitignore vendored
View File

@ -35,3 +35,19 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
# android
/android/.gradle
/android/build
/android/app/build
/android/local.properties
/android/.idea
# capacitor
/ios
/android/.cxx
# data storage (ne pas commit les données clients)
/data/clients.json
# APK (commenté pour permettre le téléchargement depuis le dépôt)
# /dist/*.apk

437
ADMIN_DEPLOY.md Normal file
View File

@ -0,0 +1,437 @@
# 🖥️ Guide de déploiement de l'interface Admin
Ce guide explique comment déployer l'interface d'administration sur votre serveur **marama.syoul.fr**.
---
## 📋 Vue d'ensemble
```
┌─────────────────────┐ ┌──────────────────────────┐
│ APK Client │ │ Admin Web │
│ (Statique) │ │ marama.syoul.fr │
│ │ │ │
│ • Accueil │ │ • Login admin │
│ • Explorer │ ←────│ • Gestion clients │
│ • Mana Tracker │ │ • Génération tokens │
│ • Infos │ │ • QR codes │
└─────────────────────┘ └──────────────────────────┘
Voyageurs Gérant (vous)
```
---
## 🚀 Déploiement en 5 étapes
### Étape 1 : Build de l'application admin
```bash
cd "/home/syoul/Ccompagnon Marama"
./scripts/build-server.sh
```
Ce script va :
- ✅ Configurer Next.js en mode serveur (avec API routes)
- ✅ Build l'application
- ✅ Préparer les fichiers dans `deploy/`
- ✅ Créer les scripts de lancement
**⏱️ Durée :** 2-3 minutes
---
### Étape 2 : Transférer sur votre serveur
```bash
# Depuis votre machine locale
rsync -avz --delete deploy/ user@marama.syoul.fr:/var/www/pension-admin/
# Remplacez 'user' par votre nom d'utilisateur SSH
```
**Alternative avec SCP :**
```bash
scp -r deploy/* user@marama.syoul.fr:/var/www/pension-admin/
```
---
### Étape 3 : Configuration sur le serveur
```bash
# Se connecter au serveur
ssh user@marama.syoul.fr
# Aller dans le dossier
cd /var/www/pension-admin
# Configurer le mot de passe admin
cp .env.example .env
nano .env
```
**Contenu de `.env` :**
```bash
# ⚠️ IMPORTANT : Changez ce mot de passe !
ADMIN_PASSWORD=votre_mot_de_passe_tres_securise
# Port (optionnel)
PORT=3000
```
---
### Étape 4 : Installer et démarrer
#### Option A : Lancement simple (test)
```bash
chmod +x start.sh
./start.sh
```
L'admin sera accessible sur `http://marama.syoul.fr:3000`
#### Option B : Avec PM2 (production, recommandé ⭐)
```bash
# Installer PM2 (si pas déjà fait)
npm install -g pm2
# Démarrer l'application
pm2 start npm --name "pension-admin" -- start
# Sauvegarder la config PM2
pm2 save
# Configurer le démarrage auto
pm2 startup
# Suivre les instructions affichées
# Commandes utiles PM2
pm2 status # Voir le statut
pm2 logs pension-admin # Voir les logs
pm2 restart pension-admin # Redémarrer
pm2 stop pension-admin # Arrêter
```
---
### Étape 5 : Configuration Nginx (reverse proxy)
#### A. Créer la configuration Nginx
```bash
sudo nano /etc/nginx/sites-available/pension-admin
```
**Contenu du fichier :**
```nginx
server {
listen 80;
server_name admin.marama.syoul.fr;
# Logs
access_log /var/log/nginx/pension-admin-access.log;
error_log /var/log/nginx/pension-admin-error.log;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
#### B. Activer la configuration
```bash
# Créer le lien symbolique
sudo ln -s /etc/nginx/sites-available/pension-admin /etc/nginx/sites-enabled/
# Tester la configuration
sudo nginx -t
# Recharger Nginx
sudo systemctl reload nginx
```
#### C. Configurer SSL (HTTPS) avec Let's Encrypt
```bash
# Installer Certbot (si pas déjà fait)
sudo apt install certbot python3-certbot-nginx
# Obtenir le certificat SSL
sudo certbot --nginx -d admin.marama.syoul.fr
# Le renouvellement est automatique !
```
---
## 🔐 Configuration DNS
Ajoutez un enregistrement A ou CNAME dans votre DNS :
```
Type: A
Nom: admin.marama.syoul.fr (ou juste "admin")
Valeur: [IP de votre serveur]
TTL: 3600
```
---
## 🎯 Utilisation de l'interface admin
### 1. Se connecter
```
URL: https://admin.marama.syoul.fr
Mot de passe: celui défini dans .env
```
### 2. Ajouter un client
1. Cliquer sur **"Nouveau client"**
2. Remplir le formulaire :
- **Email** : email du voyageur
- **N° Bungalow** : 1, 2, 3, etc.
- **WiFi** : nom et mot de passe
- **Message du gérant** : message personnalisé
3. Cliquer sur **"Créer"**
4. Un **QR code** et un **lien unique** sont générés automatiquement
### 3. Partager avec le client
**Option A : QR Code** (recommandé)
- Afficher le QR code
- Le voyageur scan avec son téléphone
- L'app se configure automatiquement
**Option B : Lien**
- Copier le lien unique
- L'envoyer par email/SMS au client
---
## 🔄 Mise à jour de l'application
Quand vous modifiez le code :
```bash
# Sur votre machine locale
cd "/home/syoul/Ccompagnon Marama"
./scripts/build-server.sh
# Transférer
rsync -avz --delete deploy/ user@marama.syoul.fr:/var/www/pension-admin/
# Sur le serveur
ssh user@marama.syoul.fr
cd /var/www/pension-admin
pm2 restart pension-admin
```
---
## 📂 Structure des fichiers sur le serveur
```
/var/www/pension-admin/
├── .next/ # Application Next.js compilée
├── public/ # Assets statiques
├── data/
│ └── clients.json # Base de données des clients (créé auto)
├── package.json # Dépendances
├── next.config.js # Config Next.js
├── .env # Variables d'environnement (mot de passe)
├── start.sh # Script de lancement
└── DEPLOY.md # Documentation
```
---
## 🔒 Sécurité
### Recommandations importantes
1. **Mot de passe fort**
```bash
# Générer un mot de passe sécurisé
openssl rand -base64 32
```
2. **Permissions fichiers**
```bash
chmod 600 /var/www/pension-admin/.env
chmod 600 /var/www/pension-admin/data/clients.json
```
3. **Firewall**
```bash
# Autoriser uniquement HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
4. **Sauvegarde des données**
```bash
# Créer un cron job pour sauvegarder clients.json
0 2 * * * cp /var/www/pension-admin/data/clients.json /backup/clients-$(date +\%Y\%m\%d).json
```
---
## 🐛 Dépannage
### L'application ne démarre pas
```bash
# Vérifier les logs
pm2 logs pension-admin
# Vérifier que Node.js est installé
node --version # doit être >= 18
# Réinstaller les dépendances
cd /var/www/pension-admin
rm -rf node_modules
npm ci --production
pm2 restart pension-admin
```
### Erreur 502 Bad Gateway
```bash
# Vérifier que l'app tourne
pm2 status
# Vérifier les logs Nginx
sudo tail -f /var/log/nginx/pension-admin-error.log
# Redémarrer Nginx
sudo systemctl restart nginx
```
### Les clients ne peuvent pas se connecter
```bash
# Vérifier les permissions du fichier data
ls -la /var/www/pension-admin/data/
# Vérifier le contenu
cat /var/www/pension-admin/data/clients.json
# Si le fichier n'existe pas, l'app le créera automatiquement
```
### Changer le mot de passe admin
```bash
# Sur le serveur
cd /var/www/pension-admin
nano .env
# Modifier ADMIN_PASSWORD
# Redémarrer
pm2 restart pension-admin
```
---
## 📊 Monitoring
### Voir les statistiques avec PM2
```bash
pm2 monit # Monitoring en temps réel
pm2 logs # Voir tous les logs
pm2 status # Statut de toutes les apps
```
### Logs Nginx
```bash
# Logs d'accès
sudo tail -f /var/log/nginx/pension-admin-access.log
# Logs d'erreurs
sudo tail -f /var/log/nginx/pension-admin-error.log
```
---
## 🎓 Commandes utiles
```bash
# Redémarrer l'application
pm2 restart pension-admin
# Voir les logs en direct
pm2 logs pension-admin --lines 100
# Recharger l'app (sans downtime)
pm2 reload pension-admin
# Arrêter l'application
pm2 stop pension-admin
# Démarrer l'application
pm2 start pension-admin
# Supprimer de PM2
pm2 delete pension-admin
# Sauvegarder la config PM2
pm2 save
```
---
## 💡 Conseils
1. **Tester localement d'abord**
```bash
# Sur votre machine
npm run dev
# Ouvrir http://localhost:3000/admin
```
2. **Utiliser un sous-domaine dédié**
- `admin.marama.syoul.fr` (recommandé)
- Plutôt que `marama.syoul.fr/admin`
3. **Configurer les sauvegardes automatiques**
- Le fichier `data/clients.json` contient toutes vos données
- Sauvegardez-le régulièrement
4. **Monitoring avec Uptime Robot**
- Gratuit
- Vous alerte si le site est down
---
## 📞 Support
Si vous rencontrez des problèmes :
1. Vérifier les logs : `pm2 logs pension-admin`
2. Vérifier Nginx : `sudo nginx -t`
3. Vérifier les permissions : `ls -la data/`
---
**Version** : 1.0.0 - Novembre 2025
**Serveur** : marama.syoul.fr
**URL Admin** : https://admin.marama.syoul.fr

161
GUIDE_APK.md Normal file
View File

@ -0,0 +1,161 @@
# 📱 Guide de génération de l'APK Android
Ce guide explique comment générer l'APK de **Compagnon du Lagon** pour la distribution aux bêta-testeurs.
## 🚀 Méthode rapide (Recommandée)
### 1. Lancer le script automatisé
```bash
cd "/home/syoul/Ccompagnon Marama"
./scripts/build-apk-simple.sh
```
Le script va :
- ✅ Installer automatiquement SDKMAN si nécessaire
- ✅ Installer Java 21 (requis par Capacitor)
- ✅ Installer Android SDK (cmdline-tools)
- ✅ Build Next.js en mode export statique
- ✅ Configurer Capacitor
- ✅ Générer l'APK Android
**⏱️ Durée :** ~15-20 minutes la première fois (téléchargement SDK), puis 2-3 minutes pour les builds suivants.
### 2. Récupérer l'APK
L'APK sera disponible dans :
```
dist/compagnon-lagon-beta.apk
```
**📊 Taille :** ~4,5 MB
## 📤 Distribution aux bêta-testeurs
### Option 1 : Envoi direct du fichier
1. **Envoyer par email/messagerie**
- Envoyez le fichier `dist/compagnon-lagon-beta.apk`
- Via Email, WhatsApp, Telegram, etc.
2. **Instructions pour les testeurs**
```
1. Télécharger le fichier APK
2. Ouvrir les Paramètres Android
3. Sécurité > Activer "Sources inconnues"
4. Ouvrir le fichier APK téléchargé
5. Appuyer sur "Installer"
```
### Option 2 : Hébergement web temporaire
1. **Via transfert.sh** (gratuit, temporaire)
```bash
curl --upload-file dist/compagnon-lagon-beta.apk https://transfer.sh/compagnon.apk
```
Vous recevrez un lien à partager (valide 14 jours).
2. **Via Google Drive/Dropbox**
- Upload `dist/compagnon-lagon-beta.apk`
- Partager le lien public
- Les testeurs téléchargent et installent
### Option 3 : Serveur local (testeurs sur même réseau)
```bash
cd dist
python3 -m http.server 8080
```
Les testeurs peuvent télécharger à l'adresse :
```
http://[VOTRE_IP]:8080/compagnon-lagon-beta.apk
```
## 🔄 Mettre à jour l'APK
Pour générer une nouvelle version après des modifications :
```bash
# 1. Modifier le code
# 2. Relancer le build
./scripts/build-apk-simple.sh
# 3. L'APK sera mis à jour dans dist/
```
## 🛠️ Configuration requise (Installation automatique)
Le script installe automatiquement :
- ✅ **SDKMAN** : Gestionnaire de SDK Java
- ✅ **Java 21** : Requis par Capacitor
- ✅ **Android SDK** : Platform-tools, Build-tools 34.0.0
- ✅ **Node.js packages** : Capacitor, dépendances
**Note :** Pas besoin de sudo, tout s'installe dans `~/.sdkman` et `~/Android/Sdk`.
## 📝 Notes importantes
### Données statiques
L'APK contient toutes les données en **statique** (JSON dans `public/data/`).
Pour mettre à jour les données :
1. Modifier les fichiers JSON
2. Rebuild l'APK
3. Redistribuer la nouvelle version
### Signature APK (Debug vs Release)
**APK Debug** (actuel) :
- ✅ Parfait pour les bêta-tests
- ✅ Signature automatique
- ❌ Ne peut pas être publié sur Play Store
**APK Release** (pour production) :
```bash
# Générer un keystore
keytool -genkey -v -keystore compagnon-release.keystore \
-alias compagnon -keyalg RSA -keysize 2048 -validity 10000
# Builder en mode release
cd android
./gradlew assembleRelease
```
## 🐛 Dépannage
### Erreur "Java not found"
```bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk use java 21.0.1-tem
```
### Erreur "ANDROID_HOME not set"
```bash
export ANDROID_HOME="$HOME/Android/Sdk"
```
### Erreur "Gradle daemon stopped"
```bash
cd android
./gradlew --stop
./gradlew clean assembleDebug
```
### APK vide / sans contenu
- Vérifier que `out/` contient les fichiers HTML
- Vérifier `public/data/*.json` sont présents
- Rebuild avec `npm run build` avant Capacitor
## 📞 Support
En cas de problème, vérifier :
1. Les logs du script : `./scripts/build-apk-simple.sh`
2. Les logs Gradle : `android/build/reports/`
3. Le contenu de `out/` après build Next.js
---
**Version** : 1.0.0 - Novembre 2025
**App ID** : `com.pensionmarama.app`
**Nom** : Compagnon du Lagon - Pension Marama

101
android/.gitignore vendored Normal file
View File

@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
android/app/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

58
android/app/build.gradle Normal file
View File

@ -0,0 +1,58 @@
apply plugin: 'com.android.application'
android {
namespace "com.pensionmarama.app"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.pensionmarama.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -0,0 +1,5 @@
package com.pensionmarama.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Compagnon du Lagon</string>
<string name="title_activity_main">Compagnon du Lagon</string>
<string name="package_name">com.pensionmarama.app</string>
<string name="custom_url_scheme">com.pensionmarama.app</string>
</resources>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

29
android/build.gradle Normal file
View File

@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

25
android/gradle.properties Normal file
View File

@ -0,0 +1,25 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# Use Java 21 toolchain (required by Capacitor)
org.gradle.java.home=/home/syoul/.sdkman/candidates/java/21.0.1-tem
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
android/gradlew vendored Executable file
View File

@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

5
android/settings.gradle Normal file
View File

@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

16
android/variables.gradle Normal file
View File

@ -0,0 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}

View File

@ -1,53 +1,37 @@
"use client"; "use client";
import { Suspense } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Layout from "@/components/layout/Layout"; import Layout from "@/components/layout/Layout";
import WifiCard from "@/components/accueil/WifiCard";
import Logo from "@/components/Logo"; import Logo from "@/components/Logo";
import { useClientData } from "@/lib/hooks/useClientData"; import { config } from "@/lib/config";
const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget"), { const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget"), {
loading: () => <div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />, loading: () => <div className="h-32 bg-gray-100 dark:bg-gray-800 rounded-2xl animate-pulse" />,
ssr: false, ssr: false,
}); });
function AccueilContent() { export default function AccueilPage() {
const { bungalowNumber, gerantMessage, loading } = useClientData();
if (loading) {
return (
<Layout>
<div className="px-4 py-6 space-y-6">
<div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />
</div>
</Layout>
);
}
return ( return (
<Layout> <Layout>
<div className="px-4 py-6 space-y-6"> <div className="px-4 py-6 space-y-6">
<header className="text-center py-4"> <header className="text-center py-4">
<Logo size={140} className="mb-4" /> <Logo size={140} className="mb-4" />
<h1 className="text-2xl font-bold text-primary mb-2"> <h1 className="text-2xl font-bold text-primary dark:text-primary mb-2">
Ia Ora Na Ia Ora Na
</h1> </h1>
<p className="text-lg text-gray-700"> <p className="text-lg text-gray-700 dark:text-gray-300">
Bienvenue au Bungalow {bungalowNumber} Bienvenue à la Pension Marama
</p> </p>
</header> </header>
<WifiCard />
<WeatherWidget /> <WeatherWidget />
<section className="bg-secondary rounded-2xl p-6"> <section className="bg-secondary dark:bg-primary/20 rounded-2xl p-6">
<h2 className="text-xl font-semibold text-primary mb-3"> <h2 className="text-xl font-semibold text-primary dark:text-primary mb-3">
Le mot du gérant Le mot du gérant
</h2> </h2>
<p className="text-gray-700 leading-relaxed"> <p className="text-gray-700 dark:text-gray-300 leading-relaxed">
{gerantMessage} {config.gerantMessage}
</p> </p>
</section> </section>
</div> </div>
@ -55,17 +39,3 @@ function AccueilContent() {
); );
} }
export default function AccueilPage() {
return (
<Suspense fallback={
<Layout>
<div className="px-4 py-6 space-y-6">
<div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />
</div>
</Layout>
}>
<AccueilContent />
</Suspense>
);
}

View File

@ -1,85 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import Logo from "@/components/Logo";
export default function AdminLoginPage() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
// Vérification simple côté client (la vraie vérification se fait côté serveur)
// Pour l'instant, on stocke le mot de passe dans localStorage
localStorage.setItem("adminPassword", password);
// Test avec une requête API
try {
const response = await fetch("/api/admin/clients", {
headers: {
Authorization: `Bearer ${password}`,
},
});
if (response.ok) {
router.push("/admin");
} else {
setError("Mot de passe incorrect");
localStorage.removeItem("adminPassword");
}
} catch (err) {
setError("Erreur de connexion");
localStorage.removeItem("adminPassword");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-background flex items-center justify-center px-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<Logo size={100} />
</div>
<CardTitle>Administration</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Mot de passe
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
required
/>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm">
{error}
</div>
)}
<Button type="submit" disabled={loading} className="w-full">
{loading ? "Connexion..." : "Se connecter"}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,73 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import AdminLayout from "@/components/admin/AdminLayout";
import ClientForm from "@/components/admin/ClientForm";
import ClientList from "@/components/admin/ClientList";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { Client } from "@/lib/types/client";
export default function AdminPage() {
const [showForm, setShowForm] = useState(false);
const [editingClient, setEditingClient] = useState<Client | undefined>();
const [refreshKey, setRefreshKey] = useState(0);
const router = useRouter();
useEffect(() => {
// Vérifier si l'admin est connecté
const adminPassword = localStorage.getItem("adminPassword");
if (!adminPassword) {
router.push("/admin/login");
}
}, [router]);
const handleNewClient = () => {
setEditingClient(undefined);
setShowForm(true);
};
const handleEdit = (client: Client) => {
setEditingClient(client);
setShowForm(true);
};
const handleSuccess = () => {
setShowForm(false);
setEditingClient(undefined);
setRefreshKey((k) => k + 1);
};
const handleCancel = () => {
setShowForm(false);
setEditingClient(undefined);
};
return (
<AdminLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-primary">Gestion des clients</h2>
{!showForm && (
<Button onClick={handleNewClient}>
<Plus className="h-4 w-4 mr-2" />
Nouveau client
</Button>
)}
</div>
{showForm ? (
<ClientForm
client={editingClient}
onSuccess={handleSuccess}
onCancel={handleCancel}
/>
) : (
<ClientList onEdit={handleEdit} onRefresh={() => setRefreshKey((k) => k + 1)} />
)}
</div>
</AdminLayout>
);
}

View File

@ -4,10 +4,10 @@
@layer base { @layer base {
body { body {
@apply bg-background; @apply bg-background dark:bg-background-dark;
font-size: 16px; font-size: 16px;
min-height: 100vh; min-height: 100vh;
color: #1f2937; @apply text-foreground dark:text-foreground-dark;
} }
} }

View File

@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import PWARegister from "@/components/PWARegister"; import PWARegister from "@/components/PWARegister";
import { ThemeProvider } from "@/components/ThemeProvider";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -29,14 +30,21 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="fr"> <html lang="fr" suppressHydrationWarning>
<head> <head>
<link rel="icon" href="/logo-relais-marama.svg" type="image/svg+xml" /> <link rel="icon" href="/logo-relais-marama.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/logo-relais-marama.svg" /> <link rel="apple-touch-icon" href="/logo-relais-marama.svg" />
</head> </head>
<body className={inter.className}> <body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem={true}
disableTransitionOnChange={false}
>
{children} {children}
<PWARegister /> <PWARegister />
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -11,11 +11,10 @@ export default function Home() {
}, [router]); }, [router]);
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen bg-background dark:bg-background-dark">
<div className="text-center"> <div className="text-center">
<p className="text-gray-600">Redirection...</p> <p className="text-gray-600 dark:text-gray-400">Redirection...</p>
</div> </div>
</div> </div>
); );
} }

View File

@ -1,9 +1,12 @@
import type { CapacitorConfig } from '@capacitor/cli'; import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = { const config: CapacitorConfig = {
appId: 'com.pensionmarama.admin', appId: 'com.pensionmarama.app',
appName: 'Compagnon Admin', appName: 'Compagnon du Lagon',
webDir: 'out' webDir: 'out',
server: {
androidScheme: 'https'
}
}; };
export default config; export default config;

View File

@ -44,7 +44,7 @@ export default function Logo({ size = 120, className = "" }: LogoProps) {
alt="Relais Marama - Fakarava" alt="Relais Marama - Fakarava"
width={size} width={size}
height={size} height={size}
className="object-contain" className="object-contain dark:invert"
style={{ maxWidth: `${size}px`, maxHeight: `${size}px` }} style={{ maxWidth: `${size}px`, maxHeight: `${size}px` }}
/> />
<p className="text-primary font-semibold mt-2" style={{ fontSize: `${size * 0.15}px` }}> <p className="text-primary font-semibold mt-2" style={{ fontSize: `${size * 0.15}px` }}>

View File

@ -0,0 +1,12 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ComponentProps } from "react";
export function ThemeProvider({
children,
...props
}: ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@ -0,0 +1,45 @@
"use client";
import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<Button
variant="ghost"
size="sm"
className="h-9 w-9 rounded-full p-0"
aria-label="Changer de thème"
>
<Sun className="h-5 w-5" />
</Button>
);
}
return (
<Button
variant="ghost"
size="sm"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="h-10 w-10 rounded-full hover:bg-secondary dark:hover:bg-gray-800 p-0"
aria-label="Changer de thème"
>
{theme === "dark" ? (
<Sun className="h-5 w-5 text-primary dark:text-yellow-400" />
) : (
<Moon className="h-5 w-5 text-primary dark:text-blue-300" />
)}
</Button>
);
}

View File

@ -1,35 +1,177 @@
import { Cloud, Sun } from "lucide-react"; "use client";
import { useEffect, useState } from "react";
import { Cloud, Sun, CloudRain, Wind, Droplets, Loader2, Clock } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
interface WeatherData {
temperature: number;
condition: string;
windSpeed: number;
humidity: number;
weatherCode: number;
}
// Coordonnées de Fakarava (Rotoava)
const FAKARAVA_LAT = -16.3167;
const FAKARAVA_LON = -145.6167;
// Codes météo Open-Meteo vers descriptions
const getWeatherCondition = (code: number): { text: string; icon: React.ReactNode } => {
// Codes Open-Meteo WMO Weather interpretation codes
if (code === 0) {
return { text: "Ciel dégagé", icon: <Sun className="h-16 w-16 text-yellow-400" /> };
} else if (code <= 3) {
return { text: "Partiellement nuageux", icon: <Cloud className="h-16 w-16 text-gray-400" /> };
} else if (code <= 48) {
return { text: "Nuageux", icon: <Cloud className="h-16 w-16 text-gray-500" /> };
} else if (code <= 55) {
return { text: "Brouillard", icon: <Cloud className="h-16 w-16 text-gray-400" /> };
} else if (code <= 67) {
return { text: "Pluie", icon: <CloudRain className="h-16 w-16 text-blue-400" /> };
} else if (code <= 77) {
return { text: "Neige", icon: <CloudRain className="h-16 w-16 text-gray-300" /> };
} else if (code <= 82) {
return { text: "Averses", icon: <CloudRain className="h-16 w-16 text-blue-500" /> };
} else if (code <= 86) {
return { text: "Averses de neige", icon: <CloudRain className="h-16 w-16 text-gray-300" /> };
} else {
return { text: "Orage", icon: <CloudRain className="h-16 w-16 text-purple-500" /> };
}
};
export default function WeatherWidget() { export default function WeatherWidget() {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [localTime, setLocalTime] = useState<string>("");
// Mise à jour de l'heure locale chaque seconde
useEffect(() => {
const updateTime = () => {
const now = new Date();
// Fuseau horaire de Tahiti (UTC-10)
const tahitiTime = new Date(now.toLocaleString("en-US", { timeZone: "Pacific/Tahiti" }));
const hours = tahitiTime.getHours().toString().padStart(2, "0");
const minutes = tahitiTime.getMinutes().toString().padStart(2, "0");
setLocalTime(`${hours}:${minutes}`);
};
updateTime();
const timeInterval = setInterval(updateTime, 1000);
return () => clearInterval(timeInterval);
}, []);
useEffect(() => {
const fetchWeather = async () => {
try {
setLoading(true);
setError(null);
// API Open-Meteo (gratuit, pas de clé API nécessaire)
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${FAKARAVA_LAT}&longitude=${FAKARAVA_LON}&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&wind_speed_unit=kmh&timezone=Pacific/Tahiti`
);
if (!response.ok) {
throw new Error("Erreur lors de la récupération de la météo");
}
const data = await response.json();
const current = data.current;
setWeather({
temperature: Math.round(current.temperature_2m),
condition: getWeatherCondition(current.weather_code).text,
windSpeed: Math.round(current.wind_speed_10m),
humidity: current.relative_humidity_2m,
weatherCode: current.weather_code,
});
} catch (err) {
console.error("Erreur météo:", err);
setError("Impossible de charger la météo");
// Valeurs par défaut en cas d'erreur
setWeather({
temperature: 28,
condition: "Ensoleillé",
windSpeed: 15,
humidity: 75,
weatherCode: 0,
});
} finally {
setLoading(false);
}
};
fetchWeather();
// Rafraîchir toutes les heures
const interval = setInterval(fetchWeather, 3600000);
return () => clearInterval(interval);
}, []);
if (loading) {
return ( return (
<Card className="bg-gradient-to-br from-primary/10 to-secondary"> <Card className="bg-gradient-to-br from-primary/10 to-secondary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Sun className="h-6 w-6 text-primary" /> <Sun className="h-6 w-6 text-primary" />
Météo Météo - Fakarava
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
</CardContent>
</Card>
);
}
if (!weather) {
return null;
}
const weatherInfo = getWeatherCondition(weather.weatherCode);
return (
<Card className="bg-gradient-to-br from-primary/10 to-secondary">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sun className="h-6 w-6 text-primary" />
Météo - Fakarava
</div>
{localTime && (
<div className="flex items-center gap-1 text-sm font-normal text-gray-600 dark:text-gray-400">
<Clock className="h-4 w-4" />
{localTime}
</div>
)}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-3xl font-bold text-primary">28°C</p> <p className="text-3xl font-bold text-primary">{weather.temperature}°C</p>
<p className="text-gray-600 mt-1">Ensoleillé</p> <p className="text-gray-600 dark:text-gray-300 mt-1">{weather.condition}</p>
{error && (
<p className="text-xs text-orange-500 mt-1">Données en cache</p>
)}
</div> </div>
<div className="text-6xl"> <div className="text-6xl">{weatherInfo.icon}</div>
<Sun className="h-16 w-16 text-yellow-400" />
</div> </div>
<div className="mt-4 flex gap-4 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1">
<Wind className="h-4 w-4" />
<span className="font-semibold">Vent:</span> {weather.windSpeed} km/h
</div> </div>
<div className="mt-4 flex gap-4 text-sm text-gray-600"> <div className="flex items-center gap-1">
<div> <Droplets className="h-4 w-4" />
<span className="font-semibold">Vent:</span> 15 km/h <span className="font-semibold">Humidité:</span> {weather.humidity}%
</div>
<div>
<span className="font-semibold">Humidité:</span> 75%
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); );
} }

View File

@ -1,129 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Wifi, Copy, Check, AlertCircle } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useClientData } from "@/lib/hooks/useClientData";
export default function WifiCard() {
const { wifiName, wifiPassword, loading } = useClientData();
const [copied, setCopied] = useState(false);
const [error, setError] = useState<string | null>(null);
// Fonction de copie avec fallback pour les navigateurs qui ne supportent pas l'API Clipboard
const copyToClipboard = async (text: string): Promise<boolean> => {
// Méthode moderne (nécessite HTTPS ou localhost)
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error("Erreur avec l'API Clipboard:", err);
}
}
// Fallback pour les navigateurs plus anciens ou contextes non sécurisés
try {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand("copy");
document.body.removeChild(textArea);
if (successful) {
return true;
} else {
throw new Error("La commande copy a échoué");
}
} catch (err) {
console.error("Erreur avec la méthode fallback:", err);
return false;
}
};
const handleCopyPassword = async () => {
if (!wifiPassword || wifiPassword.trim() === "") {
setError("Le mot de passe WiFi n'est pas disponible");
setTimeout(() => setError(null), 3000);
return;
}
setError(null);
const success = await copyToClipboard(wifiPassword);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} else {
setError("Impossible de copier. Veuillez sélectionner manuellement le mot de passe ci-dessous.");
setTimeout(() => setError(null), 5000);
}
};
// Afficher le mot de passe en cas d'échec de la copie
const showPasswordFallback = error && error.includes("sélectionner manuellement");
return (
<Card className="bg-white">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Wifi className="h-6 w-6 text-primary" />
Connexion WiFi
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="text-sm text-gray-600 mb-1">Nom du réseau</p>
<p className="text-lg font-semibold text-primary">{wifiName || "Chargement..."}</p>
</div>
{showPasswordFallback && wifiPassword && (
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-3">
<p className="text-sm text-yellow-800 font-mono select-all">
{wifiPassword}
</p>
</div>
)}
{error && !showPasswordFallback && (
<div className="bg-red-50 border border-red-200 rounded-xl p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<Button
onClick={handleCopyPassword}
disabled={loading || !wifiPassword}
className="w-full h-14 text-lg"
size="lg"
>
{loading ? (
<>
<Copy className="mr-2 h-5 w-5" />
Chargement...
</>
) : copied ? (
<>
<Check className="mr-2 h-5 w-5" />
Mot de passe copié !
</>
) : (
<>
<Copy className="mr-2 h-5 w-5" />
Copier le mot de passe
</>
)}
</Button>
</CardContent>
</Card>
);
}

View File

@ -1,30 +0,0 @@
"use client";
import { LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
export default function AdminLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const handleLogout = () => {
localStorage.removeItem("adminPassword");
router.push("/admin/login");
};
return (
<div className="min-h-screen bg-background">
<header className="bg-white border-b border-gray-200 shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold text-primary">Administration</h1>
<Button variant="outline" size="sm" onClick={handleLogout}>
<LogOut className="h-4 w-4 mr-2" />
Déconnexion
</Button>
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-6">{children}</main>
</div>
);
}

View File

@ -1,195 +0,0 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Client, ClientInput } from "@/lib/types/client";
import QRCodeDisplay from "./QRCodeDisplay";
interface ClientFormProps {
client?: Client;
onSuccess: () => void;
onCancel: () => void;
}
export default function ClientForm({ client, onSuccess, onCancel }: ClientFormProps) {
const [formData, setFormData] = useState<ClientInput>({
email: client?.email || "",
bungalowNumber: client?.bungalowNumber || "",
wifiName: client?.wifiName || "Lagon-WiFi",
wifiPassword: client?.wifiPassword || "",
gerantMessage: client?.gerantMessage || "Bienvenue dans notre pension de famille !",
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [createdClient, setCreatedClient] = useState<Client | null>(client || null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const adminPassword = localStorage.getItem("adminPassword") || "";
const url = client
? `/api/admin/clients/${client.id}`
: "/api/admin/clients";
const method = client ? "PUT" : "POST";
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminPassword}`,
},
body: JSON.stringify(formData),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || "Erreur lors de la sauvegarde");
}
const data = await response.json();
setCreatedClient(data);
onSuccess();
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
const getClientUrl = () => {
if (!createdClient) return "";
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
return `${baseUrl}/accueil?token=${createdClient.token}`;
};
return (
<Card>
<CardHeader>
<CardTitle>{client ? "Modifier le client" : "Nouveau client"}</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email *
</label>
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
disabled={!!client}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Numéro de bungalow *
</label>
<input
type="text"
required
value={formData.bungalowNumber}
onChange={(e) =>
setFormData({ ...formData, bungalowNumber: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nom du WiFi
</label>
<input
type="text"
value={formData.wifiName}
onChange={(e) =>
setFormData({ ...formData, wifiName: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Mot de passe WiFi
</label>
<input
type="text"
value={formData.wifiPassword}
onChange={(e) =>
setFormData({ ...formData, wifiPassword: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Message du gérant
</label>
<textarea
value={formData.gerantMessage}
onChange={(e) =>
setFormData({ ...formData, gerantMessage: e.target.value })
}
rows={3}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm">
{error}
</div>
)}
<div className="flex gap-3">
<Button type="button" variant="outline" onClick={onCancel} className="flex-1">
Annuler
</Button>
<Button type="submit" disabled={loading} className="flex-1">
{loading ? "Enregistrement..." : client ? "Modifier" : "Créer"}
</Button>
</div>
</form>
{createdClient && !client && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="font-semibold text-primary mb-3">Client créé avec succès !</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-600 mb-2">Lien unique :</p>
<div className="bg-secondary rounded-xl p-3 flex items-center justify-between gap-2">
<code className="text-xs break-all flex-1">{getClientUrl()}</code>
<Button
size="sm"
variant="outline"
onClick={() => {
navigator.clipboard.writeText(getClientUrl());
}}
>
Copier
</Button>
</div>
</div>
<div>
<p className="text-sm text-gray-600 mb-2">QR Code :</p>
<QRCodeDisplay url={getClientUrl()} />
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -1,165 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Trash2, Edit, Copy, QrCode } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Client } from "@/lib/types/client";
import QRCodeDisplay from "./QRCodeDisplay";
interface ClientListProps {
onEdit: (client: Client) => void;
onRefresh: () => void;
}
export default function ClientList({ onEdit, onRefresh }: ClientListProps) {
const [clients, setClients] = useState<Client[]>([]);
const [loading, setLoading] = useState(true);
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
const [showQR, setShowQR] = useState<string | null>(null);
useEffect(() => {
fetchClients();
}, []);
const fetchClients = async () => {
try {
const adminPassword = localStorage.getItem("adminPassword") || "";
const response = await fetch("/api/admin/clients", {
headers: {
Authorization: `Bearer ${adminPassword}`,
},
});
if (response.ok) {
const data = await response.json();
setClients(data);
}
} catch (error) {
console.error("Erreur lors du chargement des clients:", error);
} finally {
setLoading(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm("Êtes-vous sûr de vouloir supprimer ce client ?")) {
return;
}
try {
const adminPassword = localStorage.getItem("adminPassword") || "";
const response = await fetch(`/api/admin/clients/${id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${adminPassword}`,
},
});
if (response.ok) {
fetchClients();
onRefresh();
}
} catch (error) {
console.error("Erreur lors de la suppression:", error);
}
};
const getClientUrl = (token: string) => {
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
return `${baseUrl}/accueil?token=${token}`;
};
if (loading) {
return (
<div className="flex items-center justify-center py-8">
<p className="text-gray-600">Chargement...</p>
</div>
);
}
if (clients.length === 0) {
return (
<Card>
<CardContent className="py-8 text-center">
<p className="text-gray-600">Aucun client pour le moment.</p>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
{clients.map((client) => (
<Card key={client.id}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg">{client.email}</CardTitle>
<p className="text-sm text-gray-600 mt-1">
Bungalow {client.bungalowNumber}
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
const url = getClientUrl(client.token);
navigator.clipboard.writeText(url);
}}
>
<Copy className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() =>
setShowQR(showQR === client.id ? null : client.id)
}
>
<QrCode className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => onEdit(client)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDelete(client.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div>
<span className="font-medium">WiFi:</span> {client.wifiName}
</div>
<div>
<span className="font-medium">Message:</span>{" "}
{client.gerantMessage.substring(0, 50)}
{client.gerantMessage.length > 50 ? "..." : ""}
</div>
<div className="text-xs text-gray-500">
Créé le {new Date(client.createdAt).toLocaleDateString("fr-FR")}
</div>
</div>
{showQR === client.id && (
<div className="mt-4 pt-4 border-t border-gray-200">
<QRCodeDisplay url={getClientUrl(client.token)} size={150} />
</div>
)}
</CardContent>
</Card>
))}
</div>
);
}

View File

@ -1,20 +0,0 @@
"use client";
import { QRCodeSVG } from "qrcode.react";
interface QRCodeDisplayProps {
url: string;
size?: number;
}
export default function QRCodeDisplay({ url, size = 200 }: QRCodeDisplayProps) {
return (
<div className="flex flex-col items-center gap-4 p-4 bg-white rounded-2xl border border-gray-200">
<QRCodeSVG value={url} size={size} level="H" />
<p className="text-xs text-gray-600 text-center break-all max-w-xs">
{url}
</p>
</div>
);
}

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { useState } from "react";
import { MapPin, ExternalLink } from "lucide-react"; import { MapPin, ExternalLink } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -10,6 +11,9 @@ interface PlaceCardProps {
} }
export default function PlaceCard({ place }: PlaceCardProps) { export default function PlaceCard({ place }: PlaceCardProps) {
const [imageError, setImageError] = useState(false);
const hasValidImage = place.image && place.image !== "";
const handleOpenMaps = () => { const handleOpenMaps = () => {
let url: string; let url: string;
if (place.gmapLink && place.gmapLink !== "LIEN_GOOGLE_MAPS_A_INSERER") { if (place.gmapLink && place.gmapLink !== "LIEN_GOOGLE_MAPS_A_INSERER") {
@ -22,10 +26,24 @@ export default function PlaceCard({ place }: PlaceCardProps) {
return ( return (
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<div className="relative h-48 bg-gradient-to-br from-primary/20 to-secondary"> <div className="relative h-48 bg-gradient-to-br from-primary/20 to-secondary overflow-hidden">
{hasValidImage && !imageError ? (
<img
src={place.image}
alt={place.name}
className="w-full h-full object-cover"
style={{
imageRendering: 'auto',
} as React.CSSProperties}
loading="lazy"
fetchPriority="high"
onError={() => setImageError(true)}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<MapPin className="h-16 w-16 text-primary/30" /> <MapPin className="h-16 w-16 text-primary/30" />
</div> </div>
)}
</div> </div>
<CardHeader> <CardHeader>
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">

View File

@ -2,7 +2,7 @@ import TabNavigation from "./TabNavigation";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<div className="min-h-screen bg-background pb-16"> <div className="min-h-screen bg-background dark:bg-background-dark pb-16">
{children} {children}
<TabNavigation /> <TabNavigation />
</div> </div>

View File

@ -4,6 +4,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Home, MapPin, Info, Waves } from "lucide-react"; import { Home, MapPin, Info, Waves } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ThemeToggle } from "@/components/ThemeToggle";
const tabs = [ const tabs = [
{ {
@ -32,7 +33,7 @@ export default function TabNavigation() {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-lg"> <nav className="fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 shadow-lg">
<div className="flex items-center justify-around h-16 px-2"> <div className="flex items-center justify-around h-16 px-2">
{tabs.map((tab) => { {tabs.map((tab) => {
const Icon = tab.icon; const Icon = tab.icon;
@ -44,8 +45,8 @@ export default function TabNavigation() {
className={cn( className={cn(
"flex flex-col items-center justify-center gap-1 flex-1 h-full rounded-xl transition-colors", "flex flex-col items-center justify-center gap-1 flex-1 h-full rounded-xl transition-colors",
isActive isActive
? "text-primary bg-secondary" ? "text-primary bg-secondary dark:bg-primary/20"
: "text-gray-500 hover:text-primary hover:bg-gray-50" : "text-gray-500 dark:text-gray-400 hover:text-primary dark:hover:text-primary hover:bg-gray-50 dark:hover:bg-gray-800"
)} )}
> >
<Icon className="h-6 w-6" /> <Icon className="h-6 w-6" />
@ -53,6 +54,9 @@ export default function TabNavigation() {
</Link> </Link>
); );
})} })}
<div className="flex items-center justify-center h-full px-2">
<ThemeToggle />
</div>
</div> </div>
</nav> </nav>
); );

View File

@ -7,10 +7,10 @@ const buttonVariants = cva(
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90 dark:bg-primary dark:hover:bg-primary/80",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 dark:bg-primary/20 dark:text-primary dark:hover:bg-primary/30",
outline: "border-2 border-primary text-primary hover:bg-primary hover:text-white", outline: "border-2 border-primary text-primary hover:bg-primary hover:text-white dark:border-primary dark:text-primary dark:hover:bg-primary dark:hover:text-white",
ghost: "hover:bg-secondary hover:text-secondary-foreground", ghost: "hover:bg-secondary hover:text-secondary-foreground dark:hover:bg-gray-800 dark:hover:text-gray-200",
}, },
size: { size: {
default: "h-12 px-6 py-3", default: "h-12 px-6 py-3",

View File

@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-2xl border border-gray-200 bg-white shadow-sm", "rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm",
className className
)} )}
{...props} {...props}
@ -46,7 +46,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <p
ref={ref} ref={ref}
className={cn("text-sm text-gray-600", className)} className={cn("text-sm text-gray-600 dark:text-gray-400", className)}
{...props} {...props}
/> />
)); ));

2
data/.gitkeep Normal file
View File

@ -0,0 +1,2 @@
# Ce dossier contiendra clients.json (ignoré par git)

5
deploy/.env.example Normal file
View File

@ -0,0 +1,5 @@
# Mot de passe admin
ADMIN_PASSWORD=votre_mot_de_passe_securise
# Port (optionnel, par défaut 3000)
PORT=3000

1
deploy/.next/BUILD_ID Normal file
View File

@ -0,0 +1 @@
3c87ZLiEKCTo7fN65B1s7

View File

@ -0,0 +1,88 @@
{
"pages": {
"/_not-found/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/app/_not-found/page-93c891698b4decca.js"
],
"/layout": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/css/37740839d02c8860.css",
"static/chunks/app/layout-012feff5f2309838.js"
],
"/accueil/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/130-fbdba5ec657d70e2.js",
"static/chunks/876-74e3127f7295a7cc.js",
"static/chunks/app/accueil/page-eeceff806dc9e963.js"
],
"/admin/login/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/130-fbdba5ec657d70e2.js",
"static/chunks/app/admin/login/page-64d4a547eaf512e7.js"
],
"/admin/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/130-fbdba5ec657d70e2.js",
"static/chunks/659-547b5910f7777d12.js",
"static/chunks/app/admin/page-be00d3b5e7fb599b.js"
],
"/explorer/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/130-fbdba5ec657d70e2.js",
"static/chunks/876-74e3127f7295a7cc.js",
"static/chunks/app/explorer/page-94fbb37299b728f0.js"
],
"/infos/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/130-fbdba5ec657d70e2.js",
"static/chunks/876-74e3127f7295a7cc.js",
"static/chunks/app/infos/page-9c23965cb3e698cb.js"
],
"/mana-tracker/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/130-fbdba5ec657d70e2.js",
"static/chunks/876-74e3127f7295a7cc.js",
"static/chunks/app/mana-tracker/page-190454f13d1cd3f9.js"
],
"/offline/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/130-fbdba5ec657d70e2.js",
"static/chunks/876-74e3127f7295a7cc.js",
"static/chunks/app/offline/page-c706c75d3862967d.js"
],
"/page": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js",
"static/chunks/app/page-66582cb216c486f4.js"
]
}
}

View File

@ -0,0 +1 @@
{"/_not-found/page":"/_not-found","/accueil/page":"/accueil","/admin/login/page":"/admin/login","/admin/page":"/admin","/api/admin/clients/route":"/api/admin/clients","/explorer/page":"/explorer","/infos/page":"/infos","/mana-tracker/page":"/mana-tracker","/offline/page":"/offline","/page":"/"}

View File

@ -0,0 +1,32 @@
{
"polyfillFiles": [
"static/chunks/polyfills-42372ed130431b0a.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [
"static/3c87ZLiEKCTo7fN65B1s7/_buildManifest.js",
"static/3c87ZLiEKCTo7fN65B1s7/_ssgManifest.js"
],
"rootMainFiles": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/fd9d1056-f29a34e8cb4f1ac5.js",
"static/chunks/117-81b684a50b4e5a2c.js",
"static/chunks/main-app-be78d15d42f0ba33.js"
],
"pages": {
"/_app": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/framework-f66176bb897dc684.js",
"static/chunks/main-98da237dfd0126ae.js",
"static/chunks/pages/_app-72b849fbd24ac258.js"
],
"/_error": [
"static/chunks/webpack-272d92e0b8f870de.js",
"static/chunks/framework-f66176bb897dc684.js",
"static/chunks/main-98da237dfd0126ae.js",
"static/chunks/pages/_error-7ba65e1336b92748.js"
]
},
"ampFirstPages": []
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
{"version":1,"hasExportPathMap":false,"exportTrailingSlash":false,"isNextImageImported":false}

View File

@ -0,0 +1 @@
{"version":1,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[16,32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":60,"formats":["image/avif","image/webp"],"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"inline","remotePatterns":[],"unoptimized":false,"sizes":[640,750,828,1080,1200,1920,2048,3840,16,32,48,64,96,128,256,384]}}

View File

@ -0,0 +1 @@
{"version":1,"files":["../node_modules/styled-jsx/index.js","../node_modules/styled-jsx/package.json","../node_modules/styled-jsx/dist/index/index.js","../node_modules/react/package.json","../node_modules/react/index.js","../node_modules/client-only/package.json","../node_modules/react/cjs/react.production.min.js","../node_modules/client-only/index.js","../node_modules/styled-jsx/style.js","../node_modules/next/dist/compiled/next-server/server.runtime.prod.js","../node_modules/next/package.json","../node_modules/next/dist/server/body-streams.js","../node_modules/next/dist/lib/constants.js","../node_modules/next/dist/lib/picocolors.js","../node_modules/next/dist/shared/lib/constants.js","../node_modules/next/dist/server/web/utils.js","../node_modules/next/dist/client/components/app-router-headers.js","../node_modules/next/dist/server/lib/trace/tracer.js","../node_modules/next/dist/server/lib/trace/constants.js","../node_modules/next/dist/client/components/static-generation-async-storage.external.js","../node_modules/next/dist/shared/lib/modern-browserslist-target.js","../node_modules/next/dist/client/components/static-generation-async-storage-instance.js","../node_modules/next/dist/shared/lib/runtime-config.external.js","../node_modules/next/dist/compiled/ws/package.json","../node_modules/next/dist/compiled/node-html-parser/package.json","../node_modules/next/dist/compiled/lru-cache/package.json","../node_modules/@swc/helpers/_/_interop_require_default/package.json","../node_modules/next/dist/client/components/async-local-storage.js","../node_modules/next/dist/compiled/ws/index.js","../node_modules/next/dist/compiled/node-html-parser/index.js","../node_modules/next/dist/compiled/lru-cache/index.js","../node_modules/next/dist/compiled/@opentelemetry/api/package.json","../node_modules/@swc/helpers/package.json","../node_modules/next/dist/compiled/jsonwebtoken/package.json","../node_modules/next/dist/shared/lib/error-source.js","../node_modules/@swc/helpers/cjs/_interop_require_default.cjs","../node_modules/next/dist/compiled/@opentelemetry/api/index.js","../node_modules/next/dist/compiled/jsonwebtoken/index.js","../node_modules/next/dist/compiled/debug/package.json","../node_modules/next/dist/lib/semver-noop.js","../node_modules/next/dist/compiled/debug/index.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/app-page/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/app-page/module.compiled.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/amp-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/app-router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/entrypoints.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/head-manager-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/hooks-client-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/html-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/image-config-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/loadable.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/router-context.js","../node_modules/next/dist/server/future/route-modules/pages/vendored/contexts/server-inserted-html.js","../node_modules/next/dist/server/future/route-modules/pages/module.compiled.js"]}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"type": "commonjs"}

View File

@ -0,0 +1 @@
{"version":4,"routes":{"/infos":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/infos","dataRoute":"/infos.rsc"},"/mana-tracker":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/mana-tracker","dataRoute":"/mana-tracker.rsc"},"/offline":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/offline","dataRoute":"/offline.rsc"},"/":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/","dataRoute":"/index.rsc"},"/accueil":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/accueil","dataRoute":"/accueil.rsc"},"/admin":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/admin","dataRoute":"/admin.rsc"},"/explorer":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/explorer","dataRoute":"/explorer.rsc"},"/admin/login":{"experimentalBypassFor":[{"type":"header","key":"Next-Action"},{"type":"header","key":"content-type","value":"multipart/form-data;.*"}],"initialRevalidateSeconds":false,"srcRoute":"/admin/login","dataRoute":"/admin/login.rsc"}},"dynamicRoutes":{},"notFoundRoutes":[],"preview":{"previewModeId":"72798059332883bb836619ead11f265d","previewModeSigningKey":"c91fe6a8fb8a6afd08166f7488f47956827aa8492d588a1be69527dd7453feda","previewModeEncryptionKey":"b90ebc3c46fd8e2c88f96f242898610916379d8d4dc5cc0f98f6d0bb8b887095"}}

View File

@ -0,0 +1,14 @@
{
"app/accueil/page.tsx -> @/components/accueil/WeatherWidget": {
"id": 5147,
"files": [
"static/chunks/147.f0c63fadc263cdbf.js"
]
},
"app/explorer/page.tsx -> @/components/explorer/PlaceList": {
"id": 3293,
"files": [
"static/chunks/80.c30ab4050c6d10af.js"
]
}
}

View File

@ -0,0 +1 @@
{"version":1,"config":{"env":{},"webpack":null,"eslint":{"ignoreDuringBuilds":false},"typescript":{"ignoreBuildErrors":false,"tsconfigPath":"tsconfig.json"},"distDir":".next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.js","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":false,"compress":true,"analyticsId":"","images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[16,32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":60,"formats":["image/avif","image/webp"],"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"inline","remotePatterns":[],"unoptimized":false},"devIndicators":{"buildActivity":true,"buildActivityPosition":"bottom-right"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"amp":{"canonicalBase":""},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"optimizeFonts":true,"excludeDefaultMomentLocales":true,"serverRuntimeConfig":{},"publicRuntimeConfig":{},"reactProductionProfiling":false,"reactStrictMode":true,"httpAgentOptions":{"keepAlive":true},"outputFileTracing":true,"staticPageGenerationTimeout":60,"swcMinify":true,"modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"experimental":{"multiZoneDraftMode":false,"prerenderEarlyExit":false,"serverMinification":true,"serverSourceMaps":false,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","middlewarePrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":7,"memoryBasedWorkersCount":false,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"outputFileTracingRoot":"/home/syoul/Ccompagnon Marama","swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"adjustFontFallbacks":false,"adjustFontFallbacksWithSizeAdjust":false,"typedRoutes":false,"instrumentationHook":false,"bundlePagesExternals":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"missingSuspenseWithCSRBailout":true,"optimizeServerReact":true,"useEarlyImport":false,"staleTimes":{"dynamic":30,"static":300},"optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"configFileName":"next.config.js"},"appDir":"/home/syoul/Ccompagnon Marama","relativeAppDir":"","files":[".next/routes-manifest.json",".next/server/pages-manifest.json",".next/build-manifest.json",".next/prerender-manifest.json",".next/server/middleware-manifest.json",".next/server/middleware-build-manifest.js",".next/server/middleware-react-loadable-manifest.js",".next/server/app-paths-manifest.json",".next/app-path-routes-manifest.json",".next/app-build-manifest.json",".next/server/server-reference-manifest.js",".next/server/server-reference-manifest.json",".next/react-loadable-manifest.json",".next/server/font-manifest.json",".next/BUILD_ID",".next/server/next-font-manifest.js",".next/server/next-font-manifest.json"],"ignore":["node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*"]}

Some files were not shown because too many files have changed in this diff Show More