diff --git a/README.md b/README.md index d6cc4c5..a67001e 100644 --- a/README.md +++ b/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 + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + +``` + +--- + +## 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'; + + + + +``` + +--- + +## 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 diff --git a/src/components/Filters/TimeFilterButtons.jsx b/src/components/Filters/TimeFilterButtons.jsx index 42106eb..5d3399a 100644 --- a/src/components/Filters/TimeFilterButtons.jsx +++ b/src/components/Filters/TimeFilterButtons.jsx @@ -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" }, ]; diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js index 268d4de..fd90ea7 100644 --- a/src/components/Map/markerManager.js +++ b/src/components/Map/markerManager.js @@ -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,26 +1056,34 @@ 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); - historyPushed = true; + try { + // 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 = () => { - const ov = map.__swCenteredOverlay; - if (ov && ov.postId === postID) { - ov.historyClosed = true; - closeCenteredOverlay(map, { skipHistory: true, force: true }); - } - }; - window.addEventListener("popstate", onPop); - } catch {} - } + + // Otherwise, we're closing the modal + const ov = map.__swCenteredOverlay; + if (ov && ov.postId === postID) { + ov.historyClosed = true; + closeCenteredOverlay(map, { skipHistory: true, force: true }); + } + }; + window.addEventListener("popstate", onPop); + } catch {} // CTA open url const cta = modalWrap.querySelector(".sw-cta-primary"); diff --git a/src/main.jsx b/src/main.jsx index 95294ce..fa38e9e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -29,9 +29,15 @@ ReactDOM.createRoot(document.getElementById("root")).render( ); -// 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(() => {});