botui/ui/suite/auth/register.html

1322 lines
50 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create Account - General Bots</title>
<script src="/js/vendor/htmx.min.js"></script>
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: rgba(59, 130, 246, 0.1);
--bg: #0f172a;
--surface: #1e293b;
--surface-hover: #334155;
--border: #334155;
--text: #f8fafc;
--text-secondary: #94a3b8;
--error: #ef4444;
--success: #22c55e;
--warning: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-image:
radial-gradient(
ellipse at top,
rgba(59, 130, 246, 0.1) 0%,
transparent 50%
),
radial-gradient(
ellipse at bottom,
rgba(139, 92, 246, 0.1) 0%,
transparent 50%
);
}
.register-container {
width: 100%;
max-width: 440px;
padding: 2rem;
}
.register-header {
text-align: center;
margin-bottom: 2rem;
}
.register-logo {
width: 72px;
height: 72px;
margin: 0 auto 1.25rem;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.3);
}
.register-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(
135deg,
var(--text),
var(--text-secondary)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.register-subtitle {
color: var(--text-secondary);
font-size: 0.9375rem;
}
.register-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.75rem;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
}
/* Message Box */
.message-box {
padding: 0.875rem 1rem;
border-radius: 10px;
margin-bottom: 1.25rem;
font-size: 0.875rem;
display: none;
align-items: flex-start;
gap: 0.625rem;
}
.message-box.visible {
display: flex;
}
.message-box.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--error);
}
.message-box.success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: var(--success);
}
.message-box svg {
flex-shrink: 0;
margin-top: 0.125rem;
}
/* Form Styles */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text);
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
pointer-events: none;
}
.form-input {
width: 100%;
padding: 0.875rem 1rem 0.875rem 2.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-size: 1rem;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.form-input::placeholder {
color: var(--text-secondary);
}
.form-input.error {
border-color: var(--error);
}
.form-input.valid {
border-color: var(--success);
}
.password-toggle {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.password-toggle:hover {
color: var(--text);
}
/* Password Strength */
.password-strength {
margin-top: 0.5rem;
}
.strength-bars {
display: flex;
gap: 0.25rem;
margin-bottom: 0.375rem;
}
.strength-bar {
flex: 1;
height: 4px;
background: var(--border);
border-radius: 2px;
transition: background 0.3s ease;
}
.strength-bar.active.weak {
background: var(--error);
}
.strength-bar.active.fair {
background: var(--warning);
}
.strength-bar.active.good {
background: #22c55e;
}
.strength-bar.active.strong {
background: #10b981;
}
.strength-text {
font-size: 0.75rem;
color: var(--text-secondary);
}
.strength-text.weak {
color: var(--error);
}
.strength-text.fair {
color: var(--warning);
}
.strength-text.good {
color: #22c55e;
}
.strength-text.strong {
color: #10b981;
}
/* Password Requirements */
.password-requirements {
margin-top: 0.75rem;
padding: 0.75rem;
background: var(--bg);
border-radius: 8px;
}
.requirement {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 0.375rem;
}
.requirement:last-child {
margin-bottom: 0;
}
.requirement svg {
flex-shrink: 0;
}
.requirement.met {
color: var(--success);
}
.requirement.met svg {
color: var(--success);
}
/* Terms Checkbox */
.terms-group {
margin-bottom: 1.5rem;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 0.75rem;
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
line-height: 1.4;
}
.checkbox-input {
display: none;
}
.checkbox-custom {
width: 20px;
height: 20px;
min-width: 20px;
border: 2px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
margin-top: 0.125rem;
}
.checkbox-input:checked + .checkbox-custom {
background: var(--primary);
border-color: var(--primary);
}
.checkbox-input:checked + .checkbox-custom svg {
display: block;
}
.checkbox-custom svg {
display: none;
color: white;
}
.terms-text a {
color: var(--primary);
text-decoration: none;
}
.terms-text a:hover {
text-decoration: underline;
}
/* Buttons */
.btn {
width: 100%;
padding: 0.875rem 1.25rem;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
position: relative;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), #6366f1);
color: white;
border: none;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-text {
transition: opacity 0.2s ease;
}
.btn.loading .btn-text {
opacity: 0;
}
.btn.loading .spinner {
display: block;
}
.spinner {
display: none;
position: absolute;
width: 22px;
height: 22px;
border: 2px solid transparent;
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Divider */
.divider {
display: flex;
align-items: center;
margin: 1.75rem 0;
color: var(--text-secondary);
font-size: 0.8125rem;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
.divider span {
padding: 0 1rem;
}
/* Social Login */
.social-login {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.social-btn {
padding: 0.75rem 1rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.625rem;
transition: all 0.2s ease;
}
.social-btn:hover {
border-color: var(--primary);
background: var(--surface-hover);
}
.social-btn:active {
transform: scale(0.98);
}
/* Footer */
.register-footer {
text-align: center;
margin-top: 1.75rem;
font-size: 0.9375rem;
color: var(--text-secondary);
}
.register-footer a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.register-footer a:hover {
text-decoration: underline;
}
/* Success State */
.success-section {
display: none;
text-align: center;
padding: 2rem 1rem;
}
.success-section.visible {
display: block;
}
.success-icon {
width: 72px;
height: 72px;
margin: 0 auto 1.5rem;
background: rgba(34, 197, 94, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--success);
}
.success-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.success-text {
color: var(--text-secondary);
margin-bottom: 1.5rem;
line-height: 1.5;
}
.success-email {
color: var(--primary);
font-weight: 500;
}
/* Responsive */
@media (max-width: 480px) {
.register-container {
padding: 1rem;
}
.register-card {
padding: 1.25rem;
}
.form-row {
grid-template-columns: 1fr;
}
.social-login {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="register-container">
<div class="register-header">
<div class="register-logo">🤖</div>
<h1 class="register-title">Create Account</h1>
<p class="register-subtitle">
Join General Bots and start building
</p>
</div>
<div class="register-card">
<!-- Registration Form -->
<div id="register-section">
<div class="message-box error" id="error-message">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<span id="error-text">An error occurred</span>
</div>
<form
id="register-form"
hx-post="/api/auth/register"
hx-target="#register-response"
hx-swap="innerHTML"
hx-indicator="#register-btn"
>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="first-name"
>First Name</label
>
<div class="input-wrapper">
<svg
class="input-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<input
type="text"
id="first-name"
name="first_name"
class="form-input"
placeholder="John"
required
autocomplete="given-name"
/>
</div>
</div>
<div class="form-group">
<label class="form-label" for="last-name"
>Last Name</label
>
<div class="input-wrapper">
<svg
class="input-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<input
type="text"
id="last-name"
name="last_name"
class="form-input"
placeholder="Doe"
required
autocomplete="family-name"
/>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label" for="email"
>Email Address</label
>
<div class="input-wrapper">
<svg
class="input-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="you@example.com"
required
autocomplete="email"
/>
</div>
</div>
<div class="form-group">
<label class="form-label" for="password"
>Password</label
>
<div class="input-wrapper">
<svg
class="input-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="11"
width="18"
height="11"
rx="2"
ry="2"
></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="••••••••"
required
autocomplete="new-password"
oninput="checkPasswordStrength(this.value)"
/>
<button
type="button"
class="password-toggle"
onclick="togglePassword('password')"
>
<svg
class="eye-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg
class="eye-off-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
style="display: none"
>
<path
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
></path>
<line
x1="1"
y1="1"
x2="23"
y2="23"
></line>
</svg>
</button>
</div>
<div class="password-strength" id="password-strength">
<div class="strength-bars">
<div class="strength-bar" data-index="0"></div>
<div class="strength-bar" data-index="1"></div>
<div class="strength-bar" data-index="2"></div>
<div class="strength-bar" data-index="3"></div>
</div>
<span class="strength-text" id="strength-text"
>Enter a password</span
>
</div>
<div class="password-requirements">
<div class="requirement" id="req-length">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
</svg>
At least 8 characters
</div>
<div class="requirement" id="req-uppercase">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
</svg>
One uppercase letter
</div>
<div class="requirement" id="req-lowercase">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
</svg>
One lowercase letter
</div>
<div class="requirement" id="req-number">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
</svg>
One number
</div>
</div>
</div>
<div class="form-group">
<label class="form-label" for="confirm-password"
>Confirm Password</label
>
<div class="input-wrapper">
<svg
class="input-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="11"
width="18"
height="11"
rx="2"
ry="2"
></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<input
type="password"
id="confirm-password"
name="confirm_password"
class="form-input"
placeholder="••••••••"
required
autocomplete="new-password"
oninput="checkPasswordMatch()"
/>
<button
type="button"
class="password-toggle"
onclick="togglePassword('confirm-password')"
>
<svg
class="eye-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg
class="eye-off-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
style="display: none"
>
<path
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
></path>
<line
x1="1"
y1="1"
x2="23"
y2="23"
></line>
</svg>
</button>
</div>
<span
class="password-match"
id="password-match"
style="
font-size: 0.75rem;
margin-top: 0.375rem;
display: none;
"
></span>
</div>
<div class="terms-group">
<label class="checkbox-label">
<input
type="checkbox"
name="terms"
id="terms"
required
class="checkbox-input"
/>
<span class="checkbox-custom">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
>
<polyline
points="20 6 9 17 4 12"
></polyline>
</svg>
</span>
<span class="terms-text">
I agree to the
<a href="/terms" target="_blank"
>Terms of Service</a
>
and
<a href="/privacy" target="_blank"
>Privacy Policy</a
>
</span>
</label>
</div>
<button
type="submit"
class="btn btn-primary"
id="register-btn"
>
<span class="btn-text">Create Account</span>
<div class="spinner"></div>
</button>
</form>
<div id="register-response"></div>
<div class="divider">
<span>or sign up with</span>
</div>
<div class="social-login">
<button
type="button"
class="social-btn"
onclick="oauthSignup('google')"
>
<svg width="18" height="18" viewBox="0 0 24 24">
<path
fill="#EA4335"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#4285F4"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Google
</button>
<button
type="button"
class="social-btn"
onclick="oauthSignup('microsoft')"
>
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="#F25022" d="M1 1h10v10H1z" />
<path fill="#00A4EF" d="M13 1h10v10H13z" />
<path fill="#7FBA00" d="M1 13h10v10H1z" />
<path fill="#FFB900" d="M13 13h10v10H13z" />
</svg>
Microsoft
</button>
</div>
</div>
<!-- Success Section -->
<div id="success-section" class="success-section">
<div class="success-icon">
<svg
width="36"
height="36"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<h2 class="success-title">Check Your Email</h2>
<p class="success-text">
We've sent a verification link to<br />
<span class="success-email" id="success-email"
>your@email.com</span
>
</p>
<p class="success-text">
Click the link in the email to verify your account and
get started.
</p>
<button
type="button"
class="btn btn-primary"
onclick="window.location.href='/auth/login'"
>
<span class="btn-text">Go to Login</span>
</button>
</div>
</div>
<div class="register-footer">
<p>
Already have an account?
<a href="/auth/login">Sign in</a>
</p>
</div>
</div>
<script>
// Password visibility toggle
function togglePassword(inputId) {
const input = document.getElementById(inputId);
const wrapper = input.closest(".input-wrapper");
const eyeIcon = wrapper.querySelector(".eye-icon");
const eyeOffIcon = wrapper.querySelector(".eye-off-icon");
if (input.type === "password") {
input.type = "text";
eyeIcon.style.display = "none";
eyeOffIcon.style.display = "block";
} else {
input.type = "password";
eyeIcon.style.display = "block";
eyeOffIcon.style.display = "none";
}
}
// Password strength checker
function checkPasswordStrength(password) {
const bars = document.querySelectorAll(".strength-bar");
const strengthText = document.getElementById("strength-text");
// Check requirements
const hasLength = password.length >= 8;
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[^A-Za-z0-9]/.test(password);
// Update requirement indicators
updateRequirement("req-length", hasLength);
updateRequirement("req-uppercase", hasUppercase);
updateRequirement("req-lowercase", hasLowercase);
updateRequirement("req-number", hasNumber);
// Calculate strength
let strength = 0;
if (hasLength) strength++;
if (hasUppercase) strength++;
if (hasLowercase) strength++;
if (hasNumber) strength++;
if (hasSpecial) strength++;
if (password.length >= 12) strength++;
// Normalize to 4 levels
let level = 0;
let levelClass = "";
let levelText = "Enter a password";
if (password.length === 0) {
levelText = "Enter a password";
} else if (strength <= 2) {
level = 1;
levelClass = "weak";
levelText = "Weak password";
} else if (strength === 3) {
level = 2;
levelClass = "fair";
levelText = "Fair password";
} else if (strength === 4) {
level = 3;
levelClass = "good";
levelText = "Good password";
} else {
level = 4;
levelClass = "strong";
levelText = "Strong password";
}
// Update bars
bars.forEach((bar, index) => {
bar.classList.remove(
"active",
"weak",
"fair",
"good",
"strong",
);
if (index < level) {
bar.classList.add("active", levelClass);
}
});
// Update text
strengthText.textContent = levelText;
strengthText.className =
"strength-text " + (password.length > 0 ? levelClass : "");
// Check password match if confirm field has value
checkPasswordMatch();
}
function updateRequirement(id, met) {
const req = document.getElementById(id);
if (met) {
req.classList.add("met");
req.querySelector("svg").innerHTML =
'<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>';
} else {
req.classList.remove("met");
req.querySelector("svg").innerHTML =
'<circle cx="12" cy="12" r="10"></circle>';
}
}
function checkPasswordMatch() {
const password = document.getElementById("password").value;
const confirmPassword =
document.getElementById("confirm-password").value;
const matchIndicator = document.getElementById("password-match");
const confirmInput = document.getElementById("confirm-password");
if (confirmPassword.length === 0) {
matchIndicator.style.display = "none";
confirmInput.classList.remove("error", "valid");
return;
}
matchIndicator.style.display = "block";
if (password === confirmPassword) {
matchIndicator.textContent = "Passwords match";
matchIndicator.style.color = "var(--success)";
confirmInput.classList.remove("error");
confirmInput.classList.add("valid");
} else {
matchIndicator.textContent = "Passwords do not match";
matchIndicator.style.color = "var(--error)";
confirmInput.classList.remove("valid");
confirmInput.classList.add("error");
}
}
// OAuth signup
function oauthSignup(provider) {
window.location.href = `/api/auth/oauth/${provider}`;
}
// Show error message
function showError(message) {
const errorBox = document.getElementById("error-message");
const errorText = document.getElementById("error-text");
errorText.textContent = message;
errorBox.classList.add("visible");
}
// Hide error message
function hideError() {
document
.getElementById("error-message")
.classList.remove("visible");
}
// Show success section
function showSuccess(email) {
document.getElementById("register-section").style.display =
"none";
document
.getElementById("success-section")
.classList.add("visible");
document.getElementById("success-email").textContent = email;
}
// Loading state
function setLoading(loading) {
const btn = document.getElementById("register-btn");
if (loading) {
btn.classList.add("loading");
btn.disabled = true;
} else {
btn.classList.remove("loading");
btn.disabled = false;
}
}
// Form validation
document
.getElementById("register-form")
.addEventListener("submit", function (e) {
const password = document.getElementById("password").value;
const confirmPassword =
document.getElementById("confirm-password").value;
if (password !== confirmPassword) {
e.preventDefault();
showError("Passwords do not match");
return false;
}
if (password.length < 8) {
e.preventDefault();
showError("Password must be at least 8 characters");
return false;
}
if (!document.getElementById("terms").checked) {
e.preventDefault();
showError(
"You must agree to the Terms of Service and Privacy Policy",
);
return false;
}
hideError();
});
// Handle HTMX events
document.body.addEventListener(
"htmx:beforeRequest",
function (event) {
if (event.target.id === "register-form") {
hideError();
setLoading(true);
}
},
);
document.body.addEventListener(
"htmx:afterRequest",
function (event) {
if (event.target.id === "register-form") {
setLoading(false);
if (event.detail.successful) {
try {
const response = JSON.parse(
event.detail.xhr.responseText,
);
if (response.success) {
const email =
document.getElementById("email").value;
showSuccess(email);
} else if (response.redirect) {
window.location.href = response.redirect;
}
} catch (e) {
if (event.detail.xhr.status === 200) {
const email =
document.getElementById("email").value;
showSuccess(email);
}
}
} else {
try {
const response = JSON.parse(
event.detail.xhr.responseText,
);
showError(
response.error ||
"Registration failed. Please try again.",
);
} catch (e) {
showError(
"Registration failed. Please try again.",
);
}
}
}
},
);
// Clear error when user starts typing
document.querySelectorAll(".form-input").forEach((input) => {
input.addEventListener("input", hideError);
});
</script>
</body>
</html>