code created by GPT-5.3-Codex

This commit is contained in:
Fuzzy Book
2026-02-15 17:09:19 +01:00
parent c30f8cd020
commit 48c0bec24e
20 changed files with 2771 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bag Exchange | Forgot Password</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-stone-200 text-zinc-800">
<main class="mx-auto grid min-h-screen w-full max-w-2xl place-items-center px-4 py-8">
<section class="w-full max-w-xl rounded-2xl border border-zinc-300/70 bg-amber-50/80 p-6 shadow-2xl shadow-stone-700/10 backdrop-blur-sm">
<div class="mb-4 flex justify-end">
<select id="language-select" class="rounded-lg border border-zinc-300 bg-white px-2 py-1 text-sm">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
</div>
<h1 class="mb-2 text-3xl font-bold tracking-tight" data-i18n="title">Recover your password</h1>
<p class="mb-4 text-sm text-zinc-700" id="intro-text" data-i18n="subtitle">Enter your email and we will send you a password reset link if your account exists.</p>
<form class="grid gap-3" id="forgot-form">
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="email" type="email" required data-i18n-placeholder="emailPlaceholder" placeholder="Your email" />
<button class="rounded-xl bg-orange-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-orange-600 disabled:cursor-not-allowed disabled:opacity-60" id="submit-btn" type="submit" data-i18n="submit">Send reset link</button>
</form>
<div class="mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed" id="feedback"></div>
<a class="mt-4 inline-block text-sm text-emerald-900 underline decoration-1 underline-offset-2" href="/" data-lang-link data-i18n="homeLink">Back to homepage</a>
</section>
</main>
<script src="/static/js/simple_i18n.js"></script>
<script>
var i18n = window.initPageI18n({
en: { pageTitle: "Bag Exchange | Forgot Password", title: "Recover your password", subtitle: "Enter your email and we will send you a password reset link if your account exists.", emailPlaceholder: "Your email", submit: "Send reset link", homeLink: "Back to homepage", msgInvalid: "Enter a valid email address.", msgOk: "If the account exists, a reset link has been sent.", msgError: "Unable to process request. Try again." },
it: { pageTitle: "Bag Exchange | Recupero Password", title: "Recupera la tua password", subtitle: "Inserisci la tua email e invieremo un link di reset se l'account esiste.", emailPlaceholder: "La tua email", submit: "Invia link di reset", homeLink: "Torna alla home", msgInvalid: "Inserisci un indirizzo email valido.", msgOk: "Se l'account esiste, e stato inviato un link di reset.", msgError: "Impossibile elaborare la richiesta. Riprova." },
fr: { pageTitle: "Bag Exchange | Recuperation du mot de passe", title: "Recuperer votre mot de passe", subtitle: "Entrez votre email et nous enverrons un lien de reinitialisation si le compte existe.", emailPlaceholder: "Votre email", submit: "Envoyer le lien", homeLink: "Retour a l'accueil", msgInvalid: "Entrez une adresse email valide.", msgOk: "Si le compte existe, un lien de reinitialisation a ete envoye.", msgError: "Impossible de traiter la demande. Veuillez reessayer." },
de: { pageTitle: "Bag Exchange | Passwort wiederherstellen", title: "Passwort wiederherstellen", subtitle: "Gib deine E-Mail ein und wir senden einen Reset-Link, falls das Konto existiert.", emailPlaceholder: "Deine E-Mail", submit: "Reset-Link senden", homeLink: "Zur Startseite", msgInvalid: "Bitte eine gueltige E-Mail-Adresse eingeben.", msgOk: "Falls das Konto existiert, wurde ein Reset-Link gesendet.", msgError: "Anfrage konnte nicht verarbeitet werden. Bitte erneut versuchen." },
es: { pageTitle: "Bag Exchange | Recuperar contrasena", title: "Recupera tu contrasena", subtitle: "Introduce tu email y enviaremos un enlace de restablecimiento si la cuenta existe.", emailPlaceholder: "Tu email", submit: "Enviar enlace", homeLink: "Volver al inicio", msgInvalid: "Introduce un correo electronico valido.", msgOk: "Si la cuenta existe, se ha enviado un enlace de restablecimiento.", msgError: "No se pudo procesar la solicitud. Intentalo de nuevo." }
});
(function () {
var form = document.getElementById("forgot-form");
var emailInput = document.getElementById("email");
var submitButton = document.getElementById("submit-btn");
var feedback = document.getElementById("feedback");
var introText = document.getElementById("intro-text");
function showMessage(kind, text) {
feedback.className = "mt-4 rounded-xl px-3 py-2 text-sm leading-relaxed";
if (kind === "error") {
feedback.classList.add("bg-red-100", "text-red-800");
} else {
feedback.classList.add("bg-emerald-100", "text-emerald-900");
}
feedback.textContent = text;
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
var email = emailInput.value.trim();
if (!email || !emailInput.checkValidity()) {
showMessage("error", i18n.t("msgInvalid"));
return;
}
submitButton.disabled = true;
feedback.className = "mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed";
feedback.textContent = "";
try {
var response = await fetch("/api/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email })
});
if (response.ok) {
showMessage("ok", i18n.t("msgOk"));
form.reset();
form.classList.add("hidden");
if (introText) introText.classList.add("hidden");
} else {
showMessage("error", i18n.t("msgError"));
}
} catch (err) {
showMessage("error", i18n.t("msgError"));
} finally {
submitButton.disabled = false;
}
});
})();
</script>
</body>
</html>

58
templates/howtowork.html Normal file
View File

@@ -0,0 +1,58 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bag Exchange | How To Work</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-stone-200 text-zinc-800">
<main class="mx-auto w-full max-w-4xl px-4 py-10">
<div class="mb-6 flex items-center justify-between gap-4">
<a href="/" data-lang-link class="inline-block text-sm text-emerald-900 underline decoration-1 underline-offset-2" data-i18n="backHome">Back to homepage</a>
<select id="language-select" class="rounded-lg border border-zinc-300 bg-white px-2 py-1 text-sm">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
</div>
<section class="rounded-2xl border border-zinc-300/70 bg-amber-50/80 p-6 shadow-2xl shadow-stone-700/10 backdrop-blur-sm">
<h1 class="text-3xl font-bold tracking-tight" data-i18n="title">How Bag Exchange works</h1>
<p class="mt-2 text-sm text-zinc-700" data-i18n="subtitle">Three simple steps to publish and exchange your bag.</p>
<ol class="mt-6 grid gap-4">
<li class="rounded-xl border border-zinc-300/70 bg-white/70 p-4">
<h2 class="text-lg font-semibold" data-i18n="step1Title">1. Create your bag gallery</h2>
<p class="mt-1 text-sm leading-relaxed text-zinc-700" data-i18n="step1Desc">Upload clear photos from multiple angles and include details about model, size, and materials.</p>
</li>
<li class="rounded-xl border border-zinc-300/70 bg-white/70 p-4">
<h2 class="text-lg font-semibold" data-i18n="step2Title">2. Describe your product</h2>
<p class="mt-1 text-sm leading-relaxed text-zinc-700" data-i18n="step2Desc">Write a transparent description with defects, usage history, and what makes the item special.</p>
</li>
<li class="rounded-xl border border-zinc-300/70 bg-white/70 p-4">
<h2 class="text-lg font-semibold" data-i18n="step3Title">3. Set exchange conditions</h2>
<p class="mt-1 text-sm leading-relaxed text-zinc-700" data-i18n="step3Desc">Define what you are looking for in exchange, shipping preferences, and availability for videochat.</p>
</li>
</ol>
<div class="mt-6 rounded-xl border border-emerald-900/15 bg-emerald-50 p-4">
<p class="text-sm text-emerald-900" data-i18n="tip">Tip: complete profiles and accurate listings get better quality matches.</p>
</div>
</section>
</main>
<script src="/static/js/simple_i18n.js"></script>
<script>
window.initPageI18n({
en: { pageTitle: "Bag Exchange | How To Work", backHome: "Back to homepage", title: "How Bag Exchange works", subtitle: "Three simple steps to publish and exchange your bag.", step1Title: "1. Create your bag gallery", step1Desc: "Upload clear photos from multiple angles and include details about model, size, and materials.", step2Title: "2. Describe your product", step2Desc: "Write a transparent description with defects, usage history, and what makes the item special.", step3Title: "3. Set exchange conditions", step3Desc: "Define what you are looking for in exchange, shipping preferences, and availability for videochat.", tip: "Tip: complete profiles and accurate listings get better quality matches." },
it: { pageTitle: "Bag Exchange | Come Funziona", backHome: "Torna alla home", title: "Come funziona Bag Exchange", subtitle: "Tre passaggi semplici per pubblicare e scambiare la tua borsa.", step1Title: "1. Crea la galleria della tua borsa", step1Desc: "Carica foto chiare da diverse angolazioni e aggiungi dettagli su modello, dimensioni e materiali.", step2Title: "2. Descrivi il prodotto", step2Desc: "Scrivi una descrizione trasparente con difetti, stato d'uso e caratteristiche distintive.", step3Title: "3. Definisci le condizioni di scambio", step3Desc: "Specifica cosa cerchi in cambio, preferenze di spedizione e disponibilita per videochat.", tip: "Suggerimento: profili completi e annunci accurati ottengono match migliori." },
fr: { pageTitle: "Bag Exchange | Comment ca marche", backHome: "Retour a l'accueil", title: "Comment fonctionne Bag Exchange", subtitle: "Trois etapes simples pour publier et echanger votre sac.", step1Title: "1. Creez la galerie de votre sac", step1Desc: "Ajoutez des photos claires sous plusieurs angles et des details sur le modele, la taille et les materiaux.", step2Title: "2. Decrivez le produit", step2Desc: "Redigez une description transparente avec les defauts, l'etat d'usage et les points forts.", step3Title: "3. Definissez les conditions d'echange", step3Desc: "Indiquez ce que vous recherchez, les preferences de livraison et la disponibilite pour videochat.", tip: "Astuce : des profils complets et des annonces precises obtiennent de meilleurs matchs." },
de: { pageTitle: "Bag Exchange | So funktioniert es", backHome: "Zur Startseite", title: "So funktioniert Bag Exchange", subtitle: "Drei einfache Schritte, um deine Tasche zu veroeffentlichen und zu tauschen.", step1Title: "1. Erstelle deine Taschen-Galerie", step1Desc: "Lade klare Fotos aus mehreren Winkeln hoch und fuege Details zu Modell, Groesse und Material hinzu.", step2Title: "2. Beschreibe das Produkt", step2Desc: "Schreibe eine transparente Beschreibung mit Maengeln, Nutzungszustand und besonderen Merkmalen.", step3Title: "3. Lege Tauschbedingungen fest", step3Desc: "Definiere, was du im Tausch suchst, Versandpraeferenzen und Verfuegbarkeit fuer Videochat.", tip: "Tipp: Vollstaendige Profile und praezise Anzeigen liefern bessere Matches." },
es: { pageTitle: "Bag Exchange | Como funciona", backHome: "Volver al inicio", title: "Como funciona Bag Exchange", subtitle: "Tres pasos simples para publicar e intercambiar tu bolso.", step1Title: "1. Crea la galeria de tu bolso", step1Desc: "Sube fotos claras desde varios angulos e incluye detalles del modelo, tamano y materiales.", step2Title: "2. Describe el producto", step2Desc: "Escribe una descripcion transparente con defectos, estado de uso y puntos destacados.", step3Title: "3. Define las condiciones de intercambio", step3Desc: "Indica que buscas a cambio, preferencias de envio y disponibilidad para videochat.", tip: "Consejo: perfiles completos y anuncios precisos consiguen mejores coincidencias." }
});
</script>
</body>
</html>

102
templates/index.html Normal file
View File

@@ -0,0 +1,102 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{.InitialTitle}}</title>
<link rel="stylesheet" href="/static/css/main.css?v={{.AssetVersion}}" />
</head>
<body>
<div class="container">
<header>
<div>
<div class="logo">{{.Brand}}</div>
<div class="auth-row">
<p class="auth-status is-hidden" id="auth-status"></p>
<a class="btn btn-secondary auth-logout" id="login-btn" href="/login" data-i18n="ctaLogin">Login</a>
<a class="btn btn-secondary auth-logout" id="register-btn" href="/signup" data-i18n="ctaRegister">Create account</a>
<button class="btn btn-secondary auth-logout is-hidden" id="logout-btn" type="button" data-i18n="ctaLogout">Logout</button>
</div>
</div>
<div class="header-actions">
<div class="lang-wrap">
<div class="lang-flags" aria-hidden="true">
<span title="English">🇬🇧</span>
<span title="Italiano">🇮🇹</span>
<span title="Français">🇫🇷</span>
<span title="Deutsch">🇩🇪</span>
<span title="Español">🇪🇸</span>
</div>
<select id="language-select" aria-label="Language selector">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
</div>
<div class="badge" data-i18n="badge">community beta</div>
</div>
</header>
<section class="hero">
<div>
<h1 data-i18n="heroTitle">Give your bags a new life.</h1>
<p data-i18n="heroDesc">
Swap bags and handbags in a simple, safe, and sustainable way.
Upload your item, find real matches, and refresh your style without buying new every time.
</p>
<div class="actions">
<button class="btn btn-primary" id="start-swapping-btn" type="button" data-i18n="ctaPrimary">Start swapping</button>
<a class="btn btn-secondary" id="how-it-works-btn" href="/howtowork" data-i18n="ctaSecondary">See how it works</a>
</div>
</div>
<aside class="showcase">
<h2 data-i18n="howTitle">How it works in 3 steps</h2>
<ul>
<li data-i18n="step1">1. List your bag with photos and condition</li>
<li data-i18n="step2">2. Receive offers from people with similar taste</li>
<li data-i18n="step3">3. Confirm the swap through chat and tracked shipping</li>
</ul>
</aside>
</section>
<section class="grid">
<article class="card">
<h3 data-i18n="card1Title">Verified profiles only</h3>
<p data-i18n="card1Desc">We reduce risk with account verification, feedback, and transparent swap history.</p>
</article>
<article class="card">
<h3 data-i18n="card2Title">Save and add value</h3>
<p data-i18n="card2Desc">A smart way to renew your wardrobe while avoiding waste and unnecessary spending.</p>
</article>
<article class="card">
<h3 data-i18n="card3Title">Circular style</h3>
<p data-i18n="card3Desc">From daily bags to elegant clutches: every piece can find a new owner.</p>
</article>
</section>
<section class="community-note">
<h3 data-i18n="communityTitle">Videochat and grow your profile</h3>
<p data-i18n="communityDesc">You can videochat with people interested in your items or simply exchange opinions and advice. You can aspire to become a fashion bag influencer.</p>
</section>
<footer data-i18n="footer">{{.FooterText}}</footer>
</div>
<div class="modal-backdrop" id="subscribe-modal" aria-hidden="true">
<div class="modal-panel" role="dialog" aria-modal="true" aria-labelledby="subscribe-title">
<button class="modal-close" id="subscribe-close-btn" type="button" aria-label="Close">×</button>
<h3 id="subscribe-title" data-i18n="subscribeTitle">We are building Bag Exchange.</h3>
<p class="modal-desc" data-i18n="subscribeDesc">Enter your email to stay updated.</p>
<form class="subscribe-form" id="subscribe-form">
<input class="form-input" id="subscribe-email" type="email" required data-i18n-placeholder="subscribePlaceholder" placeholder="Your email" />
<button class="btn btn-primary" id="subscribe-submit-btn" type="submit" data-i18n="subscribeCta">Keep me updated</button>
</form>
<p class="subscribe-feedback is-hidden" id="subscribe-feedback" data-i18n="subscribeThanks">Thank you. We will keep you updated.</p>
<p class="subscribe-error is-hidden" id="subscribe-error"></p>
</div>
</div>
<script src="/static/js/i18n.js?v={{.AssetVersion}}"></script>
</body>
</html>

104
templates/login.html Normal file
View File

@@ -0,0 +1,104 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bag Exchange | Login</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-stone-200 text-zinc-800">
<main class="mx-auto grid min-h-screen w-full max-w-2xl place-items-center px-4 py-8">
<section class="w-full max-w-xl rounded-2xl border border-zinc-300/70 bg-amber-50/80 p-6 shadow-2xl shadow-stone-700/10 backdrop-blur-sm">
<div class="mb-4 flex justify-end">
<select id="language-select" class="rounded-lg border border-zinc-300 bg-white px-2 py-1 text-sm">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
</div>
<h1 class="mb-2 text-3xl font-bold tracking-tight" data-i18n="title">Login</h1>
<p class="mb-4 text-sm text-zinc-700" data-i18n="subtitle">Access your Bag Exchange account.</p>
<form class="grid gap-3" id="login-form">
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="email" type="email" required data-i18n-placeholder="emailPlaceholder" placeholder="Your email" />
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="password" type="password" required data-i18n-placeholder="passwordPlaceholder" placeholder="Password" />
<button class="rounded-xl bg-orange-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-orange-600 disabled:cursor-not-allowed disabled:opacity-60" id="submit-btn" type="submit" data-i18n="submit">Log in</button>
</form>
<p class="mt-3 text-sm"><a class="text-emerald-900 underline decoration-1 underline-offset-2" href="/forgot-password" data-lang-link data-i18n="forgotLink">Forgot your password?</a></p>
<p class="mt-1 text-sm"><a class="text-emerald-900 underline decoration-1 underline-offset-2" href="/signup" data-lang-link data-i18n="signupLink">Create an account</a></p>
<p class="mt-1 text-sm"><a class="text-emerald-900 underline decoration-1 underline-offset-2" href="/" data-lang-link data-i18n="homeLink">Back to homepage</a></p>
<div class="mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed" id="feedback"></div>
</section>
</main>
<script src="/static/js/simple_i18n.js"></script>
<script>
var i18n = window.initPageI18n({
en: { pageTitle: "Bag Exchange | Login", title: "Login", subtitle: "Access your Bag Exchange account.", emailPlaceholder: "Your email", passwordPlaceholder: "Password", submit: "Log in", forgotLink: "Forgot your password?", signupLink: "Create an account", homeLink: "Back to homepage", msgInvalid: "Invalid email or password.", msgNotVerified: "Email not verified. Check your inbox or request a new verification email.", msgError: "Unable to log in. Try again." },
it: { pageTitle: "Bag Exchange | Accesso", title: "Accedi", subtitle: "Accedi al tuo account Bag Exchange.", emailPlaceholder: "La tua email", passwordPlaceholder: "Password", submit: "Accedi", forgotLink: "Hai dimenticato la password?", signupLink: "Crea un account", homeLink: "Torna alla home", msgInvalid: "Email o password non validi.", msgNotVerified: "Email non verificata. Controlla la tua casella o richiedi un nuovo link di verifica.", msgError: "Impossibile effettuare il login. Riprova." },
fr: { pageTitle: "Bag Exchange | Connexion", title: "Connexion", subtitle: "Accedez a votre compte Bag Exchange.", emailPlaceholder: "Votre email", passwordPlaceholder: "Mot de passe", submit: "Connexion", forgotLink: "Mot de passe oublie ?", signupLink: "Creer un compte", homeLink: "Retour a l'accueil", msgInvalid: "Email ou mot de passe invalide.", msgNotVerified: "Email non verifie. Verifiez votre boite de reception ou demandez un nouveau lien.", msgError: "Connexion impossible. Veuillez reessayer." },
de: { pageTitle: "Bag Exchange | Login", title: "Anmelden", subtitle: "Melde dich bei deinem Bag Exchange Konto an.", emailPlaceholder: "Deine E-Mail", passwordPlaceholder: "Passwort", submit: "Anmelden", forgotLink: "Passwort vergessen?", signupLink: "Konto erstellen", homeLink: "Zur Startseite", msgInvalid: "Ungueltige E-Mail oder Passwort.", msgNotVerified: "E-Mail nicht verifiziert. Bitte pruefe dein Postfach oder fordere einen neuen Link an.", msgError: "Anmeldung nicht moeglich. Bitte erneut versuchen." },
es: { pageTitle: "Bag Exchange | Inicio de sesion", title: "Iniciar sesion", subtitle: "Accede a tu cuenta de Bag Exchange.", emailPlaceholder: "Tu email", passwordPlaceholder: "Contrasena", submit: "Iniciar sesion", forgotLink: "Has olvidado tu contrasena?", signupLink: "Crear una cuenta", homeLink: "Volver al inicio", msgInvalid: "Email o contrasena no validos.", msgNotVerified: "Email no verificado. Revisa tu bandeja o solicita un nuevo enlace.", msgError: "No se pudo iniciar sesion. Intentalo de nuevo." }
});
(function () {
var form = document.getElementById("login-form");
var emailInput = document.getElementById("email");
var passwordInput = document.getElementById("password");
var submitButton = document.getElementById("submit-btn");
var feedback = document.getElementById("feedback");
function showMessage(kind, text) {
feedback.className = "mt-4 rounded-xl px-3 py-2 text-sm leading-relaxed";
if (kind === "error") {
feedback.classList.add("bg-red-100", "text-red-800");
} else {
feedback.classList.add("bg-emerald-100", "text-emerald-900");
}
feedback.textContent = text;
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
var email = emailInput.value.trim();
var password = passwordInput.value;
if (!email || !emailInput.checkValidity() || !password) {
showMessage("error", i18n.t("msgInvalid"));
return;
}
submitButton.disabled = true;
feedback.className = "mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed";
feedback.textContent = "";
try {
var response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email, password: password })
});
var payload = {};
try { payload = await response.json(); } catch (e) {}
if (response.ok) {
window.location.href = "/?lang=" + encodeURIComponent(i18n.getLanguage());
} else if (payload.error === "email_not_verified") {
showMessage("error", i18n.t("msgNotVerified"));
} else {
showMessage("error", i18n.t("msgInvalid"));
}
} catch (err) {
showMessage("error", i18n.t("msgError"));
} finally {
submitButton.disabled = false;
}
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,118 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bag Exchange | Reset Password</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-stone-200 text-zinc-800">
<main class="mx-auto grid min-h-screen w-full max-w-2xl place-items-center px-4 py-8">
<section class="w-full max-w-xl rounded-2xl border border-zinc-300/70 bg-amber-50/80 p-6 shadow-2xl shadow-stone-700/10 backdrop-blur-sm">
<div class="mb-4 flex justify-end">
<select id="language-select" class="rounded-lg border border-zinc-300 bg-white px-2 py-1 text-sm">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
</div>
<h1 class="mb-2 text-3xl font-bold tracking-tight" data-i18n="title">Set a new password</h1>
<p class="mb-4 text-sm text-zinc-700" data-i18n="subtitle">Create your new password to recover access to your Bag Exchange account.</p>
<form class="grid gap-3" id="reset-form">
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="password" type="password" minlength="8" required data-i18n-placeholder="passwordPlaceholder" placeholder="New password" />
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="confirm-password" type="password" minlength="8" required data-i18n-placeholder="confirmPlaceholder" placeholder="Confirm new password" />
<button class="rounded-xl bg-orange-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-orange-600 disabled:cursor-not-allowed disabled:opacity-60" id="submit-btn" type="submit" data-i18n="submit">Reset password</button>
</form>
<div class="mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed" id="feedback"></div>
<a class="mt-4 inline-block text-sm text-emerald-900 underline decoration-1 underline-offset-2" href="/login" data-lang-link data-i18n="loginLink">Back to login</a>
</section>
</main>
<script src="/static/js/simple_i18n.js"></script>
<script>
var i18n = window.initPageI18n({
en: { pageTitle: "Bag Exchange | Reset Password", title: "Set a new password", subtitle: "Create your new password to recover access to your Bag Exchange account.", passwordPlaceholder: "New password", confirmPlaceholder: "Confirm new password", submit: "Reset password", loginLink: "Back to login", msgInvalidToken: "Invalid reset token. Open the link from your email again.", msgShort: "Use at least 8 characters.", msgMismatch: "Passwords do not match.", msgOk: "Password updated successfully. You can now log in.", msgExpired: "Reset link is invalid or expired.", msgWeak: "Use at least 8 characters with letters and numbers.", msgError: "Unable to reset password. Try again." },
it: { pageTitle: "Bag Exchange | Reimposta Password", title: "Imposta una nuova password", subtitle: "Crea una nuova password per recuperare l'accesso al tuo account Bag Exchange.", passwordPlaceholder: "Nuova password", confirmPlaceholder: "Conferma nuova password", submit: "Reimposta password", loginLink: "Torna al login", msgInvalidToken: "Token di reset non valido. Apri di nuovo il link ricevuto via email.", msgShort: "Usa almeno 8 caratteri.", msgMismatch: "Le password non coincidono.", msgOk: "Password aggiornata con successo. Ora puoi accedere.", msgExpired: "Il link di reset non e valido o e scaduto.", msgWeak: "Usa almeno 8 caratteri con lettere e numeri.", msgError: "Impossibile reimpostare la password. Riprova." },
fr: { pageTitle: "Bag Exchange | Reinitialiser le mot de passe", title: "Definir un nouveau mot de passe", subtitle: "Creez un nouveau mot de passe pour recuperer l'acces a votre compte Bag Exchange.", passwordPlaceholder: "Nouveau mot de passe", confirmPlaceholder: "Confirmer le nouveau mot de passe", submit: "Reinitialiser", loginLink: "Retour a la connexion", msgInvalidToken: "Jeton de reinitialisation invalide. Ouvrez a nouveau le lien recu par email.", msgShort: "Utilisez au moins 8 caracteres.", msgMismatch: "Les mots de passe ne correspondent pas.", msgOk: "Mot de passe mis a jour. Vous pouvez maintenant vous connecter.", msgExpired: "Le lien de reinitialisation est invalide ou expire.", msgWeak: "Utilisez au moins 8 caracteres avec lettres et chiffres.", msgError: "Impossible de reinitialiser le mot de passe. Veuillez reessayer." },
de: { pageTitle: "Bag Exchange | Passwort zuruecksetzen", title: "Neues Passwort festlegen", subtitle: "Lege ein neues Passwort fest, um wieder Zugriff auf dein Bag Exchange Konto zu erhalten.", passwordPlaceholder: "Neues Passwort", confirmPlaceholder: "Neues Passwort bestaetigen", submit: "Passwort zuruecksetzen", loginLink: "Zurueck zum Login", msgInvalidToken: "Ungueltiger Reset-Token. Oeffne den Link aus der E-Mail erneut.", msgShort: "Mindestens 8 Zeichen verwenden.", msgMismatch: "Die Passwoerter stimmen nicht ueberein.", msgOk: "Passwort erfolgreich aktualisiert. Du kannst dich jetzt anmelden.", msgExpired: "Reset-Link ist ungueltig oder abgelaufen.", msgWeak: "Mindestens 8 Zeichen mit Buchstaben und Zahlen verwenden.", msgError: "Passwort konnte nicht zurueckgesetzt werden. Bitte erneut versuchen." },
es: { pageTitle: "Bag Exchange | Restablecer contrasena", title: "Define una nueva contrasena", subtitle: "Crea una nueva contrasena para recuperar el acceso a tu cuenta Bag Exchange.", passwordPlaceholder: "Nueva contrasena", confirmPlaceholder: "Confirmar nueva contrasena", submit: "Restablecer contrasena", loginLink: "Volver al login", msgInvalidToken: "Token de restablecimiento no valido. Abre de nuevo el enlace del correo.", msgShort: "Usa al menos 8 caracteres.", msgMismatch: "Las contrasenas no coinciden.", msgOk: "Contrasena actualizada correctamente. Ya puedes iniciar sesion.", msgExpired: "El enlace de restablecimiento no es valido o ha caducado.", msgWeak: "Usa al menos 8 caracteres con letras y numeros.", msgError: "No se pudo restablecer la contrasena. Intentalo de nuevo." }
});
(function () {
var token = "{{.Token}}";
var form = document.getElementById("reset-form");
var passwordInput = document.getElementById("password");
var confirmInput = document.getElementById("confirm-password");
var submitButton = document.getElementById("submit-btn");
var feedback = document.getElementById("feedback");
function showMessage(kind, text) {
feedback.className = "mt-4 rounded-xl px-3 py-2 text-sm leading-relaxed";
if (kind === "error") {
feedback.classList.add("bg-red-100", "text-red-800");
} else {
feedback.classList.add("bg-emerald-100", "text-emerald-900");
}
feedback.textContent = text;
}
if (!token) {
showMessage("error", i18n.t("msgInvalidToken"));
submitButton.disabled = true;
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
var password = passwordInput.value;
var confirmPassword = confirmInput.value;
if (!password || password.length < 8) {
showMessage("error", i18n.t("msgShort"));
return;
}
if (password !== confirmPassword) {
showMessage("error", i18n.t("msgMismatch"));
return;
}
submitButton.disabled = true;
feedback.className = "mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed";
feedback.textContent = "";
try {
var response = await fetch("/api/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: token, password: password, confirmPassword: confirmPassword })
});
var payload = {};
try { payload = await response.json(); } catch (e) { payload = {}; }
if (response.ok) {
form.classList.add("hidden");
showMessage("ok", i18n.t("msgOk"));
} else if (payload.error === "invalid_or_expired_token" || payload.error === "invalid_token") {
showMessage("error", i18n.t("msgExpired"));
} else if (payload.error === "password_too_weak") {
showMessage("error", i18n.t("msgWeak"));
} else if (payload.error === "password_mismatch") {
showMessage("error", i18n.t("msgMismatch"));
} else {
showMessage("error", i18n.t("msgError"));
}
} catch (err) {
showMessage("error", i18n.t("msgError"));
} finally {
submitButton.disabled = false;
}
});
})();
</script>
</body>
</html>

120
templates/signup.html Normal file
View File

@@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bag Exchange | Sign Up</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-stone-200 text-zinc-800">
<main class="mx-auto grid min-h-screen w-full max-w-2xl place-items-center px-4 py-8">
<section class="w-full max-w-xl rounded-2xl border border-zinc-300/70 bg-amber-50/80 p-6 shadow-2xl shadow-stone-700/10 backdrop-blur-sm">
<div class="mb-4 flex justify-end">
<select id="language-select" class="rounded-lg border border-zinc-300 bg-white px-2 py-1 text-sm">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
</div>
<h1 class="mb-2 text-3xl font-bold tracking-tight" data-i18n="title">Create account</h1>
<p class="mb-4 text-sm text-zinc-700" data-i18n="subtitle">Join Bag Exchange and verify your email to activate access.</p>
<form class="grid gap-3" id="signup-form">
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="email" type="email" required data-i18n-placeholder="emailPlaceholder" placeholder="Your email" />
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="password" type="password" minlength="8" required data-i18n-placeholder="passwordPlaceholder" placeholder="Password" />
<input class="w-full rounded-xl border border-zinc-400/40 bg-white px-3 py-3 text-sm outline-none ring-orange-300 transition focus:ring-2" id="confirm-password" type="password" minlength="8" required data-i18n-placeholder="confirmPlaceholder" placeholder="Confirm password" />
<button class="rounded-xl bg-orange-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-orange-600 disabled:cursor-not-allowed disabled:opacity-60" id="submit-btn" type="submit" data-i18n="submit">Create account</button>
</form>
<p class="mt-3 text-sm"><a class="text-emerald-900 underline decoration-1 underline-offset-2" href="/login" data-lang-link data-i18n="loginLink">Already have an account? Log in</a></p>
<p class="mt-1 text-sm"><a class="text-emerald-900 underline decoration-1 underline-offset-2" href="/" data-lang-link data-i18n="homeLink">Back to homepage</a></p>
<div class="mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed" id="feedback"></div>
</section>
</main>
<script src="/static/js/simple_i18n.js"></script>
<script>
var i18n = window.initPageI18n({
en: { pageTitle: "Bag Exchange | Sign Up", title: "Create account", subtitle: "Join Bag Exchange and verify your email to activate access.", emailPlaceholder: "Your email", passwordPlaceholder: "Password", confirmPlaceholder: "Confirm password", submit: "Create account", loginLink: "Already have an account? Log in", homeLink: "Back to homepage", msgInvalidEmail: "Please enter a valid email address.", msgMismatch: "Passwords do not match.", msgWeak: "Use at least 8 characters with letters and numbers.", msgExists: "This email is already registered.", msgGeneric: "Unable to create account. Try again.", msgSuccess: "Account created. Check your email to verify your account before login." },
it: { pageTitle: "Bag Exchange | Registrazione", title: "Crea account", subtitle: "Unisciti a Bag Exchange e verifica la tua email per attivare l'accesso.", emailPlaceholder: "La tua email", passwordPlaceholder: "Password", confirmPlaceholder: "Conferma password", submit: "Crea account", loginLink: "Hai gia un account? Accedi", homeLink: "Torna alla home", msgInvalidEmail: "Inserisci un indirizzo email valido.", msgMismatch: "Le password non coincidono.", msgWeak: "Usa almeno 8 caratteri con lettere e numeri.", msgExists: "Questa email e gia registrata.", msgGeneric: "Impossibile creare l'account. Riprova.", msgSuccess: "Account creato. Controlla la tua email per verificare l'account prima del login." },
fr: { pageTitle: "Bag Exchange | Inscription", title: "Creer un compte", subtitle: "Rejoignez Bag Exchange et verifiez votre email pour activer l'acces.", emailPlaceholder: "Votre email", passwordPlaceholder: "Mot de passe", confirmPlaceholder: "Confirmer le mot de passe", submit: "Creer un compte", loginLink: "Vous avez deja un compte ? Connectez-vous", homeLink: "Retour a l'accueil", msgInvalidEmail: "Veuillez entrer une adresse email valide.", msgMismatch: "Les mots de passe ne correspondent pas.", msgWeak: "Utilisez au moins 8 caracteres avec lettres et chiffres.", msgExists: "Cet email est deja enregistre.", msgGeneric: "Impossible de creer le compte. Veuillez reessayer.", msgSuccess: "Compte cree. Verifiez votre email avant de vous connecter." },
de: { pageTitle: "Bag Exchange | Registrierung", title: "Konto erstellen", subtitle: "Tritt Bag Exchange bei und bestaetige deine E-Mail, um den Zugang zu aktivieren.", emailPlaceholder: "Deine E-Mail", passwordPlaceholder: "Passwort", confirmPlaceholder: "Passwort bestaetigen", submit: "Konto erstellen", loginLink: "Schon ein Konto? Anmelden", homeLink: "Zur Startseite", msgInvalidEmail: "Bitte gib eine gueltige E-Mail-Adresse ein.", msgMismatch: "Die Passwoerter stimmen nicht ueberein.", msgWeak: "Mindestens 8 Zeichen mit Buchstaben und Zahlen verwenden.", msgExists: "Diese E-Mail ist bereits registriert.", msgGeneric: "Konto konnte nicht erstellt werden. Bitte erneut versuchen.", msgSuccess: "Konto erstellt. Bitte bestaetige deine E-Mail vor dem Login." },
es: { pageTitle: "Bag Exchange | Registro", title: "Crear cuenta", subtitle: "Unete a Bag Exchange y verifica tu email para activar el acceso.", emailPlaceholder: "Tu email", passwordPlaceholder: "Contrasena", confirmPlaceholder: "Confirmar contrasena", submit: "Crear cuenta", loginLink: "Ya tienes cuenta? Inicia sesion", homeLink: "Volver al inicio", msgInvalidEmail: "Introduce un correo electronico valido.", msgMismatch: "Las contrasenas no coinciden.", msgWeak: "Usa al menos 8 caracteres con letras y numeros.", msgExists: "Este email ya esta registrado.", msgGeneric: "No se pudo crear la cuenta. Intentalo de nuevo.", msgSuccess: "Cuenta creada. Revisa tu email para verificar la cuenta antes de iniciar sesion." }
});
(function () {
var form = document.getElementById("signup-form");
var emailInput = document.getElementById("email");
var passwordInput = document.getElementById("password");
var confirmInput = document.getElementById("confirm-password");
var submitButton = document.getElementById("submit-btn");
var feedback = document.getElementById("feedback");
function showMessage(kind, text) {
feedback.className = "mt-4 rounded-xl px-3 py-2 text-sm leading-relaxed";
if (kind === "error") {
feedback.classList.add("bg-red-100", "text-red-800");
} else {
feedback.classList.add("bg-emerald-100", "text-emerald-900");
}
feedback.textContent = text;
}
form.addEventListener("submit", async function (event) {
event.preventDefault();
var email = emailInput.value.trim();
var password = passwordInput.value;
var confirmPassword = confirmInput.value;
if (!email || !emailInput.checkValidity()) {
showMessage("error", i18n.t("msgInvalidEmail"));
return;
}
if (password !== confirmPassword) {
showMessage("error", i18n.t("msgMismatch"));
return;
}
if (password.length < 8) {
showMessage("error", i18n.t("msgWeak"));
return;
}
submitButton.disabled = true;
feedback.className = "mt-4 hidden rounded-xl px-3 py-2 text-sm leading-relaxed";
feedback.textContent = "";
try {
var response = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email, password: password, confirmPassword: confirmPassword })
});
var payload = {};
try { payload = await response.json(); } catch (e) {}
if (response.ok) {
form.classList.add("hidden");
showMessage("ok", i18n.t("msgSuccess"));
} else if (payload.error === "email_exists") {
showMessage("error", i18n.t("msgExists"));
} else if (payload.error === "password_too_weak") {
showMessage("error", i18n.t("msgWeak"));
} else if (payload.error === "password_mismatch") {
showMessage("error", i18n.t("msgMismatch"));
} else {
showMessage("error", i18n.t("msgGeneric"));
}
} catch (err) {
showMessage("error", i18n.t("msgGeneric"));
} finally {
submitButton.disabled = false;
}
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html>
<body style="margin:0;padding:24px;background:#f5ede1;font-family:Arial,sans-serif;color:#202126;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:620px;margin:0 auto;background:#fff9f0;border:1px solid #dfcfb7;border-radius:12px;">
<tr>
<td style="padding:24px;">
<h1 style="margin:0 0 12px;font-size:22px;">Reset your Bag Exchange password</h1>
<p style="margin:0 0 14px;line-height:1.5;">We received a request to reset your password. Use the button below to set a new one.</p>
<p style="margin:20px 0;">
<a href="{{.ActionURL}}" style="display:inline-block;background:#4f5e3d;color:#ffffff;text-decoration:none;padding:12px 16px;border-radius:10px;font-weight:700;">Reset password</a>
</p>
<p style="margin:0 0 8px;line-height:1.5;">If the button does not work, copy and paste this link into your browser:</p>
<p style="margin:0;word-break:break-all;"><a href="{{.ActionURL}}">{{.ActionURL}}</a></p>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html>
<body style="margin:0;padding:24px;background:#f5ede1;font-family:Arial,sans-serif;color:#202126;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:620px;margin:0 auto;background:#fff9f0;border:1px solid #dfcfb7;border-radius:12px;">
<tr>
<td style="padding:24px;">
<h1 style="margin:0 0 12px;font-size:22px;">Verify your Bag Exchange email</h1>
<p style="margin:0 0 14px;line-height:1.5;">Thanks for joining Bag Exchange. Please verify your email to activate your account and start exchanging.</p>
<p style="margin:20px 0;">
<a href="{{.ActionURL}}" style="display:inline-block;background:#e0795a;color:#ffffff;text-decoration:none;padding:12px 16px;border-radius:10px;font-weight:700;">Verify email</a>
</p>
<p style="margin:0 0 8px;line-height:1.5;">If the button does not work, copy and paste this link into your browser:</p>
<p style="margin:0;word-break:break-all;"><a href="{{.ActionURL}}">{{.ActionURL}}</a></p>
</td>
</tr>
</table>
</body>
</html>