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(() => {});