work with kc

This commit is contained in:
Your Name 2025-12-11 00:58:23 +00:00
parent 5e3c2df7f4
commit af03d992cb
8 changed files with 431 additions and 290 deletions

10
package-lock.json generated
View File

@ -58,6 +58,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -1074,6 +1075,7 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -1124,6 +1126,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -1246,6 +1249,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -1547,6 +1551,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -2767,6 +2772,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -2852,6 +2858,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -2861,6 +2868,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -3163,6 +3171,7 @@
"integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@oxc-project/runtime": "0.97.0", "@oxc-project/runtime": "0.97.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -3285,6 +3294,7 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -4,6 +4,9 @@
const API_BASE = "/api"; const API_BASE = "/api";
/**
* Récupère la liste des posts, avec filtres optionnels.
*/
export async function fetchPosts(filters = {}) { export async function fetchPosts(filters = {}) {
const params = new URLSearchParams(); const params = new URLSearchParams();
@ -81,7 +84,7 @@ export async function fetchIpLocation() {
/** /**
* Crée un nouveau post * Crée un nouveau post
* token: access token Keycloak (string) * token: access token Keycloak (string) ou null
*/ */
export async function createPost(payload, token) { export async function createPost(payload, token) {
const headers = { const headers = {
@ -108,11 +111,12 @@ export async function createPost(payload, token) {
} }
/** /**
* Crée un nouvel utilisateur (backend doit exposer POST /api/register) * Crée un nouvel utilisateur via le backend
* (backend: POST /api/signup)
* payload: { username, email, password } * payload: { username, email, password }
*/ */
export async function registerUser(payload) { export async function registerUser(payload) {
const res = await fetch(`${API_BASE}/register`, { const res = await fetch(`${API_BASE}/signup`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@ -1,73 +1,62 @@
import React, { createContext, useContext, useEffect, useState } from "react"; import React, {
createContext,
useContext,
useEffect,
useState,
} from "react";
// CONFIG KEYCLOAK const AuthContext = createContext(null);
// Config Keycloak pour le login inline
const KC_BASE = "https://web.sociowire.com:8443"; const KC_BASE = "https://web.sociowire.com:8443";
const KC_REALM = "sociowire"; const KC_REALM = "sociowire";
const KC_CLIENT_ID = "sociowire-frontend"; const KC_CLIENT_ID = "sociowire-frontend";
const TOKEN_KEY = "sociowire:token"; const TOKEN_KEY = "sociowire:access_token";
const USERNAME_KEY = "sociowire:username";
const AuthContext = createContext(null);
function parseJwt(token) {
try {
const [, payload] = token.split(".");
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
return JSON.parse(json);
} catch {
return {};
}
}
export function AuthProvider({ children }) { export function AuthProvider({ children }) {
const [token, setToken] = useState(null); const [loading, setLoading] = useState(true);
const [authenticated, setAuthenticated] = useState(false); const [authenticated, setAuthenticated] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [loading, setLoading] = useState(true); const [token, setToken] = useState(null);
const [lastError, setLastError] = useState(""); const [lastError, setLastError] = useState("");
// charger token depuis localStorage au démarrage // Charge token/username depuis localStorage au démarrage
useEffect(() => { useEffect(() => {
try { try {
const saved = localStorage.getItem(TOKEN_KEY); const storedToken = localStorage.getItem(TOKEN_KEY);
if (saved) { const storedUser = localStorage.getItem(USERNAME_KEY) || "";
const data = JSON.parse(saved); if (storedToken) {
if (data && data.access_token) { setToken(storedToken);
const now = Date.now() / 1000; setUsername(storedUser);
const payload = parseJwt(data.access_token); setAuthenticated(true);
if (!payload.exp || payload.exp > now) { } else {
setToken(data.access_token); setAuthenticated(false);
setAuthenticated(true);
const name =
payload.preferred_username ||
payload.email ||
payload.name ||
(payload.sub ? String(payload.sub) : "");
setUsername(name || "");
}
}
} }
} catch (e) { } catch (e) {
console.warn("[AUTH] load token error", e); console.warn("[AUTH] localStorage error", e);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, []);
// Login via password grant sur Keycloak
async function login(user, pass) { async function login(user, pass) {
setLastError(""); setLastError("");
if (!user || !pass) { const u = (user || "").trim();
setLastError("Username & password required."); if (!u || !pass) {
setLastError("Username and password required.");
return; return;
} }
setLoading(true);
try {
const params = new URLSearchParams();
params.set("grant_type", "password");
params.set("client_id", KC_CLIENT_ID);
params.set("username", user);
params.set("password", pass);
const params = new URLSearchParams();
params.set("grant_type", "password");
params.set("client_id", KC_CLIENT_ID);
params.set("username", u);
params.set("password", pass);
try {
const res = await fetch( const res = await fetch(
`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, `${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`,
{ {
@ -82,72 +71,96 @@ export function AuthProvider({ children }) {
if (!res.ok) { if (!res.ok) {
const text = await res.text(); const text = await res.text();
console.error("[AUTH] login failed", res.status, text); console.error("[AUTH] login failed", res.status, text);
setLastError("Invalid credentials or auth error.");
let msg = "Invalid credentials or auth error.";
try {
const j = JSON.parse(text);
if (j.error_description) {
msg = j.error_description;
} else if (j.error) {
msg = j.error;
}
} catch (e) {
// ignore parse error
}
setLastError(`${res.status} ${msg}`);
setAuthenticated(false); setAuthenticated(false);
setToken(null); setToken(null);
setUsername(""); setUsername("");
try { try {
localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY);
} catch {} localStorage.removeItem(USERNAME_KEY);
} catch (e) {
// ignore
}
return; return;
} }
const data = await res.json(); const data = await res.json();
const accessToken = data.access_token; const accessToken = data.access_token;
if (!accessToken) { if (!accessToken) {
setLastError("No access token returned."); setLastError("No access token in response.");
setAuthenticated(false);
setToken(null);
setUsername("");
return; return;
} }
const payload = parseJwt(accessToken);
const name =
payload.preferred_username ||
payload.email ||
payload.name ||
(payload.sub ? String(payload.sub) : "");
setToken(accessToken); setToken(accessToken);
setAuthenticated(true); setAuthenticated(true);
setUsername(name || ""); setUsername(u);
setLastError(""); setLastError("");
try { try {
localStorage.setItem(TOKEN_KEY, JSON.stringify(data)); localStorage.setItem(TOKEN_KEY, accessToken);
localStorage.setItem(USERNAME_KEY, u);
} catch (e) { } catch (e) {
console.warn("[AUTH] save token error", e); console.warn("[AUTH] failed to persist token", e);
} }
} catch (err) { } catch (e) {
console.error("[AUTH] login error", err); console.error("[AUTH] network error", e);
setLastError("Network/auth error."); setLastError("Network error during login.");
setAuthenticated(false); setAuthenticated(false);
setToken(null); setToken(null);
setUsername(""); setUsername("");
} finally { try {
setLoading(false); localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USERNAME_KEY);
} catch (e2) {
// ignore
}
} }
} }
function logout() { function logout() {
setToken(null);
setAuthenticated(false); setAuthenticated(false);
setToken(null);
setUsername(""); setUsername("");
setLastError(""); setLastError("");
try { try {
localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY);
} catch {} localStorage.removeItem(USERNAME_KEY);
} catch (e) {
// ignore
}
} }
const value = { return (
loading, <AuthContext.Provider
authenticated, value={{
username, loading,
token, authenticated,
lastError, username,
login, token,
logout, login,
}; logout,
lastError,
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }}
>
{children}
</AuthContext.Provider>
);
} }
export function useAuth() { export function useAuth() {

View File

@ -18,13 +18,16 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
lastError, lastError,
} = useAuth(); } = useAuth();
const [mode, setMode] = useState("login"); // "login" | "register" const [showAuth, setShowAuth] = useState(false);
const [mode, setMode] = useState("login"); // "login" | "signup"
// login form // login
const [user, setUser] = useState(""); const [loginUser, setLoginUser] = useState("");
const [pass, setPass] = useState(""); const [loginPass, setLoginPass] = useState("");
// signup form // signup
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [regUser, setRegUser] = useState(""); const [regUser, setRegUser] = useState("");
const [regEmail, setRegEmail] = useState(""); const [regEmail, setRegEmail] = useState("");
const [regPass, setRegPass] = useState(""); const [regPass, setRegPass] = useState("");
@ -35,16 +38,28 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
if (loading) authLabel = "AUTH: loading…"; if (loading) authLabel = "AUTH: loading…";
else if (authenticated) authLabel = `AUTH: user=${username || "?"}`; else if (authenticated) authLabel = `AUTH: user=${username || "?"}`;
const handleLoginSubmit = (e) => { const openModal = (nextMode) => {
e.preventDefault(); setMode(nextMode);
login(user.trim(), pass); setShowAuth(true);
setSignupError("");
}; };
const handleRegisterSubmit = async (e) => { const closeModal = () => {
setShowAuth(false);
};
const handleLoginSubmit = async (e) => {
e.preventDefault();
await login(loginUser, loginPass);
// on laisse l'utilisateur fermer manuellement,
// il voit en haut si ça a marché ou pas.
};
const handleSignupSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setSignupError(""); setSignupError("");
if (!regUser.trim() || !regEmail.trim() || !regPass) { if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
setSignupError("All fields required."); setSignupError("Nom, prénom, user, email et mot de passe sont requis.");
return; return;
} }
try { try {
@ -53,9 +68,13 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
username: regUser.trim(), username: regUser.trim(),
email: regEmail.trim(), email: regEmail.trim(),
password: regPass, password: regPass,
firstName: firstName.trim(),
lastName: lastName.trim(),
}); });
// auto login après création // après création, on tente le login direct
login(regUser.trim(), regPass); await login(regUser.trim(), regPass);
// on ferme le popup, l'état auth s'affiche dans la topbar
setShowAuth(false);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setSignupError(err.message || "Register error."); setSignupError(err.message || "Register error.");
@ -65,215 +84,192 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
}; };
return ( return (
<header className="topbar"> <>
<div className="logo-circle">SW</div> <header className="topbar">
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<div className="title-block"> <div className="logo-circle">SW</div>
<div className="main-title">SOCIOWIRE.com</div> <div className="title-block">
<div className="sub-title">Wired to life</div> <div className="main-title">SOCIOWIRE.com</div>
<div style={{ fontSize: "0.65rem", opacity: 0.7 }}> <div className="sub-title">Wired to life</div>
{authLabel} <div style={{ fontSize: "0.65rem", opacity: 0.7 }}>
{lastError && ( {authLabel}
<span style={{ color: "#f97373", marginLeft: "0.3rem" }}> {lastError && (
({lastError}) <span style={{ color: "#f97373", marginLeft: "0.3rem" }}>
</span> ({lastError})
)} </span>
{signupError && ( )}
<span style={{ color: "#f97373", marginLeft: "0.3rem" }}> </div>
({signupError})
</span>
)}
</div>
</div>
<div className="topbar-right">
<div className="theme-switch">
{["dark", "blue", "light"].map((t) => (
<button
key={t}
type="button"
className={
"theme-dot" + (theme === t ? " theme-dot-active" : "")
}
title={THEME_LABELS[t]}
onClick={() => onChangeTheme && onChangeTheme(t)}
>
{THEME_LABELS[t][0]}
</button>
))}
</div>
{authenticated ? (
<div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
<span style={{ fontSize: "0.8rem" }}>{username}</span>
<button className="btn-primary" onClick={logout}>
Logout
</button>
</div> </div>
) : ( </div>
<div
style={{ <div className="topbar-right">
display: "flex", <div className="theme-switch">
flexDirection: "column", {["dark", "blue", "light"].map((t) => (
alignItems: "flex-end", <button
gap: "0.25rem", key={t}
minWidth: "230px", type="button"
}} className={
> "theme-dot" + (theme === t ? " theme-dot-active" : "")
<div }
style={{ title={THEME_LABELS[t]}
display: "flex", onClick={() => onChangeTheme && onChangeTheme(t)}
gap: "0.25rem", >
fontSize: "0.65rem", {THEME_LABELS[t][0]}
}} </button>
> ))}
</div>
{authenticated ? (
<div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
<span style={{ fontSize: "0.8rem" }}>{username}</span>
<button className="btn-primary" onClick={logout}>
Logout
</button>
</div>
) : (
<div style={{ display: "flex", gap: "0.3rem" }}>
<button <button
type="button" type="button"
onClick={() => setMode("login")} className="btn-primary"
style={{ onClick={() => openModal("login")}
padding: "0.15rem 0.45rem",
borderRadius: 999,
border: "1px solid #4b5563",
background:
mode === "login" ? "#1e293b" : "transparent",
color: "#e5e7eb",
cursor: "pointer",
fontSize: "0.65rem",
}}
> >
Login Login
</button> </button>
<button <button
type="button" type="button"
onClick={() => setMode("register")} className="btn-secondary"
style={{ onClick={() => openModal("signup")}
padding: "0.15rem 0.45rem",
borderRadius: 999,
border: "1px solid #4b5563",
background:
mode === "register" ? "#1e293b" : "transparent",
color: "#e5e7eb",
cursor: "pointer",
fontSize: "0.65rem",
}}
> >
Sign up Sign up
</button> </button>
</div> </div>
)}
</div>
</header>
{showAuth && (
<div className="auth-modal-backdrop" onClick={closeModal}>
<div
className="auth-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="auth-modal-header">
<button
type="button"
className={
"auth-tab" + (mode === "login" ? " auth-tab-active" : "")
}
onClick={() => setMode("login")}
>
Login
</button>
<button
type="button"
className={
"auth-tab" + (mode === "signup" ? " auth-tab-active" : "")
}
onClick={() => setMode("signup")}
>
Sign up
</button>
<button
type="button"
className="auth-close"
onClick={closeModal}
>
</button>
</div>
{mode === "login" ? ( {mode === "login" ? (
<form <form className="auth-form" onSubmit={handleLoginSubmit}>
onSubmit={handleLoginSubmit} <label>
style={{ Nom d&apos;utilisateur
display: "flex", <input
alignItems: "center", type="text"
gap: "0.25rem", value={loginUser}
}} onChange={(e) => setLoginUser(e.target.value)}
> autoComplete="username"
<input />
type="text" </label>
placeholder="user" <label>
value={user} Mot de passe
onChange={(e) => setUser(e.target.value)} <input
style={{ type="password"
fontSize: "0.7rem", value={loginPass}
padding: "0.15rem 0.4rem", onChange={(e) => setLoginPass(e.target.value)}
borderRadius: 999, autoComplete="current-password"
border: "1px solid #4b5563", />
background: "#020617", </label>
color: "#e5e7eb",
}}
/>
<input
type="password"
placeholder="pass"
value={pass}
onChange={(e) => setPass(e.target.value)}
style={{
fontSize: "0.7rem",
padding: "0.15rem 0.4rem",
borderRadius: 999,
border: "1px solid #4b5563",
background: "#020617",
color: "#e5e7eb",
}}
/>
<button <button
className="btn-primary"
type="submit" type="submit"
className="btn-primary btn-full"
disabled={loading} disabled={loading}
style={{ fontSize: "0.7rem" }}
> >
{loading ? "..." : "Login"} {loading ? "..." : "Se connecter"}
</button> </button>
</form> </form>
) : ( ) : (
<form <form className="auth-form" onSubmit={handleSignupSubmit}>
onSubmit={handleRegisterSubmit} <div className="auth-row">
style={{ <label>
display: "flex", Prénom
alignItems: "center", <input
gap: "0.25rem", type="text"
}} value={firstName}
> onChange={(e) => setFirstName(e.target.value)}
<input />
type="text" </label>
placeholder="user" <label>
value={regUser} Nom
onChange={(e) => setRegUser(e.target.value)} <input
style={{ type="text"
fontSize: "0.7rem", value={lastName}
padding: "0.15rem 0.4rem", onChange={(e) => setLastName(e.target.value)}
borderRadius: 999, />
border: "1px solid #4b5563", </label>
background: "#020617", </div>
color: "#e5e7eb", <label>
minWidth: "70px", Nom d&apos;utilisateur
}} <input
/> type="text"
<input value={regUser}
type="email" onChange={(e) => setRegUser(e.target.value)}
placeholder="email" />
value={regEmail} </label>
onChange={(e) => setRegEmail(e.target.value)} <label>
style={{ Courriel
fontSize: "0.7rem", <input
padding: "0.15rem 0.4rem", type="email"
borderRadius: 999, value={regEmail}
border: "1px solid #4b5563", onChange={(e) => setRegEmail(e.target.value)}
background: "#020617", />
color: "#e5e7eb", </label>
minWidth: "90px", <label>
}} Mot de passe
/> <input
<input type="password"
type="password" value={regPass}
placeholder="pass" onChange={(e) => setRegPass(e.target.value)}
value={regPass} />
onChange={(e) => setRegPass(e.target.value)} </label>
style={{
fontSize: "0.7rem", {signupError && (
padding: "0.15rem 0.4rem", <div className="auth-error">{signupError}</div>
borderRadius: 999, )}
border: "1px solid #4b5563",
background: "#020617",
color: "#e5e7eb",
minWidth: "70px",
}}
/>
<button <button
className="btn-primary"
type="submit" type="submit"
className="btn-primary btn-full"
disabled={signupLoading} disabled={signupLoading}
style={{ fontSize: "0.7rem" }}
> >
{signupLoading ? "..." : "Create"} {signupLoading ? "..." : "Créer le compte"}
</button> </button>
</form> </form>
)} )}
</div> </div>
)} </div>
</div> )}
</header> </>
); );
} }

View File

@ -10,14 +10,16 @@ export function useUserPosition(mapRef, hasLastView) {
if (!map) return; if (!map) return;
let cancelled = false; let cancelled = false;
let hasPos = false;
function saveUserPos(lat, lon) { function saveUserPos(lat, lon) {
if (cancelled) return; if (cancelled) return;
hasPos = true;
const coords = [lon, lat]; const coords = [lon, lat];
setUserPosition(coords); setUserPosition(coords);
try { try {
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon })); localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
} catch { } catch (e) {
// ignore // ignore
} }
@ -32,27 +34,27 @@ export function useUserPosition(mapRef, hasLastView) {
} }
} }
// 1) dernière position user // 1) dernière position user dans localStorage
try { try {
const raw = localStorage.getItem(LAST_POS_KEY); const raw = localStorage.getItem(LAST_POS_KEY);
if (raw) { if (raw) {
const p = JSON.parse(raw); const p = JSON.parse(raw);
if (typeof p.lat === "number" && typeof p.lon === "number") { if (typeof p.lat === "number" && typeof p.lon === "number") {
hasPos = true;
setUserPosition([p.lon, p.lat]); setUserPosition([p.lon, p.lat]);
} }
} }
} catch { } catch (e) {
// ignore // ignore
} }
// 2) GPS → 3) IP backend // 2) GPS → 3) IP backend (uniquement si on n'a VRAIMENT rien)
function viaIpFallback() { async function viaIpFallback() {
(async () => { if (cancelled || hasPos) return;
const ipCoords = await fetchIpLocation(); const ipCoords = await fetchIpLocation();
if (!ipCoords || cancelled) return; if (!ipCoords || cancelled) return;
const [lon, lat] = ipCoords; const [lon, lat] = ipCoords;
saveUserPos(lat, lon); saveUserPos(lat, lon);
})();
} }
if ("geolocation" in navigator) { if ("geolocation" in navigator) {
@ -62,7 +64,7 @@ export function useUserPosition(mapRef, hasLastView) {
saveUserPos(pos.coords.latitude, pos.coords.longitude); saveUserPos(pos.coords.latitude, pos.coords.longitude);
}, },
() => { () => {
if (cancelled) return; if (cancelled || hasPos) return;
viaIpFallback(); viaIpFallback();
}, },
{ {
@ -71,7 +73,8 @@ export function useUserPosition(mapRef, hasLastView) {
maximumAge: 30000, maximumAge: 30000,
} }
); );
} else { } else if (!hasPos) {
// pas de geolocation et aucune position connue → IP fallback
viaIpFallback(); viaIpFallback();
} }

View File

@ -11,7 +11,8 @@ import "./styles/popup.css";
import "./styles/overlays.css"; import "./styles/overlays.css";
import "./styles/posts.css"; import "./styles/posts.css";
import "./styles/theme.css"; import "./styles/theme.css";
// dans main.jsx, avec les autres styles
import "./styles/auth-modal.css";
import { AuthProvider } from "./auth/AuthContext.jsx"; import { AuthProvider } from "./auth/AuthContext.jsx";
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(

89
src/styles/auth-modal.css Normal file
View File

@ -0,0 +1,89 @@
.auth-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.auth-modal {
width: 100%;
max-width: 360px;
background: #020617;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.9);
box-shadow:
0 18px 45px rgba(0, 0, 0, 0.9),
0 0 24px rgba(56, 189, 248, 0.5);
padding: 0.9rem 1rem 1rem 1rem;
color: #e5e7eb;
}
.auth-modal-header {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.7rem;
}
.auth-tab {
border-radius: 999px;
border: 1px solid #4b5563;
background: #020617;
color: #e5e7eb;
font-size: 0.75rem;
padding: 0.25rem 0.7rem;
cursor: pointer;
}
.auth-tab-active {
background: #1e293b;
border-color: #60a5fa;
}
.auth-close {
margin-left: auto;
border: none;
background: transparent;
color: #9ca3af;
cursor: pointer;
font-size: 0.9rem;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.auth-form label {
display: flex;
flex-direction: column;
font-size: 0.75rem;
gap: 0.15rem;
}
.auth-form input {
font-size: 0.8rem;
padding: 0.25rem 0.55rem;
border-radius: 999px;
border: 1px solid #4b5563;
background: #020617;
color: #e5e7eb;
}
.auth-row {
display: flex;
gap: 0.4rem;
}
.auth-row label {
flex: 1;
}
.auth-error {
font-size: 0.7rem;
color: #fecaca;
}

View File

@ -2,6 +2,7 @@
.topbar { .topbar {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.6rem; gap: 0.6rem;
@ -24,6 +25,7 @@
.title-block { .title-block {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0;
} }
.main-title { .main-title {
@ -36,14 +38,16 @@
opacity: 0.7; opacity: 0.7;
} }
/* bloc de droite : thèmes + bouton login */ /* bloc de droite : thèmes + boutons */
.topbar-right { .topbar-right {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: flex-end;
gap: 0.5rem; gap: 0.5rem;
margin-left: auto;
} }
/* petits boutons de thème (D / B / L) */
.theme-switch { .theme-switch {
display: flex; display: flex;
gap: 0.25rem; gap: 0.25rem;
@ -69,7 +73,6 @@
box-shadow: 0 0 10px rgba(250, 204, 21, 0.7); box-shadow: 0 0 10px rgba(250, 204, 21, 0.7);
} }
/* bouton Login */
.btn-primary { .btn-primary {
padding: 0.25rem 0.7rem; padding: 0.25rem 0.7rem;
font-size: 0.75rem; font-size: 0.75rem;
@ -79,3 +82,25 @@
color: #f9fafb; color: #f9fafb;
cursor: pointer; cursor: pointer;
} }
.btn-secondary {
padding: 0.25rem 0.7rem;
font-size: 0.75rem;
border-radius: 999px;
border: 1px solid #60a5fa;
background: #1e293b;
color: #dbeafe;
cursor: pointer;
}
.btn-full {
width: 100%;
}
/* mobile */
@media (max-width: 480px) {
.topbar-right {
width: 100%;
justify-content: flex-start;
}
}