work with kc
This commit is contained in:
parent
5e3c2df7f4
commit
af03d992cb
|
|
@ -58,6 +58,7 @@
|
|||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
@ -1074,6 +1075,7 @@
|
|||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -1124,6 +1126,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -1246,6 +1249,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -1547,6 +1551,7 @@
|
|||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -2767,6 +2772,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2852,6 +2858,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -2861,6 +2868,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -3163,6 +3171,7 @@
|
|||
"integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/runtime": "0.97.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -3285,6 +3294,7 @@
|
|||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
const API_BASE = "/api";
|
||||
|
||||
/**
|
||||
* Récupère la liste des posts, avec filtres optionnels.
|
||||
*/
|
||||
export async function fetchPosts(filters = {}) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
|
@ -81,7 +84,7 @@ export async function fetchIpLocation() {
|
|||
|
||||
/**
|
||||
* Crée un nouveau post
|
||||
* token: access token Keycloak (string)
|
||||
* token: access token Keycloak (string) ou null
|
||||
*/
|
||||
export async function createPost(payload, token) {
|
||||
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 }
|
||||
*/
|
||||
export async function registerUser(payload) {
|
||||
const res = await fetch(`${API_BASE}/register`, {
|
||||
const res = await fetch(`${API_BASE}/signup`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -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_REALM = "sociowire";
|
||||
const KC_CLIENT_ID = "sociowire-frontend";
|
||||
|
||||
const TOKEN_KEY = "sociowire:token";
|
||||
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
const TOKEN_KEY = "sociowire:access_token";
|
||||
const USERNAME_KEY = "sociowire:username";
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [token, setToken] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
const [username, setUsername] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [token, setToken] = useState(null);
|
||||
const [lastError, setLastError] = useState("");
|
||||
|
||||
// charger token depuis localStorage au démarrage
|
||||
// Charge token/username depuis localStorage au démarrage
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(TOKEN_KEY);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
if (data && data.access_token) {
|
||||
const now = Date.now() / 1000;
|
||||
const payload = parseJwt(data.access_token);
|
||||
if (!payload.exp || payload.exp > now) {
|
||||
setToken(data.access_token);
|
||||
setAuthenticated(true);
|
||||
const name =
|
||||
payload.preferred_username ||
|
||||
payload.email ||
|
||||
payload.name ||
|
||||
(payload.sub ? String(payload.sub) : "");
|
||||
setUsername(name || "");
|
||||
}
|
||||
}
|
||||
const storedToken = localStorage.getItem(TOKEN_KEY);
|
||||
const storedUser = localStorage.getItem(USERNAME_KEY) || "";
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
setUsername(storedUser);
|
||||
setAuthenticated(true);
|
||||
} else {
|
||||
setAuthenticated(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[AUTH] load token error", e);
|
||||
console.warn("[AUTH] localStorage error", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Login via password grant sur Keycloak
|
||||
async function login(user, pass) {
|
||||
setLastError("");
|
||||
if (!user || !pass) {
|
||||
setLastError("Username & password required.");
|
||||
const u = (user || "").trim();
|
||||
if (!u || !pass) {
|
||||
setLastError("Username and password required.");
|
||||
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(
|
||||
`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`,
|
||||
{
|
||||
|
|
@ -82,72 +71,96 @@ export function AuthProvider({ children }) {
|
|||
if (!res.ok) {
|
||||
const text = await res.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);
|
||||
setToken(null);
|
||||
setUsername("");
|
||||
try {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
} catch {}
|
||||
localStorage.removeItem(USERNAME_KEY);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const accessToken = data.access_token;
|
||||
if (!accessToken) {
|
||||
setLastError("No access token returned.");
|
||||
setLastError("No access token in response.");
|
||||
setAuthenticated(false);
|
||||
setToken(null);
|
||||
setUsername("");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = parseJwt(accessToken);
|
||||
const name =
|
||||
payload.preferred_username ||
|
||||
payload.email ||
|
||||
payload.name ||
|
||||
(payload.sub ? String(payload.sub) : "");
|
||||
|
||||
setToken(accessToken);
|
||||
setAuthenticated(true);
|
||||
setUsername(name || "");
|
||||
setUsername(u);
|
||||
setLastError("");
|
||||
|
||||
try {
|
||||
localStorage.setItem(TOKEN_KEY, JSON.stringify(data));
|
||||
localStorage.setItem(TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(USERNAME_KEY, u);
|
||||
} catch (e) {
|
||||
console.warn("[AUTH] save token error", e);
|
||||
console.warn("[AUTH] failed to persist token", e);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[AUTH] login error", err);
|
||||
setLastError("Network/auth error.");
|
||||
} catch (e) {
|
||||
console.error("[AUTH] network error", e);
|
||||
setLastError("Network error during login.");
|
||||
setAuthenticated(false);
|
||||
setToken(null);
|
||||
setUsername("");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
try {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USERNAME_KEY);
|
||||
} catch (e2) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
setToken(null);
|
||||
setAuthenticated(false);
|
||||
setToken(null);
|
||||
setUsername("");
|
||||
setLastError("");
|
||||
try {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
} catch {}
|
||||
localStorage.removeItem(USERNAME_KEY);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const value = {
|
||||
loading,
|
||||
authenticated,
|
||||
username,
|
||||
token,
|
||||
lastError,
|
||||
login,
|
||||
logout,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
loading,
|
||||
authenticated,
|
||||
username,
|
||||
token,
|
||||
login,
|
||||
logout,
|
||||
lastError,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
|
|
|
|||
|
|
@ -18,13 +18,16 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
|||
lastError,
|
||||
} = useAuth();
|
||||
|
||||
const [mode, setMode] = useState("login"); // "login" | "register"
|
||||
const [showAuth, setShowAuth] = useState(false);
|
||||
const [mode, setMode] = useState("login"); // "login" | "signup"
|
||||
|
||||
// login form
|
||||
const [user, setUser] = useState("");
|
||||
const [pass, setPass] = useState("");
|
||||
// login
|
||||
const [loginUser, setLoginUser] = useState("");
|
||||
const [loginPass, setLoginPass] = useState("");
|
||||
|
||||
// signup form
|
||||
// signup
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [regUser, setRegUser] = useState("");
|
||||
const [regEmail, setRegEmail] = useState("");
|
||||
const [regPass, setRegPass] = useState("");
|
||||
|
|
@ -35,16 +38,28 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
|||
if (loading) authLabel = "AUTH: loading…";
|
||||
else if (authenticated) authLabel = `AUTH: user=${username || "?"}`;
|
||||
|
||||
const handleLoginSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
login(user.trim(), pass);
|
||||
const openModal = (nextMode) => {
|
||||
setMode(nextMode);
|
||||
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();
|
||||
setSignupError("");
|
||||
if (!regUser.trim() || !regEmail.trim() || !regPass) {
|
||||
setSignupError("All fields required.");
|
||||
if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
|
||||
setSignupError("Nom, prénom, user, email et mot de passe sont requis.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
@ -53,9 +68,13 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
|||
username: regUser.trim(),
|
||||
email: regEmail.trim(),
|
||||
password: regPass,
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
});
|
||||
// auto login après création
|
||||
login(regUser.trim(), regPass);
|
||||
// après création, on tente le login direct
|
||||
await login(regUser.trim(), regPass);
|
||||
// on ferme le popup, l'état auth s'affiche dans la topbar
|
||||
setShowAuth(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setSignupError(err.message || "Register error.");
|
||||
|
|
@ -65,215 +84,192 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="logo-circle">SW</div>
|
||||
|
||||
<div className="title-block">
|
||||
<div className="main-title">SOCIOWIRE.com</div>
|
||||
<div className="sub-title">Wired to life</div>
|
||||
<div style={{ fontSize: "0.65rem", opacity: 0.7 }}>
|
||||
{authLabel}
|
||||
{lastError && (
|
||||
<span style={{ color: "#f97373", marginLeft: "0.3rem" }}>
|
||||
({lastError})
|
||||
</span>
|
||||
)}
|
||||
{signupError && (
|
||||
<span style={{ color: "#f97373", marginLeft: "0.3rem" }}>
|
||||
({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>
|
||||
<>
|
||||
<header className="topbar">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
<div className="logo-circle">SW</div>
|
||||
<div className="title-block">
|
||||
<div className="main-title">SOCIOWIRE.com</div>
|
||||
<div className="sub-title">Wired to life</div>
|
||||
<div style={{ fontSize: "0.65rem", opacity: 0.7 }}>
|
||||
{authLabel}
|
||||
{lastError && (
|
||||
<span style={{ color: "#f97373", marginLeft: "0.3rem" }}>
|
||||
({lastError})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: "0.25rem",
|
||||
minWidth: "230px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.25rem",
|
||||
fontSize: "0.65rem",
|
||||
}}
|
||||
>
|
||||
</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 style={{ display: "flex", gap: "0.3rem" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("login")}
|
||||
style={{
|
||||
padding: "0.15rem 0.45rem",
|
||||
borderRadius: 999,
|
||||
border: "1px solid #4b5563",
|
||||
background:
|
||||
mode === "login" ? "#1e293b" : "transparent",
|
||||
color: "#e5e7eb",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.65rem",
|
||||
}}
|
||||
className="btn-primary"
|
||||
onClick={() => openModal("login")}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("register")}
|
||||
style={{
|
||||
padding: "0.15rem 0.45rem",
|
||||
borderRadius: 999,
|
||||
border: "1px solid #4b5563",
|
||||
background:
|
||||
mode === "register" ? "#1e293b" : "transparent",
|
||||
color: "#e5e7eb",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.65rem",
|
||||
}}
|
||||
className="btn-secondary"
|
||||
onClick={() => openModal("signup")}
|
||||
>
|
||||
Sign up
|
||||
</button>
|
||||
</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" ? (
|
||||
<form
|
||||
onSubmit={handleLoginSubmit}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="user"
|
||||
value={user}
|
||||
onChange={(e) => setUser(e.target.value)}
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.15rem 0.4rem",
|
||||
borderRadius: 999,
|
||||
border: "1px solid #4b5563",
|
||||
background: "#020617",
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
<form className="auth-form" onSubmit={handleLoginSubmit}>
|
||||
<label>
|
||||
Nom d'utilisateur
|
||||
<input
|
||||
type="text"
|
||||
value={loginUser}
|
||||
onChange={(e) => setLoginUser(e.target.value)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Mot de passe
|
||||
<input
|
||||
type="password"
|
||||
value={loginPass}
|
||||
onChange={(e) => setLoginPass(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="btn-primary"
|
||||
type="submit"
|
||||
className="btn-primary btn-full"
|
||||
disabled={loading}
|
||||
style={{ fontSize: "0.7rem" }}
|
||||
>
|
||||
{loading ? "..." : "Login"}
|
||||
{loading ? "..." : "Se connecter"}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleRegisterSubmit}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="user"
|
||||
value={regUser}
|
||||
onChange={(e) => setRegUser(e.target.value)}
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.15rem 0.4rem",
|
||||
borderRadius: 999,
|
||||
border: "1px solid #4b5563",
|
||||
background: "#020617",
|
||||
color: "#e5e7eb",
|
||||
minWidth: "70px",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="email"
|
||||
value={regEmail}
|
||||
onChange={(e) => setRegEmail(e.target.value)}
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.15rem 0.4rem",
|
||||
borderRadius: 999,
|
||||
border: "1px solid #4b5563",
|
||||
background: "#020617",
|
||||
color: "#e5e7eb",
|
||||
minWidth: "90px",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="pass"
|
||||
value={regPass}
|
||||
onChange={(e) => setRegPass(e.target.value)}
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.15rem 0.4rem",
|
||||
borderRadius: 999,
|
||||
border: "1px solid #4b5563",
|
||||
background: "#020617",
|
||||
color: "#e5e7eb",
|
||||
minWidth: "70px",
|
||||
}}
|
||||
/>
|
||||
<form className="auth-form" onSubmit={handleSignupSubmit}>
|
||||
<div className="auth-row">
|
||||
<label>
|
||||
Prénom
|
||||
<input
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Nom
|
||||
<input
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Nom d'utilisateur
|
||||
<input
|
||||
type="text"
|
||||
value={regUser}
|
||||
onChange={(e) => setRegUser(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Courriel
|
||||
<input
|
||||
type="email"
|
||||
value={regEmail}
|
||||
onChange={(e) => setRegEmail(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Mot de passe
|
||||
<input
|
||||
type="password"
|
||||
value={regPass}
|
||||
onChange={(e) => setRegPass(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{signupError && (
|
||||
<div className="auth-error">{signupError}</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn-primary"
|
||||
type="submit"
|
||||
className="btn-primary btn-full"
|
||||
disabled={signupLoading}
|
||||
style={{ fontSize: "0.7rem" }}
|
||||
>
|
||||
{signupLoading ? "..." : "Create"}
|
||||
{signupLoading ? "..." : "Créer le compte"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,16 @@ export function useUserPosition(mapRef, hasLastView) {
|
|||
if (!map) return;
|
||||
|
||||
let cancelled = false;
|
||||
let hasPos = false;
|
||||
|
||||
function saveUserPos(lat, lon) {
|
||||
if (cancelled) return;
|
||||
hasPos = true;
|
||||
const coords = [lon, lat];
|
||||
setUserPosition(coords);
|
||||
try {
|
||||
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
|
|
@ -32,27 +34,27 @@ export function useUserPosition(mapRef, hasLastView) {
|
|||
}
|
||||
}
|
||||
|
||||
// 1) dernière position user
|
||||
// 1) dernière position user dans localStorage
|
||||
try {
|
||||
const raw = localStorage.getItem(LAST_POS_KEY);
|
||||
if (raw) {
|
||||
const p = JSON.parse(raw);
|
||||
if (typeof p.lat === "number" && typeof p.lon === "number") {
|
||||
hasPos = true;
|
||||
setUserPosition([p.lon, p.lat]);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 2) GPS → 3) IP backend
|
||||
function viaIpFallback() {
|
||||
(async () => {
|
||||
const ipCoords = await fetchIpLocation();
|
||||
if (!ipCoords || cancelled) return;
|
||||
const [lon, lat] = ipCoords;
|
||||
saveUserPos(lat, lon);
|
||||
})();
|
||||
// 2) GPS → 3) IP backend (uniquement si on n'a VRAIMENT rien)
|
||||
async function viaIpFallback() {
|
||||
if (cancelled || hasPos) return;
|
||||
const ipCoords = await fetchIpLocation();
|
||||
if (!ipCoords || cancelled) return;
|
||||
const [lon, lat] = ipCoords;
|
||||
saveUserPos(lat, lon);
|
||||
}
|
||||
|
||||
if ("geolocation" in navigator) {
|
||||
|
|
@ -62,7 +64,7 @@ export function useUserPosition(mapRef, hasLastView) {
|
|||
saveUserPos(pos.coords.latitude, pos.coords.longitude);
|
||||
},
|
||||
() => {
|
||||
if (cancelled) return;
|
||||
if (cancelled || hasPos) return;
|
||||
viaIpFallback();
|
||||
},
|
||||
{
|
||||
|
|
@ -71,7 +73,8 @@ export function useUserPosition(mapRef, hasLastView) {
|
|||
maximumAge: 30000,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
} else if (!hasPos) {
|
||||
// pas de geolocation et aucune position connue → IP fallback
|
||||
viaIpFallback();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import "./styles/popup.css";
|
|||
import "./styles/overlays.css";
|
||||
import "./styles/posts.css";
|
||||
import "./styles/theme.css";
|
||||
|
||||
// dans main.jsx, avec les autres styles
|
||||
import "./styles/auth-modal.css";
|
||||
import { AuthProvider } from "./auth/AuthContext.jsx";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
.topbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem;
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
.title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
|
|
@ -36,14 +38,16 @@
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* bloc de droite : thèmes + bouton login */
|
||||
/* bloc de droite : thèmes + boutons */
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* petits boutons de thème (D / B / L) */
|
||||
.theme-switch {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
|
@ -69,7 +73,6 @@
|
|||
box-shadow: 0 0 10px rgba(250, 204, 21, 0.7);
|
||||
}
|
||||
|
||||
/* bouton Login */
|
||||
.btn-primary {
|
||||
padding: 0.25rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -79,3 +82,25 @@
|
|||
color: #f9fafb;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue