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==",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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'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'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>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue