Commit remaining frontend tweaks
This commit is contained in:
parent
c955b122e3
commit
d9d3b9df20
534
README.md
534
README.md
|
|
@ -1,17 +1,531 @@
|
|||
# React + Vite
|
||||
char ok tourbmarxhe y
|
||||
# SocioWire Frontend
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
Interface utilisateur de SocioWire - plateforme sociale géolocalisée. Application React moderne avec cartes 3D, authentification Keycloak, et fonctionnalités temps réel.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
## Description
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
Le frontend SocioWire est une Single Page Application (SPA) construite avec React 19 et Vite. Elle offre une expérience utilisateur riche avec:
|
||||
- Authentification OAuth2 via Keycloak
|
||||
- Feed de posts géolocalisés avec cartes interactives
|
||||
- Recherche intelligente avec autocomplete
|
||||
- Visualisation 3D des "Islands" utilisateurs
|
||||
- Engagement temps réel (likes, comments, shares)
|
||||
- Interface responsive et moderne
|
||||
|
||||
## React Compiler
|
||||
## Technologies
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
- **Framework**: React 19
|
||||
- **Build Tool**: Vite (rolldown-vite)
|
||||
- **Routing**: React Router DOM v7
|
||||
- **Auth**: Keycloak.js (OAuth2/OIDC)
|
||||
- **Maps**: MapLibre GL
|
||||
- **HTTP Client**: Axios
|
||||
- **Icons**: Font Awesome
|
||||
- **Styling**: CSS modules + custom themes
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
## Structure du projet
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
```
|
||||
sociowire-frontend/
|
||||
├── src/
|
||||
│ ├── main.jsx # Entry point
|
||||
│ ├── App.jsx # App router
|
||||
│ ├── api/
|
||||
│ │ ├── client.js # Axios client avec auth
|
||||
│ │ └── templates.js # API templates
|
||||
│ ├── auth/
|
||||
│ │ ├── AuthContext.jsx # Auth provider
|
||||
│ │ ├── authStorage.js # Token storage
|
||||
│ │ └── emailVerified.js # Email verification
|
||||
│ ├── components/
|
||||
│ │ ├── Posts/ # Post components
|
||||
│ │ │ ├── PostCard.jsx
|
||||
│ │ │ ├── PostList.jsx
|
||||
│ │ │ └── PostCardTemplate.jsx
|
||||
│ │ ├── Search/ # Search components
|
||||
│ │ │ └── SmartSearchBar.jsx
|
||||
│ │ ├── Filters/ # Filter components
|
||||
│ │ │ ├── FilterButtons.jsx
|
||||
│ │ │ └── TimeFilterButtons.jsx
|
||||
│ │ ├── Profile/ # User profile
|
||||
│ │ │ └── UserProfile.jsx
|
||||
│ │ ├── Island/ # Island visualization
|
||||
│ │ │ ├── IslandViewer.jsx
|
||||
│ │ │ └── IslandUniverse.jsx
|
||||
│ │ ├── Map/ # Map components
|
||||
│ │ │ ├── MapView.jsx
|
||||
│ │ │ ├── MapControls.jsx
|
||||
│ │ │ └── TemplateMarkerCard.jsx
|
||||
│ │ ├── Auth/ # Auth components
|
||||
│ │ │ ├── AuthToast.jsx
|
||||
│ │ │ └── AuthPill.jsx
|
||||
│ │ └── ui/ # UI primitives
|
||||
│ │ └── ToastHost.jsx
|
||||
│ ├── services/
|
||||
│ │ └── recoSearch.js # Reco service client
|
||||
│ ├── theme/
|
||||
│ │ └── cardTokens.js # Design tokens
|
||||
│ └── index.css # Global styles
|
||||
├── public/ # Static assets
|
||||
├── package.json
|
||||
├── vite.config.js
|
||||
└── eslint.config.js
|
||||
```
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Node.js 18+ (recommandé: 20+)
|
||||
- npm ou yarn
|
||||
- Backend SocioWire en cours d'exécution (port 8081)
|
||||
- Keycloak configuré
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd /home/swire/sociowire-frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Les URLs des services sont configurées dans le code:
|
||||
|
||||
**Backend API**: `http://localhost:8081`
|
||||
**Keycloak**: `https://auth.sociowire.com`
|
||||
|
||||
Pour changer ces URLs, modifier:
|
||||
- `src/api/client.js` - Backend URL
|
||||
- `src/auth/AuthContext.jsx` - Keycloak URL
|
||||
|
||||
### Variables d'environnement (optionnel)
|
||||
|
||||
Créer un fichier `.env.local`:
|
||||
|
||||
```env
|
||||
VITE_API_URL=http://localhost:8081
|
||||
VITE_KEYCLOAK_URL=https://auth.sociowire.com
|
||||
VITE_KEYCLOAK_REALM=sociowire
|
||||
VITE_KEYCLOAK_CLIENT_ID=sociowire-frontend
|
||||
```
|
||||
|
||||
## Démarrage
|
||||
|
||||
### Mode développement
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
L'application démarre sur `http://localhost:5173` (ou autre port si occupé).
|
||||
|
||||
### Build production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Les fichiers sont générés dans `dist/`.
|
||||
|
||||
### Preview production
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Teste le build production localement.
|
||||
|
||||
## Fonctionnalités principales
|
||||
|
||||
### 1. Authentification
|
||||
|
||||
- **Login**: Email/password via Keycloak
|
||||
- **Signup**: Création de compte avec vérification email
|
||||
- **OAuth**: Login social (Google, GitHub, etc.)
|
||||
- **Refresh tokens**: Gestion automatique de l'expiration
|
||||
- **Logout**: Déconnexion complète (Keycloak + local)
|
||||
|
||||
**Composants**:
|
||||
- `AuthContext` - Provider global d'auth
|
||||
- `AuthPill` - Widget utilisateur
|
||||
- `AuthToast` - Notifications auth
|
||||
|
||||
### 2. Feed de posts
|
||||
|
||||
- **Posts géolocalisés**: Affichage avec lat/lon
|
||||
- **Catégories**: NEWS, EVENT, DISCUSSION, QUESTION
|
||||
- **Filtres**: Par catégorie, temps, géolocalisation
|
||||
- **Pagination**: Chargement progressif
|
||||
- **Templates**: Posts avec templates visuels
|
||||
|
||||
**Composants**:
|
||||
- `PostList` - Liste de posts
|
||||
- `PostCard` - Carte de post individuelle
|
||||
- `PostCardTemplate` - Post avec template
|
||||
|
||||
### 3. Recherche
|
||||
|
||||
- **Smart Search**: Recherche full-text via reco-service
|
||||
- **Autocomplete**: Suggestions en temps réel (posts, profils, lieux)
|
||||
- **Filtres avancés**: Catégorie, temps, géo
|
||||
- **Unified Search**: Posts + profils + places
|
||||
|
||||
**Composants**:
|
||||
- `SmartSearchBar` - Barre de recherche avec autocomplete
|
||||
- `FilterButtons` - Filtres de catégorie
|
||||
- `TimeFilterButtons` - Filtres temporels
|
||||
|
||||
### 4. Cartes interactives
|
||||
|
||||
- **MapLibre GL**: Cartes vectorielles performantes
|
||||
- **Markers**: Posts géolocalisés sur la carte
|
||||
- **Clustering**: Regroupement de markers
|
||||
- **3D**: Visualisation 3D optionnelle
|
||||
- **Controls**: Zoom, rotation, pitch
|
||||
|
||||
**Composants**:
|
||||
- `MapView` - Carte principale
|
||||
- `MapControls` - Contrôles de carte
|
||||
- `TemplateMarkerCard` - Popup de marker
|
||||
|
||||
### 5. Islands (Univers utilisateurs)
|
||||
|
||||
- **Island Universe**: Visualisation 3D des utilisateurs
|
||||
- **Island Viewer**: Détails d'une island
|
||||
- **Génération procédurale**: Islands uniques par utilisateur
|
||||
- **Navigation**: Exploration de l'univers
|
||||
|
||||
**Composants**:
|
||||
- `IslandUniverse` - Univers 3D
|
||||
- `IslandViewer` - Vue détaillée d'island
|
||||
|
||||
### 6. Profils utilisateurs
|
||||
|
||||
- **User Profile**: Profil complet avec posts
|
||||
- **Avatar**: Upload et affichage
|
||||
- **Stats**: Posts, followers, etc.
|
||||
- **Bio**: Description personnalisée
|
||||
|
||||
**Composants**:
|
||||
- `UserProfile` - Page de profil
|
||||
|
||||
### 7. Engagement
|
||||
|
||||
- **Likes**: Like/unlike de posts
|
||||
- **Comments**: Commentaires hiérarchiques
|
||||
- **Shares**: Partage de posts
|
||||
- **Truth Voting**: Vote de vérité (fact-checking)
|
||||
|
||||
Intégré dans `PostCard`.
|
||||
|
||||
## Appels API
|
||||
|
||||
Le frontend communique avec:
|
||||
|
||||
### 1. sociowire-backend (`http://localhost:8081`)
|
||||
|
||||
**Authentification**:
|
||||
- `POST /api/signup-email`
|
||||
- `POST /api/signup`
|
||||
- `POST /api/login`
|
||||
- `POST /api/refresh`
|
||||
- `GET /api/me`
|
||||
|
||||
**Posts**:
|
||||
- `GET /api/posts` - Feed
|
||||
- `POST /api/post` - Créer
|
||||
- `GET /api/post?id=...` - Get
|
||||
- `POST /api/post/like` - Like
|
||||
- `GET /api/post/comments` - Comments
|
||||
- `POST /api/post/comment` - Ajouter comment
|
||||
|
||||
**Recherche**:
|
||||
- `GET /api/search?q=...` - Recherche full-text
|
||||
- `GET /api/suggestions?q=...` - Autocomplete
|
||||
- `GET /api/smart-feed` - Feed personnalisé
|
||||
- `GET /api/unified-search` - Search combinée
|
||||
|
||||
**Profils**:
|
||||
- `GET /api/profile` - Mon profil
|
||||
- `PATCH /api/profile/username` - Update username
|
||||
- `POST /api/profile/avatar` - Upload avatar
|
||||
|
||||
**Assets**:
|
||||
- `POST /api/assets/upload` - Upload image
|
||||
- `GET /api/assets/url?id=...` - Get asset URL
|
||||
|
||||
**Islands**:
|
||||
- `GET /api/islands/nearby` - Islands à proximité
|
||||
- `GET /api/islands/by-username/{username}` - Island par username
|
||||
|
||||
**Utilitaires**:
|
||||
- `GET /api/templates` - Templates de posts
|
||||
- `GET /api/ip-location` - Géolocalisation IP
|
||||
|
||||
---
|
||||
|
||||
### 2. Keycloak (`https://auth.sociowire.com`)
|
||||
|
||||
Le frontend utilise `keycloak-js` pour communiquer avec Keycloak:
|
||||
|
||||
- **Init**: `keycloak.init({ onLoad: 'check-sso' })`
|
||||
- **Login**: `keycloak.login()`
|
||||
- **Logout**: `keycloak.logout()`
|
||||
- **Token**: Rafraîchissement automatique via `keycloak.updateToken()`
|
||||
|
||||
**Realm**: `sociowire`
|
||||
**Client ID**: `sociowire-frontend`
|
||||
|
||||
---
|
||||
|
||||
## Gestion d'état
|
||||
|
||||
### AuthContext
|
||||
|
||||
Provider global pour l'authentification:
|
||||
|
||||
```jsx
|
||||
import { useAuth } from './auth/AuthContext';
|
||||
|
||||
function Component() {
|
||||
const { keycloak, isAuthenticated, username, login, logout } = useAuth();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**État**:
|
||||
- `keycloak` - Instance Keycloak
|
||||
- `isAuthenticated` - Boolean
|
||||
- `username` - String
|
||||
- `emailVerified` - Boolean
|
||||
- `login()` - Fonction login
|
||||
- `logout()` - Fonction logout
|
||||
|
||||
---
|
||||
|
||||
## Axios Client
|
||||
|
||||
Client HTTP configuré avec intercepteurs pour auth:
|
||||
|
||||
```javascript
|
||||
// src/api/client.js
|
||||
import axios from 'axios';
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: 'http://localhost:8081',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
// Request interceptor: ajoute Bearer token
|
||||
client.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('keycloak_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor: refresh token si 401
|
||||
client.interceptors.response.use(
|
||||
response => response,
|
||||
async error => {
|
||||
if (error.response?.status === 401) {
|
||||
// Refresh token logic
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Routing
|
||||
|
||||
Routes principales:
|
||||
|
||||
```jsx
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/feed" element={<Feed />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route path="/profile/:username" element={<Profile />} />
|
||||
<Route path="/islands" element={<Islands />} />
|
||||
<Route path="/post/:id" element={<PostDetail />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thème et Styling
|
||||
|
||||
Le projet utilise CSS modules + variables CSS:
|
||||
|
||||
**Design Tokens** (`theme/cardTokens.js`):
|
||||
- Couleurs de catégories
|
||||
- Spacing
|
||||
- Border radius
|
||||
- Shadows
|
||||
|
||||
**CSS Variables**:
|
||||
```css
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #28a745;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icônes (Font Awesome)
|
||||
|
||||
```jsx
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
|
||||
<i className="fas fa-heart"></i>
|
||||
<i className="fas fa-comment"></i>
|
||||
<i className="fas fa-share"></i>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MapLibre GL
|
||||
|
||||
Configuration de la carte:
|
||||
|
||||
```jsx
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: 'https://demotiles.maplibre.org/style.json',
|
||||
center: [-73.5, 45.5],
|
||||
zoom: 10
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build et Déploiement
|
||||
|
||||
### Build optimisé
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Génère:
|
||||
- `dist/index.html` - HTML principal
|
||||
- `dist/assets/*.js` - Bundles JavaScript
|
||||
- `dist/assets/*.css` - Styles
|
||||
- `dist/assets/*` - Assets (images, fonts)
|
||||
|
||||
### Déploiement
|
||||
|
||||
Le build peut être déployé sur:
|
||||
- **Nginx**: Servir `dist/` avec fallback vers `index.html`
|
||||
- **Vercel**: Détection automatique de Vite
|
||||
- **Netlify**: Build command `npm run build`, publish dir `dist`
|
||||
- **Docker**: Nginx container avec `dist/`
|
||||
|
||||
**Nginx config**:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
root /var/www/sociowire-frontend/dist;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Développement
|
||||
|
||||
### Hot Module Replacement (HMR)
|
||||
|
||||
Vite supporte HMR out-of-the-box:
|
||||
- Modifications CSS: Rechargement instantané
|
||||
- Modifications JSX: Preserve state
|
||||
- Modifications API: Reload complet
|
||||
|
||||
### ESLint
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Configuration dans `eslint.config.js`.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
Tests à ajouter avec Vitest + React Testing Library:
|
||||
|
||||
```bash
|
||||
npm install -D vitest @testing-library/react @testing-library/jest-dom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Optimisations:
|
||||
- **Code splitting**: Routes lazy-loaded
|
||||
- **Tree shaking**: Vite optimise automatiquement
|
||||
- **Asset optimization**: Images optimisées
|
||||
- **Caching**: Service Worker (à ajouter)
|
||||
- **Bundle size**: Analysé avec rollup-plugin-visualizer
|
||||
|
||||
---
|
||||
|
||||
## Sécurité
|
||||
|
||||
- Tokens stockés en localStorage (préférer httpOnly cookies en prod)
|
||||
- Validation input côté client + serveur
|
||||
- CORS configuré sur backend
|
||||
- CSP headers recommandés
|
||||
- Sanitisation des URLs externes
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### React DevTools
|
||||
|
||||
Installer l'extension browser React DevTools.
|
||||
|
||||
### Vite DevTools
|
||||
|
||||
Console affiche:
|
||||
- HMR updates
|
||||
- Build errors
|
||||
- Network requests (via browser)
|
||||
|
||||
### Logs
|
||||
|
||||
```javascript
|
||||
console.log('Debug:', data);
|
||||
```
|
||||
|
||||
En production, utiliser un service de logging (Sentry, LogRocket).
|
||||
|
||||
---
|
||||
|
||||
## Notes de développement
|
||||
|
||||
- Le frontend assume que le backend est sur `localhost:8081`
|
||||
- Keycloak doit être configuré avec le client `sociowire-frontend`
|
||||
- Les tokens JWT sont stockés en localStorage
|
||||
- MapLibre nécessite une clé API pour certains styles (optionnel)
|
||||
- L'upload d'images passe par le backend, qui proxy vers asset-service
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import React from "react";
|
|||
|
||||
export default function TimeFilterButtons({ activeHours, onSelect }) {
|
||||
const buttons = [
|
||||
{ hours: 2, label: "Now", icon: "fa-solid fa-bolt" },
|
||||
{ hours: 5, label: "Now", icon: "fa-solid fa-bolt" },
|
||||
{ hours: 24, label: "Today", icon: "fa-solid fa-sun" },
|
||||
{ hours: 72, label: "Recent", icon: "fa-solid fa-clock-rotate-left" },
|
||||
{ hours: 96, label: "4Days", icon: "fa-solid fa-calendar-days" },
|
||||
{ hours: 336, label: "Old", icon: "fa-solid fa-hourglass-half" },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -473,18 +473,22 @@ function openImageLightbox(src) {
|
|||
try { document.removeEventListener("keydown", onKey); } catch {}
|
||||
try { window.removeEventListener("popstate", onPop); } catch {}
|
||||
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
// When closing image, go back in history to return to post (not close post)
|
||||
if (!opts.skipHistory && overlay.__swPushed) {
|
||||
overlay.__swPushed = false;
|
||||
try { history.back(); } catch {}
|
||||
}
|
||||
};
|
||||
const onKey = (e) => {
|
||||
// ESC closes image and returns to post (via history.back)
|
||||
if (e.key === "Escape") close();
|
||||
};
|
||||
const onPop = () => close({ skipHistory: true });
|
||||
overlay.addEventListener("click", close);
|
||||
document.addEventListener("keydown", onKey);
|
||||
window.addEventListener("popstate", onPop);
|
||||
|
||||
// Push history for image so back button closes image (returns to post)
|
||||
try {
|
||||
history.pushState({ sw: "image" }, "");
|
||||
overlay.__swPushed = true;
|
||||
|
|
@ -1052,17 +1056,26 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
|
|||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
|
||||
// Back button support WITHOUT changing URL
|
||||
// We push a fake history state so back button closes the modal
|
||||
let onPop = null;
|
||||
let historyPushed = false;
|
||||
if (postID) {
|
||||
try {
|
||||
const url = `/?post_id=${postID}`;
|
||||
const state = window.history.state || {};
|
||||
if (!state || state.swModal !== true || state.postId !== postID) {
|
||||
window.history.pushState({ ...state, swModal: true, postId: postID }, "", url);
|
||||
// Only push history state without changing URL
|
||||
window.history.pushState({ swModal: true, postId: postID }, "");
|
||||
historyPushed = true;
|
||||
|
||||
onPop = (e) => {
|
||||
// Check if we're returning to the modal state (from image) or actually closing
|
||||
const state = e.state || window.history.state;
|
||||
|
||||
// If state still has swModal=true, we're just returning from image to post
|
||||
// Don't close the post modal
|
||||
if (state && state.swModal === true && state.postId === postID) {
|
||||
return; // Stay in post modal
|
||||
}
|
||||
onPop = () => {
|
||||
|
||||
// Otherwise, we're closing the modal
|
||||
const ov = map.__swCenteredOverlay;
|
||||
if (ov && ov.postId === postID) {
|
||||
ov.historyClosed = true;
|
||||
|
|
@ -1071,7 +1084,6 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
|
|||
};
|
||||
window.addEventListener("popstate", onPop);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// CTA open url
|
||||
const cta = modalWrap.querySelector(".sw-cta-primary");
|
||||
|
|
|
|||
|
|
@ -29,9 +29,15 @@ ReactDOM.createRoot(document.getElementById("root")).render(
|
|||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// Enregistre le service worker pour PWA
|
||||
// Enregistre le service worker pour PWA (désactivé en dev pour éviter le cache)
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
if (import.meta?.env?.DEV) {
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then((regs) => Promise.all(regs.map((r) => r.unregister())))
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
navigator.serviceWorker.register("/service-worker.js")
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
|
|
|||
Loading…
Reference in New Issue