Commit remaining frontend tweaks

This commit is contained in:
Your Name 2025-12-31 02:41:50 -05:00
parent c955b122e3
commit d9d3b9df20
4 changed files with 562 additions and 30 deletions

534
README.md
View File

@ -1,17 +1,531 @@
# React + Vite # SocioWire Frontend
char ok tourbmarxhe y
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 Le frontend SocioWire est une Single Page Application (SPA) construite avec React 19 et Vite. Elle offre une expérience utilisateur riche avec:
- [@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 - 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

View File

@ -2,9 +2,9 @@ import React from "react";
export default function TimeFilterButtons({ activeHours, onSelect }) { export default function TimeFilterButtons({ activeHours, onSelect }) {
const buttons = [ 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: 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" }, { hours: 336, label: "Old", icon: "fa-solid fa-hourglass-half" },
]; ];

View File

@ -473,18 +473,22 @@ function openImageLightbox(src) {
try { document.removeEventListener("keydown", onKey); } catch {} try { document.removeEventListener("keydown", onKey); } catch {}
try { window.removeEventListener("popstate", onPop); } catch {} try { window.removeEventListener("popstate", onPop); } catch {}
if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 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) { if (!opts.skipHistory && overlay.__swPushed) {
overlay.__swPushed = false; overlay.__swPushed = false;
try { history.back(); } catch {} try { history.back(); } catch {}
} }
}; };
const onKey = (e) => { const onKey = (e) => {
// ESC closes image and returns to post (via history.back)
if (e.key === "Escape") close(); if (e.key === "Escape") close();
}; };
const onPop = () => close({ skipHistory: true }); const onPop = () => close({ skipHistory: true });
overlay.addEventListener("click", close); overlay.addEventListener("click", close);
document.addEventListener("keydown", onKey); document.addEventListener("keydown", onKey);
window.addEventListener("popstate", onPop); window.addEventListener("popstate", onPop);
// Push history for image so back button closes image (returns to post)
try { try {
history.pushState({ sw: "image" }, ""); history.pushState({ sw: "image" }, "");
overlay.__swPushed = true; overlay.__swPushed = true;
@ -1052,26 +1056,34 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
}; };
window.addEventListener("keydown", onKey); 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 onPop = null;
let historyPushed = false; let historyPushed = false;
if (postID) { try {
try { // Only push history state without changing URL
const url = `/?post_id=${postID}`; window.history.pushState({ swModal: true, postId: postID }, "");
const state = window.history.state || {}; historyPushed = true;
if (!state || state.swModal !== true || state.postId !== postID) {
window.history.pushState({ ...state, swModal: true, postId: postID }, "", url); onPop = (e) => {
historyPushed = true; // 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; // Otherwise, we're closing the modal
if (ov && ov.postId === postID) { const ov = map.__swCenteredOverlay;
ov.historyClosed = true; if (ov && ov.postId === postID) {
closeCenteredOverlay(map, { skipHistory: true, force: true }); ov.historyClosed = true;
} closeCenteredOverlay(map, { skipHistory: true, force: true });
}; }
window.addEventListener("popstate", onPop); };
} catch {} window.addEventListener("popstate", onPop);
} } catch {}
// CTA open url // CTA open url
const cta = modalWrap.querySelector(".sw-cta-primary"); const cta = modalWrap.querySelector(".sw-cta-primary");

View File

@ -29,9 +29,15 @@ ReactDOM.createRoot(document.getElementById("root")).render(
</React.StrictMode> </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) { if ("serviceWorker" in navigator) {
window.addEventListener("load", () => { 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") navigator.serviceWorker.register("/service-worker.js")
.then(() => {}) .then(() => {})
.catch(() => {}); .catch(() => {});