update frontend
This commit is contained in:
parent
7ea3b2c4fa
commit
555342d377
|
|
@ -1,28 +1,25 @@
|
|||
// src/pwaInstall.js
|
||||
let deferredPrompt = null;
|
||||
|
||||
// Écoute l'événement natif PWA (se déclenche quand Chrome pense que l'app est installable)
|
||||
// Stocke l'événement natif quand Chrome le déclenche
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
// Empêche le prompt auto (on veut le contrôler avec le bouton)
|
||||
// Empêche le prompt auto (on veut le contrôler)
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
console.log('PWA install prompt ready - waiting for user click');
|
||||
console.log('PWA: Install prompt ready - ready to trigger on button click');
|
||||
});
|
||||
|
||||
// Fonction appelée quand on clique sur le bouton "Install"
|
||||
// Fonction appelée par le bouton "Install"
|
||||
export function promptInstall() {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt(); // Affiche le prompt natif "Add to home screen"
|
||||
console.log('PWA: Triggering native install prompt');
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
console.log('User accepted PWA install');
|
||||
} else {
|
||||
console.log('User dismissed PWA install');
|
||||
}
|
||||
console.log('PWA Install outcome:', choiceResult.outcome);
|
||||
deferredPrompt = null;
|
||||
});
|
||||
} else {
|
||||
console.log('No install prompt available yet - try again later');
|
||||
alert('PWA installation not ready yet. Try again in a few seconds.');
|
||||
console.log('PWA: No prompt ready yet - wait for beforeinstallprompt event');
|
||||
alert('PWA not ready yet. Try scrolling or interacting more with the site.');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { clearAuth, guessUsernameFromToken, jwtExpSeconds, loadAuth, saveAuth } from "./authStorage";
|
||||
|
||||
const AuthCtx = createContext(null);
|
||||
|
||||
function apiBase() {
|
||||
return (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
async function apiFetch(path, opts = {}) {
|
||||
const url = apiBase() + path;
|
||||
const res = await fetch(url, {
|
||||
credentials: "include",
|
||||
...opts,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(opts.headers || {}),
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
// tolerant whoami: accepts {authenticated:true} OR {username:""} OR {user:""} OR just ok+token
|
||||
async function whoami(accessToken) {
|
||||
const res = await apiFetch("/api/whoami", {
|
||||
method: "GET",
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
|
||||
});
|
||||
|
||||
const text = await res.text().catch(() => "");
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
|
||||
const uname =
|
||||
(json && (json.username || json.user || json.preferred_username)) ||
|
||||
guessUsernameFromToken(accessToken) ||
|
||||
"";
|
||||
|
||||
const authed =
|
||||
!!res.ok &&
|
||||
(json?.authenticated === true || json?.ok === true || !!json?.username || !!json?.user || !!uname);
|
||||
|
||||
return { ok: !!res.ok, status: res.status, json, text, authenticated: authed, username: uname };
|
||||
}
|
||||
|
||||
async function refreshTokens(refreshToken) {
|
||||
const res = await apiFetch("/api/refresh", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
const text = await res.text().catch(() => "");
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
|
||||
return { ok: res.ok, status: res.status, json, text };
|
||||
}
|
||||
|
||||
function nowSec() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [status, setStatus] = useState("loading"); // loading | anon | auth
|
||||
const [user, setUser] = useState(null); // { username }
|
||||
const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
|
||||
const [error, setError] = useState("");
|
||||
const refreshTimer = useRef(null);
|
||||
|
||||
function setAnon(msg = "") {
|
||||
setStatus("anon");
|
||||
setUser(null);
|
||||
setTokens(null);
|
||||
setError(msg || "");
|
||||
clearAuth();
|
||||
}
|
||||
|
||||
function setAuthFromTokens(t, usernameGuess = "") {
|
||||
setTokens(t);
|
||||
saveAuth({ ...t, obtained_at: Date.now() });
|
||||
const u = usernameGuess || guessUsernameFromToken(t?.access_token) || "";
|
||||
setUser(u ? { username: u } : null);
|
||||
setStatus("auth");
|
||||
setError("");
|
||||
}
|
||||
|
||||
async function ensureFreshSession(existing) {
|
||||
if (!existing?.access_token) return { ok: false, reason: "no_access_token" };
|
||||
|
||||
const exp = jwtExpSeconds(existing.access_token);
|
||||
const skew = 60;
|
||||
const isExpiredOrSoon = !exp || exp <= nowSec() + skew;
|
||||
|
||||
if (isExpiredOrSoon) {
|
||||
if (!existing.refresh_token) return { ok: false, reason: "expired_no_refresh" };
|
||||
|
||||
const rr = await refreshTokens(existing.refresh_token);
|
||||
if (!rr.ok || !rr.json?.access_token) {
|
||||
return { ok: false, reason: `refresh_failed_${rr.status}`, detail: rr.text };
|
||||
}
|
||||
|
||||
const merged = { ...existing, ...rr.json };
|
||||
if (!merged.refresh_token) merged.refresh_token = existing.refresh_token;
|
||||
|
||||
setAuthFromTokens(merged);
|
||||
existing = merged;
|
||||
} else {
|
||||
setAuthFromTokens(existing);
|
||||
}
|
||||
|
||||
const w = await whoami(existing.access_token);
|
||||
if (w.ok && w.authenticated) {
|
||||
setUser(w.username ? { username: w.username } : null);
|
||||
setStatus("auth");
|
||||
setError("");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
return { ok: false, reason: `whoami_${w.status}`, detail: w.text || "" };
|
||||
}
|
||||
|
||||
function scheduleAutoRefresh() {
|
||||
if (refreshTimer.current) {
|
||||
clearInterval(refreshTimer.current);
|
||||
refreshTimer.current = null;
|
||||
}
|
||||
|
||||
refreshTimer.current = setInterval(async () => {
|
||||
const cur = loadAuth();
|
||||
if (!cur?.access_token) return;
|
||||
|
||||
const exp = jwtExpSeconds(cur.access_token);
|
||||
if (!exp) return;
|
||||
|
||||
if (exp <= nowSec() + 90) {
|
||||
if (!cur.refresh_token) {
|
||||
setAnon("Session expirée (pas de refresh_token).");
|
||||
return;
|
||||
}
|
||||
const rr = await refreshTokens(cur.refresh_token);
|
||||
if (!rr.ok || !rr.json?.access_token) {
|
||||
setAnon("Session expirée. Please login again.");
|
||||
return;
|
||||
}
|
||||
const merged = { ...cur, ...rr.json };
|
||||
if (!merged.refresh_token) merged.refresh_token = cur.refresh_token;
|
||||
setAuthFromTokens(merged);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
async function restore() {
|
||||
setStatus("loading");
|
||||
setError("");
|
||||
const saved = loadAuth();
|
||||
if (!saved?.access_token) {
|
||||
setAnon("");
|
||||
return;
|
||||
}
|
||||
|
||||
const r = await ensureFreshSession(saved);
|
||||
if (!r.ok) {
|
||||
setAnon("Login desynced. Please login again.");
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleAutoRefresh();
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
setError("");
|
||||
setStatus("loading");
|
||||
|
||||
const res = await apiFetch("/api/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const text = await res.text().catch(() => "");
|
||||
let j = null;
|
||||
try {
|
||||
j = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
j = null;
|
||||
}
|
||||
|
||||
if (!res.ok || !j?.access_token) {
|
||||
setAnon(j?.error_description || j?.error || text || "Login failed");
|
||||
return { ok: false, status: res.status, json: j, text };
|
||||
}
|
||||
|
||||
const t = { ...j, obtained_at: Date.now() };
|
||||
setAuthFromTokens(t);
|
||||
scheduleAutoRefresh();
|
||||
|
||||
const w = await whoami(t.access_token);
|
||||
if (!w.ok || !w.authenticated) {
|
||||
setAnon("Login desynced. Please login again.");
|
||||
return { ok: false, status: w.status, json: w.json, text: w.text };
|
||||
}
|
||||
|
||||
setUser(w.username ? { username: w.username } : null);
|
||||
setStatus("auth");
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if (refreshTimer.current) clearInterval(refreshTimer.current);
|
||||
refreshTimer.current = null;
|
||||
setAnon("");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
restore();
|
||||
const onStorage = (e) => {
|
||||
if (e.key === "sociowire:auth") restore();
|
||||
};
|
||||
window.addEventListener("storage", onStorage);
|
||||
return () => window.removeEventListener("storage", onStorage);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Backward-compatible fields expected by TopBar/MapView
|
||||
const value = useMemo(() => {
|
||||
const loading = status === "loading";
|
||||
const authenticated = status === "auth";
|
||||
const username = user?.username || "";
|
||||
const token = tokens?.access_token || "";
|
||||
|
||||
return {
|
||||
// internal
|
||||
status,
|
||||
user,
|
||||
tokens,
|
||||
error,
|
||||
|
||||
// expected by UI
|
||||
loading,
|
||||
authenticated,
|
||||
username,
|
||||
token,
|
||||
|
||||
lastError: error || "",
|
||||
lastInfo: "",
|
||||
needsEmailVerification: false,
|
||||
resendVerificationEmail: async () => {},
|
||||
|
||||
login,
|
||||
logout,
|
||||
restore,
|
||||
setError,
|
||||
};
|
||||
}, [status, user, tokens, error]);
|
||||
|
||||
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
return useContext(AuthCtx);
|
||||
}
|
||||
|
|
@ -1,379 +0,0 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import { registerUser } from "../../api/client";
|
||||
|
||||
const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" };
|
||||
const LAST_SIGNUP_USER_KEY = "sociowire:lastSignupUser";
|
||||
|
||||
const KC_BASE =
|
||||
(import.meta?.env?.VITE_KEYCLOAK_BASE_URL) || "https://auth.sociowire.com";
|
||||
const KC_REALM =
|
||||
(import.meta?.env?.VITE_KEYCLOAK_REALM) || "sociowire";
|
||||
const KC_CLIENT =
|
||||
(import.meta?.env?.VITE_KEYCLOAK_CLIENT_ID) || "sociowire";
|
||||
|
||||
function isEmailVerifyError(raw) {
|
||||
const msg = String(raw || "").toLowerCase();
|
||||
return (
|
||||
msg.includes("not fully set up") ||
|
||||
msg.includes("verify") ||
|
||||
msg.includes("verification") ||
|
||||
msg.includes("required action") ||
|
||||
(msg.includes("email") && msg.includes("verified"))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAuthMessage(raw) {
|
||||
const msg = String(raw || "").trim();
|
||||
if (!msg) return "";
|
||||
|
||||
const lower = msg.toLowerCase();
|
||||
|
||||
if (isEmailVerifyError(lower)) {
|
||||
return "Ton compte est créé, mais l’email n’est pas confirmé. Clique “Vérifier mon email” (Keycloak), puis utilise le lien reçu par email.";
|
||||
}
|
||||
|
||||
if (
|
||||
lower.includes("invalid user credentials") ||
|
||||
lower.includes("bad credentials") ||
|
||||
lower.includes("invalid_grant")
|
||||
) {
|
||||
return "Mauvais username ou mot de passe.";
|
||||
}
|
||||
|
||||
if (msg.length > 220) return msg.slice(0, 220) + "…";
|
||||
return msg;
|
||||
}
|
||||
|
||||
function buildKeycloakAuthUrl() {
|
||||
const redirectUri = `${window.location.origin}/auth/verified`;
|
||||
const url =
|
||||
`${KC_BASE}/realms/${encodeURIComponent(KC_REALM)}` +
|
||||
`/protocol/openid-connect/auth` +
|
||||
`?client_id=${encodeURIComponent(KC_CLIENT)}` +
|
||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
||||
`&response_type=code&scope=openid`;
|
||||
return url;
|
||||
}
|
||||
|
||||
export default function TopBar({ theme = "dark", onChangeTheme }) {
|
||||
const {
|
||||
loading,
|
||||
authenticated,
|
||||
username,
|
||||
login,
|
||||
logout,
|
||||
resendVerificationEmail,
|
||||
lastError,
|
||||
lastInfo,
|
||||
} = useAuth();
|
||||
|
||||
const [showAuth, setShowAuth] = useState(false);
|
||||
const [mode, setMode] = useState("login");
|
||||
|
||||
const [loginUser, setLoginUser] = useState("");
|
||||
const [loginPass, setLoginPass] = useState("");
|
||||
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [regUser, setRegUser] = useState("");
|
||||
const [regEmail, setRegEmail] = useState("");
|
||||
const [regPass, setRegPass] = useState("");
|
||||
const [signupError, setSignupError] = useState("");
|
||||
const [signupInfo, setSignupInfo] = useState("");
|
||||
const [signupLoading, setSignupLoading] = useState(false);
|
||||
|
||||
const prettyError = useMemo(() => normalizeAuthMessage(lastError), [lastError]);
|
||||
const showVerifyHelp = useMemo(() => isEmailVerifyError(lastError), [lastError]);
|
||||
|
||||
const isVerifiedPage =
|
||||
typeof window !== "undefined" && window.location?.pathname === "/auth/verified";
|
||||
|
||||
const openModal = (nextMode) => {
|
||||
setMode(nextMode);
|
||||
setShowAuth(true);
|
||||
setSignupError("");
|
||||
setSignupInfo("");
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setShowAuth(false);
|
||||
setLoginPass("");
|
||||
setSignupError("");
|
||||
setSignupInfo("");
|
||||
};
|
||||
|
||||
// ✅ If error happens, open modal so errors are not in topbar
|
||||
useEffect(() => {
|
||||
if (prettyError) {
|
||||
setMode("login");
|
||||
setShowAuth(true);
|
||||
}
|
||||
}, [prettyError]);
|
||||
|
||||
// ✅ On /auth/verified: open login + prefill username
|
||||
useEffect(() => {
|
||||
if (!isVerifiedPage) return;
|
||||
setMode("login");
|
||||
setShowAuth(true);
|
||||
|
||||
try {
|
||||
const lastU = localStorage.getItem(LAST_SIGNUP_USER_KEY) || "";
|
||||
if (lastU && !loginUser) setLoginUser(lastU);
|
||||
} catch {}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVerifiedPage]);
|
||||
|
||||
// ✅ After login on /auth/verified, go home
|
||||
useEffect(() => {
|
||||
if (!authenticated) return;
|
||||
if (typeof window === "undefined") return;
|
||||
const path = window.location?.pathname || "";
|
||||
if (path === "/auth/verified") {
|
||||
try { window.history.replaceState({}, "", "/"); } catch {}
|
||||
}
|
||||
}, [authenticated]);
|
||||
|
||||
const handleLoginSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
await login(loginUser, loginPass);
|
||||
};
|
||||
|
||||
const handleSignupSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSignupError("");
|
||||
setSignupInfo("");
|
||||
|
||||
if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
|
||||
setSignupError("First name, last name, username, email and password are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSignupLoading(true);
|
||||
|
||||
await registerUser({
|
||||
username: regUser.trim(),
|
||||
email: regEmail.trim(),
|
||||
password: regPass,
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
});
|
||||
|
||||
try { localStorage.setItem(LAST_SIGNUP_USER_KEY, regUser.trim()); } catch {}
|
||||
|
||||
// ✅ Do NOT auto-login (verify email flow)
|
||||
setSignupInfo("Compte créé ✅ Vérifie ton email (spam inclus), puis connecte-toi.");
|
||||
setMode("login");
|
||||
setLoginUser(regUser.trim());
|
||||
setLoginPass("");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setSignupError(err.message || "Registration error.");
|
||||
} finally {
|
||||
setSignupLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const NoticeBox = ({ variant, children }) => (
|
||||
<div className={"sw-notice sw-notice--" + variant}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="topbar">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
|
||||
<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>
|
||||
</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.45rem" }}>
|
||||
{/* ✅ one single user chip */}
|
||||
<div className="user-chip" title={username || ""}>
|
||||
<span className="user-avatar">
|
||||
<i className="fa-solid fa-user-astronaut" />
|
||||
</span>
|
||||
<span className="user-name">{username || "user"}</span>
|
||||
<span className="user-badge">
|
||||
<i className="fa-solid fa-circle-check" />
|
||||
Connecté
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button className="btn-primary" onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", gap: "0.3rem" }}>
|
||||
<button type="button" className="btn-primary" onClick={() => openModal("login")}>
|
||||
Login
|
||||
</button>
|
||||
<button type="button" 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>
|
||||
|
||||
{/* ✅ Verified page success message */}
|
||||
{isVerifiedPage && !authenticated && (
|
||||
<NoticeBox variant="success">
|
||||
<b>✅ Email confirmé.</b> Connecte-toi pour continuer.
|
||||
</NoticeBox>
|
||||
)}
|
||||
|
||||
{/* ✅ Any error/info now lives INSIDE modal */}
|
||||
{prettyError && (
|
||||
<NoticeBox variant="error">
|
||||
<i className="fa-solid fa-circle-xmark" style={{ marginRight: 8 }} />
|
||||
{prettyError}
|
||||
</NoticeBox>
|
||||
)}
|
||||
|
||||
{!prettyError && lastInfo && (
|
||||
<NoticeBox variant="info">
|
||||
<i className="fa-solid fa-circle-info" style={{ marginRight: 8 }} />
|
||||
{lastInfo}
|
||||
</NoticeBox>
|
||||
)}
|
||||
|
||||
{/* ✅ Email verify helper buttons */}
|
||||
{showVerifyHelp && (
|
||||
<NoticeBox variant="warn">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<div style={{ fontWeight: 900 }}>
|
||||
<i className="fa-solid fa-triangle-exclamation" style={{ marginRight: 8 }} />
|
||||
Vérification email requise
|
||||
</div>
|
||||
<div style={{ opacity: 0.92, lineHeight: 1.25 }}>
|
||||
Ton login “modal” ne peut pas compléter l’action Keycloak. Ouvre la page Keycloak,
|
||||
clique “proceed”, puis clique le lien reçu par email.
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
|
||||
<a className="btn-secondary" href={buildKeycloakAuthUrl()}>
|
||||
Vérifier mon email (Keycloak)
|
||||
</a>
|
||||
{typeof resendVerificationEmail === "function" && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
onClick={resendVerificationEmail}
|
||||
>
|
||||
Renvoyer l’email
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NoticeBox>
|
||||
)}
|
||||
|
||||
{mode === "login" ? (
|
||||
<form className="auth-form" onSubmit={handleLoginSubmit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
value={loginUser}
|
||||
onChange={(e) => setLoginUser(e.target.value)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
value={loginPass}
|
||||
onChange={(e) => setLoginPass(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="btn-primary btn-full" disabled={loading}>
|
||||
{loading ? "..." : "Sign in"}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<form className="auth-form" onSubmit={handleSignupSubmit}>
|
||||
<div className="auth-row">
|
||||
<label>
|
||||
First name
|
||||
<input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Last name
|
||||
<input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Username
|
||||
<input type="text" value={regUser} onChange={(e) => setRegUser(e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input type="email" value={regEmail} onChange={(e) => setRegEmail(e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" value={regPass} onChange={(e) => setRegPass(e.target.value)} />
|
||||
</label>
|
||||
|
||||
{signupError && <NoticeBox variant="error">{signupError}</NoticeBox>}
|
||||
{signupInfo && <NoticeBox variant="success">{signupInfo}</NoticeBox>}
|
||||
|
||||
<button type="submit" className="btn-primary btn-full" disabled={signupLoading}>
|
||||
{signupLoading ? "..." : "Create account"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue