feat(email): Adding email options for registration
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
<!-- Auth Modal ──────────────────────────────────────────────────────────── -->
|
||||
<div class="authOverlay" id="authOverlay">
|
||||
<div class="authModal">
|
||||
<div class="authTabs">
|
||||
<!-- Normal tabs (login / register) -->
|
||||
<div class="authTabs" id="authTabsBar">
|
||||
<button type="button" class="authTab authTab--active" id="tabLogin">Se connecter</button>
|
||||
<button type="button" class="authTab" id="tabRegister">S'enregistrer</button>
|
||||
</div>
|
||||
@@ -27,6 +28,7 @@
|
||||
</div>
|
||||
<div class="authError hidden" id="loginError"></div>
|
||||
<button type="submit" class="authSubmit">Se connecter</button>
|
||||
<button type="button" class="authLink" id="forgotPasswordBtn">Mot de passe oublié ?</button>
|
||||
</form>
|
||||
|
||||
<!-- Register form -->
|
||||
@@ -59,6 +61,51 @@
|
||||
<div class="authError hidden" id="registerError"></div>
|
||||
<button type="submit" class="authSubmit">Créer le compte</button>
|
||||
</form>
|
||||
|
||||
<!-- Forgot password form -->
|
||||
<div class="authPanel hidden" id="forgotPanel">
|
||||
<p class="authPanelTitle">Mot de passe oublié</p>
|
||||
<p class="authPanelDesc">Entrez votre adresse courriel pour recevoir un lien de réinitialisation.</p>
|
||||
<form id="forgotForm">
|
||||
<div class="authField">
|
||||
<label>Adresse courriel</label>
|
||||
<input type="email" id="forgotEmail" autocomplete="email" required />
|
||||
</div>
|
||||
<div class="authError hidden" id="forgotError"></div>
|
||||
<button type="submit" class="authSubmit">Envoyer le lien</button>
|
||||
</form>
|
||||
<button type="button" class="authLink" id="backToLoginBtn">← Retour à la connexion</button>
|
||||
</div>
|
||||
|
||||
<!-- Check your email message (after register or forgot password) -->
|
||||
<div class="authPanel hidden" id="checkEmailPanel">
|
||||
<div class="authSuccessIcon">✉</div>
|
||||
<p class="authPanelTitle" id="checkEmailTitle">Vérifiez votre courriel</p>
|
||||
<p class="authPanelDesc" id="checkEmailMsg">Un email de confirmation a été envoyé. Cliquez sur le lien dans l'email pour activer votre compte.</p>
|
||||
<button type="button" class="authSubmit authSubmit--ghost" id="resendEmailBtn">Renvoyer l'email</button>
|
||||
<button type="button" class="authLink" id="backToLoginBtn2">← Retour à la connexion</button>
|
||||
</div>
|
||||
|
||||
<!-- Reset password form (shown when ?reset=TOKEN is in URL) -->
|
||||
<div class="authPanel hidden" id="resetPanel">
|
||||
<p class="authPanelTitle">Nouveau mot de passe</p>
|
||||
<form id="resetForm">
|
||||
<div class="authField">
|
||||
<label>Nouveau mot de passe <span class="authHint">(6 caractères min.)</span></label>
|
||||
<input type="password" id="resetPassword" autocomplete="new-password" required />
|
||||
</div>
|
||||
<div class="authError hidden" id="resetError"></div>
|
||||
<button type="submit" class="authSubmit">Réinitialiser</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Email confirmed success (shown after redirect) -->
|
||||
<div class="authPanel hidden" id="confirmedPanel">
|
||||
<div class="authSuccessIcon">✓</div>
|
||||
<p class="authPanelTitle">Email confirmé !</p>
|
||||
<p class="authPanelDesc">Votre adresse email a été confirmée. Vous pouvez maintenant vous connecter.</p>
|
||||
<button type="button" class="authSubmit" id="goToLoginBtn">Se connecter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -50,6 +50,30 @@ export async function apiRegister(username, email, password, team) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function apiResendConfirmation(email) {
|
||||
return fetch("/api/auth/resend-confirmation", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function apiForgotPassword(email) {
|
||||
return fetch("/api/auth/forgot-password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function apiResetPassword(token, password) {
|
||||
return fetch("/api/auth/reset-password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token, password }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function apiGetMe(token) {
|
||||
return fetch("/api/auth/me", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { apiLogin, apiRegister, apiGetMe } from "./api.js";
|
||||
import { apiLogin, apiRegister, apiGetMe, apiResendConfirmation, apiForgotPassword, apiResetPassword } from "./api.js";
|
||||
import { setCurrentTeam, refreshFromServer } from "./game.js";
|
||||
|
||||
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const authOverlay = document.getElementById("authOverlay");
|
||||
const authTabsBar = document.getElementById("authTabsBar");
|
||||
const tabLogin = document.getElementById("tabLogin");
|
||||
const tabRegister = document.getElementById("tabRegister");
|
||||
const loginForm = document.getElementById("loginForm");
|
||||
const registerForm = document.getElementById("registerForm");
|
||||
const forgotPanel = document.getElementById("forgotPanel");
|
||||
const checkEmailPanel = document.getElementById("checkEmailPanel");
|
||||
const resetPanel = document.getElementById("resetPanel");
|
||||
const confirmedPanel = document.getElementById("confirmedPanel");
|
||||
const loginUsernameEl = document.getElementById("loginUsername");
|
||||
const loginPasswordEl = document.getElementById("loginPassword");
|
||||
const loginErrorEl = document.getElementById("loginError");
|
||||
@@ -15,6 +20,14 @@ const regUsernameEl = document.getElementById("regUsername");
|
||||
const regEmailEl = document.getElementById("regEmail");
|
||||
const regPasswordEl = document.getElementById("regPassword");
|
||||
const registerErrorEl = document.getElementById("registerError");
|
||||
const forgotForm = document.getElementById("forgotForm");
|
||||
const forgotEmailEl = document.getElementById("forgotEmail");
|
||||
const forgotErrorEl = document.getElementById("forgotError");
|
||||
const resetForm = document.getElementById("resetForm");
|
||||
const resetPasswordEl = document.getElementById("resetPassword");
|
||||
const resetErrorEl = document.getElementById("resetError");
|
||||
const checkEmailMsg = document.getElementById("checkEmailMsg");
|
||||
const resendEmailBtn = document.getElementById("resendEmailBtn");
|
||||
const userDisplayEl = document.getElementById("userDisplay");
|
||||
const logoutBtn = document.getElementById("logoutBtn");
|
||||
|
||||
@@ -23,11 +36,39 @@ const logoutBtn = document.getElementById("logoutBtn");
|
||||
export let authToken = localStorage.getItem("authToken") ?? null;
|
||||
export let currentUser = null;
|
||||
|
||||
// Tracks which email to use for resend (set after register)
|
||||
let pendingEmail = null;
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function showError(el, msg) { el.textContent = msg; el.classList.remove("hidden"); }
|
||||
function clearError(el) { el.textContent = ""; el.classList.add("hidden"); }
|
||||
|
||||
// Show only one panel, hide all others
|
||||
const allPanels = [loginForm, registerForm, forgotPanel, checkEmailPanel, resetPanel, confirmedPanel];
|
||||
|
||||
function showPanel(panel) {
|
||||
for (const p of allPanels) p.classList.add("hidden");
|
||||
authTabsBar.classList.add("hidden");
|
||||
panel.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function showLoginTab() {
|
||||
for (const p of allPanels) p.classList.add("hidden");
|
||||
authTabsBar.classList.remove("hidden");
|
||||
loginForm.classList.remove("hidden");
|
||||
tabLogin.classList.add("authTab--active");
|
||||
tabRegister.classList.remove("authTab--active");
|
||||
}
|
||||
|
||||
function showRegisterTab() {
|
||||
for (const p of allPanels) p.classList.add("hidden");
|
||||
authTabsBar.classList.remove("hidden");
|
||||
registerForm.classList.remove("hidden");
|
||||
tabRegister.classList.add("authTab--active");
|
||||
tabLogin.classList.remove("authTab--active");
|
||||
}
|
||||
|
||||
export function showAuthOverlay() { authOverlay.classList.remove("hidden"); }
|
||||
export function hideAuthOverlay() { authOverlay.classList.add("hidden"); }
|
||||
|
||||
@@ -49,6 +90,7 @@ function logout() {
|
||||
userDisplayEl.textContent = "—";
|
||||
logoutBtn.classList.add("hidden");
|
||||
showAuthOverlay();
|
||||
showLoginTab();
|
||||
}
|
||||
|
||||
// ── Session restore ───────────────────────────────────────────────────────────
|
||||
@@ -66,21 +108,63 @@ export async function tryRestoreSession() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Handle URL params (email confirmation / password reset) ───────────────────
|
||||
|
||||
export function handleUrlEmailFlows() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (params.has("email_confirm")) {
|
||||
// Trigger confirmation via API redirect — this is handled server-side.
|
||||
// The server redirects to /?email_confirmed=1 or /?confirm_error=...
|
||||
const token = params.get("email_confirm");
|
||||
window.history.replaceState({}, "", "/");
|
||||
// Call server-side confirm endpoint by navigating (GET request)
|
||||
window.location.href = `/api/auth/confirm-email?token=${encodeURIComponent(token)}`;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (params.has("email_confirmed")) {
|
||||
window.history.replaceState({}, "", "/");
|
||||
showAuthOverlay();
|
||||
showPanel(confirmedPanel);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (params.has("confirm_error")) {
|
||||
const err = params.get("confirm_error");
|
||||
window.history.replaceState({}, "", "/");
|
||||
showAuthOverlay();
|
||||
showLoginTab();
|
||||
const msgs = {
|
||||
expired: "Le lien de confirmation a expiré. Connectez-vous pour en recevoir un nouveau.",
|
||||
invalid: "Lien de confirmation invalide.",
|
||||
server_error: "Erreur serveur. Réessayez plus tard.",
|
||||
};
|
||||
showError(loginErrorEl, msgs[err] ?? "Erreur de confirmation.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (params.has("reset")) {
|
||||
const token = params.get("reset");
|
||||
window.history.replaceState({}, "", "/");
|
||||
showAuthOverlay();
|
||||
resetPanel.dataset.token = token;
|
||||
showPanel(resetPanel);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Tab switching ─────────────────────────────────────────────────────────────
|
||||
|
||||
tabLogin.addEventListener("click", () => {
|
||||
tabLogin.classList.add("authTab--active");
|
||||
tabRegister.classList.remove("authTab--active");
|
||||
loginForm.classList.remove("hidden");
|
||||
registerForm.classList.add("hidden");
|
||||
showLoginTab();
|
||||
clearError(loginErrorEl);
|
||||
});
|
||||
|
||||
tabRegister.addEventListener("click", () => {
|
||||
tabRegister.classList.add("authTab--active");
|
||||
tabLogin.classList.remove("authTab--active");
|
||||
registerForm.classList.remove("hidden");
|
||||
loginForm.classList.add("hidden");
|
||||
showRegisterTab();
|
||||
clearError(registerErrorEl);
|
||||
});
|
||||
|
||||
@@ -96,15 +180,23 @@ loginForm.addEventListener("submit", async (e) => {
|
||||
const res = await apiLogin(username, password);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
const msgs = { invalid_credentials: "Invalid username or password.", missing_fields: "Please fill in all fields." };
|
||||
showError(loginErrorEl, msgs[data.error] ?? "Login failed.");
|
||||
if (data.error === "email_not_verified") {
|
||||
// Show check-email panel with resend option
|
||||
pendingEmail = null; // email unknown here, ask user to re-register or resend
|
||||
checkEmailMsg.textContent = "Votre adresse email n'a pas encore été confirmée. Vérifiez votre boîte de réception ou renvoyez l'email de confirmation.";
|
||||
resendEmailBtn.dataset.email = "";
|
||||
showPanel(checkEmailPanel);
|
||||
return;
|
||||
}
|
||||
const msgs = { invalid_credentials: "Identifiant ou mot de passe invalide.", missing_fields: "Veuillez remplir tous les champs." };
|
||||
showError(loginErrorEl, msgs[data.error] ?? "Connexion échouée.");
|
||||
return;
|
||||
}
|
||||
applyUser(data.user, data.token);
|
||||
hideAuthOverlay();
|
||||
await refreshFromServer();
|
||||
} catch {
|
||||
showError(loginErrorEl, "Network error. Try again.");
|
||||
showError(loginErrorEl, "Erreur réseau. Réessayez.");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,30 +209,134 @@ registerForm.addEventListener("submit", async (e) => {
|
||||
const email = regEmailEl.value.trim();
|
||||
const password = regPasswordEl.value;
|
||||
const teamInput = registerForm.querySelector('input[name="regTeam"]:checked');
|
||||
if (!teamInput) { showError(registerErrorEl, "Please choose a team."); return; }
|
||||
if (!teamInput) { showError(registerErrorEl, "Veuillez choisir une équipe."); return; }
|
||||
try {
|
||||
const res = await apiRegister(username, email, password, teamInput.value);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
const msgs = {
|
||||
username_taken: "This username is already taken.",
|
||||
email_taken: "This email is already registered.",
|
||||
password_too_short:"Password must be at least 6 characters.",
|
||||
invalid_username: "Username must be 2–32 characters.",
|
||||
missing_fields: "Please fill in all fields.",
|
||||
invalid_team: "Invalid team selected.",
|
||||
username_taken: "Ce nom d'utilisateur est déjà pris.",
|
||||
email_taken: "Cette adresse email est déjà utilisée.",
|
||||
password_too_short:"Le mot de passe doit comporter au moins 6 caractères.",
|
||||
invalid_username: "Le nom d'utilisateur doit comporter entre 2 et 32 caractères.",
|
||||
missing_fields: "Veuillez remplir tous les champs.",
|
||||
invalid_team: "Équipe invalide.",
|
||||
};
|
||||
showError(registerErrorEl, msgs[data.error] ?? "Registration failed.");
|
||||
showError(registerErrorEl, msgs[data.error] ?? "Inscription échouée.");
|
||||
return;
|
||||
}
|
||||
applyUser(data.user, data.token);
|
||||
hideAuthOverlay();
|
||||
await refreshFromServer();
|
||||
// Registration successful — pending email confirmation
|
||||
pendingEmail = email;
|
||||
resendEmailBtn.dataset.email = email;
|
||||
checkEmailMsg.textContent = `Un email de confirmation a été envoyé à ${email}. Cliquez sur le lien pour activer votre compte.`;
|
||||
document.getElementById("checkEmailTitle").textContent = "Vérifiez votre courriel";
|
||||
showPanel(checkEmailPanel);
|
||||
} catch {
|
||||
showError(registerErrorEl, "Network error. Try again.");
|
||||
showError(registerErrorEl, "Erreur réseau. Réessayez.");
|
||||
}
|
||||
});
|
||||
|
||||
// ── Forgot password ───────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById("forgotPasswordBtn").addEventListener("click", () => {
|
||||
clearError(forgotErrorEl);
|
||||
forgotEmailEl.value = "";
|
||||
showPanel(forgotPanel);
|
||||
});
|
||||
|
||||
document.getElementById("backToLoginBtn").addEventListener("click", () => {
|
||||
showLoginTab();
|
||||
clearError(loginErrorEl);
|
||||
});
|
||||
|
||||
forgotForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearError(forgotErrorEl);
|
||||
const email = forgotEmailEl.value.trim();
|
||||
if (!email) return;
|
||||
try {
|
||||
await apiForgotPassword(email);
|
||||
// Always show the same message regardless of whether email exists (anti-enumeration)
|
||||
checkEmailMsg.textContent = `Si un compte existe pour ${email}, un lien de réinitialisation a été envoyé.`;
|
||||
document.getElementById("checkEmailTitle").textContent = "Email envoyé";
|
||||
resendEmailBtn.dataset.email = "";
|
||||
resendEmailBtn.classList.add("hidden");
|
||||
showPanel(checkEmailPanel);
|
||||
} catch {
|
||||
showError(forgotErrorEl, "Erreur réseau. Réessayez.");
|
||||
}
|
||||
});
|
||||
|
||||
// ── Check-email panel ─────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById("backToLoginBtn2").addEventListener("click", () => {
|
||||
resendEmailBtn.classList.remove("hidden");
|
||||
showLoginTab();
|
||||
clearError(loginErrorEl);
|
||||
});
|
||||
|
||||
resendEmailBtn.addEventListener("click", async () => {
|
||||
const email = resendEmailBtn.dataset.email;
|
||||
if (!email) return;
|
||||
resendEmailBtn.disabled = true;
|
||||
resendEmailBtn.textContent = "Envoi…";
|
||||
try {
|
||||
await apiResendConfirmation(email);
|
||||
resendEmailBtn.textContent = "Email renvoyé !";
|
||||
} catch {
|
||||
resendEmailBtn.textContent = "Erreur — réessayez";
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
resendEmailBtn.disabled = false;
|
||||
resendEmailBtn.textContent = "Renvoyer l'email";
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Reset password form ───────────────────────────────────────────────────────
|
||||
|
||||
resetForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
clearError(resetErrorEl);
|
||||
const token = resetPanel.dataset.token;
|
||||
const password = resetPasswordEl.value;
|
||||
if (!token) { showError(resetErrorEl, "Token manquant."); return; }
|
||||
try {
|
||||
const res = await apiResetPassword(token, password);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
const msgs = {
|
||||
invalid_token: "Ce lien est invalide.",
|
||||
token_expired: "Ce lien a expiré. Refaites une demande.",
|
||||
password_too_short: "Le mot de passe doit comporter au moins 6 caractères.",
|
||||
missing_fields: "Veuillez remplir tous les champs.",
|
||||
};
|
||||
showError(resetErrorEl, msgs[data.error] ?? "Erreur lors de la réinitialisation.");
|
||||
return;
|
||||
}
|
||||
showLoginTab();
|
||||
showError(loginErrorEl, ""); // clear
|
||||
clearError(loginErrorEl);
|
||||
// Show success in login form briefly
|
||||
loginErrorEl.style.color = "rgba(100,220,100,0.95)";
|
||||
loginErrorEl.style.background = "rgba(30,100,30,0.12)";
|
||||
loginErrorEl.style.border = "1px solid rgba(30,100,30,0.25)";
|
||||
showError(loginErrorEl, "Mot de passe réinitialisé ! Vous pouvez vous connecter.");
|
||||
setTimeout(() => {
|
||||
loginErrorEl.removeAttribute("style");
|
||||
clearError(loginErrorEl);
|
||||
}, 6000);
|
||||
} catch {
|
||||
showError(resetErrorEl, "Erreur réseau. Réessayez.");
|
||||
}
|
||||
});
|
||||
|
||||
// ── Confirmed panel ───────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById("goToLoginBtn").addEventListener("click", () => {
|
||||
showLoginTab();
|
||||
});
|
||||
|
||||
// ── Logout ────────────────────────────────────────────────────────────────────
|
||||
|
||||
logoutBtn.addEventListener("click", logout);
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
tryRestoreSession,
|
||||
showAuthOverlay,
|
||||
hideAuthOverlay,
|
||||
handleUrlEmailFlows,
|
||||
} from "./auth.js";
|
||||
|
||||
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
||||
@@ -90,11 +91,15 @@ async function boot() {
|
||||
// Load the SVG playfield mask before any drawing or data fetch
|
||||
await loadPlayfieldMask();
|
||||
|
||||
const restored = await tryRestoreSession();
|
||||
if (!restored) {
|
||||
showAuthOverlay();
|
||||
} else {
|
||||
hideAuthOverlay();
|
||||
// Handle email confirmation / password reset URL params first
|
||||
const urlHandled = handleUrlEmailFlows();
|
||||
if (!urlHandled) {
|
||||
const restored = await tryRestoreSession();
|
||||
if (!restored) {
|
||||
showAuthOverlay();
|
||||
} else {
|
||||
hideAuthOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -192,6 +192,76 @@ body {
|
||||
background: rgba(113, 199, 255, 0.28);
|
||||
}
|
||||
|
||||
.authSubmit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.authSubmit--ghost {
|
||||
background: transparent;
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.authSubmit--ghost:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
/* Link-style button inside auth modal */
|
||||
.authLink {
|
||||
margin-top: 2px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(113, 199, 255, 0.75);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.authLink:hover {
|
||||
color: rgba(113, 199, 255, 1);
|
||||
}
|
||||
|
||||
/* Free-standing panel (not a form) inside the auth modal */
|
||||
.authPanel {
|
||||
padding: 28px 22px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.authPanel.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.authPanel form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.authPanelTitle {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #e9eef6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.authPanelDesc {
|
||||
font-size: 13px;
|
||||
color: rgba(233, 238, 246, 0.65);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.authSuccessIcon {
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
|
||||
/* ── Score board ──────────────────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user