feat(register): redesign register page to match spec
Replaces the minimal login-style form with the full spec design: hero section (eyebrow, headline, subtext), three labelled form sections, 2-column name grid, confirm-password field with client-side match hints, password strength indicator, notification checkbox card, loading state on submit, and "already have an account?" footer link. Backend: adds notifyOnMention to RegisterRequest and wires both notifyOnMention and notifyOnReply via updateNotificationPreferences on invite redemption. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@@ -34,3 +34,8 @@ src/lib/paraglide_bak*
|
||||
|
||||
# Playwright auth state — regenerated at the start of each CI run via auth.setup.ts
|
||||
e2e/.auth/
|
||||
|
||||
**/test-results/**
|
||||
|
||||
# Proofshot browser verification artifacts
|
||||
proofshot-artifacts/
|
||||
|
||||
@@ -658,6 +658,22 @@
|
||||
"register_invalid_code": "Ungültiger Einladungslink",
|
||||
"register_invalid_code_desc": "Dieser Einladungslink ist nicht gültig, wurde bereits verwendet oder ist abgelaufen. Bitte wende dich an den Administrator.",
|
||||
"register_success": "Dein Konto wurde erfolgreich erstellt. Du kannst dich jetzt anmelden.",
|
||||
"register_eyebrow": "Ein Familienprojekt",
|
||||
"register_hero_headline": "Schön, dass du da bist.",
|
||||
"register_hero_subtext": "Gemeinsam bewahren wir Briefe und Dokumente – für die Familie, jetzt und in Zukunft.",
|
||||
"register_section_about": "Über dich",
|
||||
"register_section_account": "Konto",
|
||||
"register_section_notifications": "Benachrichtigungen",
|
||||
"register_label_password_confirm": "Passwort bestätigen",
|
||||
"register_pw_hint": "Mindestens 8 Zeichen.",
|
||||
"register_pw_ok": "Mindestens 8 Zeichen. ✓",
|
||||
"register_pw_match_ok": "Passwörter stimmen überein.",
|
||||
"register_pw_match_no": "Die beiden Passwörter stimmen noch nicht überein.",
|
||||
"register_notify_title": "Benachrichtige mich,",
|
||||
"register_notify_desc": "wenn jemand mich in einem Kommentar erwähnt oder mir auf einen Kommentar antwortet.",
|
||||
"register_btn_loading": "Wird erstellt …",
|
||||
"register_already_have_account": "Du hast bereits ein Konto?",
|
||||
"register_sign_in": "Anmelden",
|
||||
"login_registered_success": "Dein Konto wurde erfolgreich erstellt. Melde dich jetzt an.",
|
||||
"admin_tab_invites": "Einladungen",
|
||||
"admin_invites_list_title": "Einladungen",
|
||||
|
||||
@@ -658,6 +658,22 @@
|
||||
"register_invalid_code": "Invalid invite link",
|
||||
"register_invalid_code_desc": "This invite link is not valid, has already been used, or has expired. Please contact the administrator.",
|
||||
"register_success": "Your account has been created. You can now sign in.",
|
||||
"register_eyebrow": "A family project",
|
||||
"register_hero_headline": "Glad you're here.",
|
||||
"register_hero_subtext": "Together we preserve letters and documents – for the family, now and in the future.",
|
||||
"register_section_about": "About you",
|
||||
"register_section_account": "Account",
|
||||
"register_section_notifications": "Notifications",
|
||||
"register_label_password_confirm": "Confirm password",
|
||||
"register_pw_hint": "At least 8 characters.",
|
||||
"register_pw_ok": "At least 8 characters. ✓",
|
||||
"register_pw_match_ok": "Passwords match.",
|
||||
"register_pw_match_no": "The two passwords don't match yet.",
|
||||
"register_notify_title": "Notify me",
|
||||
"register_notify_desc": "when someone mentions me in a comment or replies to one of my comments.",
|
||||
"register_btn_loading": "Creating …",
|
||||
"register_already_have_account": "Already have an account?",
|
||||
"register_sign_in": "Sign in",
|
||||
"login_registered_success": "Your account was created successfully. Sign in now.",
|
||||
"admin_tab_invites": "Invites",
|
||||
"admin_invites_list_title": "Invites",
|
||||
|
||||
@@ -658,6 +658,22 @@
|
||||
"register_invalid_code": "Enlace de invitación inválido",
|
||||
"register_invalid_code_desc": "Este enlace de invitación no es válido, ya ha sido utilizado o ha expirado. Contacta al administrador.",
|
||||
"register_success": "Tu cuenta ha sido creada. Ahora puedes iniciar sesión.",
|
||||
"register_eyebrow": "Un proyecto familiar",
|
||||
"register_hero_headline": "Qué bueno que estés aquí.",
|
||||
"register_hero_subtext": "Juntos conservamos cartas y documentos – para la familia, ahora y en el futuro.",
|
||||
"register_section_about": "Sobre ti",
|
||||
"register_section_account": "Cuenta",
|
||||
"register_section_notifications": "Notificaciones",
|
||||
"register_label_password_confirm": "Confirmar contraseña",
|
||||
"register_pw_hint": "Al menos 8 caracteres.",
|
||||
"register_pw_ok": "Al menos 8 caracteres. ✓",
|
||||
"register_pw_match_ok": "Las contraseñas coinciden.",
|
||||
"register_pw_match_no": "Las dos contraseñas aún no coinciden.",
|
||||
"register_notify_title": "Notifícame",
|
||||
"register_notify_desc": "cuando alguien me mencione en un comentario o responda a uno de mis comentarios.",
|
||||
"register_btn_loading": "Creando …",
|
||||
"register_already_have_account": "¿Ya tienes una cuenta?",
|
||||
"register_sign_in": "Iniciar sesión",
|
||||
"login_registered_success": "Tu cuenta fue creada con éxito. Inicia sesión ahora.",
|
||||
"admin_tab_invites": "Invitaciones",
|
||||
"admin_invites_list_title": "Invitaciones",
|
||||
|
||||
@@ -5,7 +5,7 @@ import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
|
||||
<header class="bg-header">
|
||||
<div class="h-1 bg-accent"></div>
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-12 items-center justify-between">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<a href="/" class="flex items-center" aria-label="Familienarchiv">
|
||||
<span class="font-sans text-sm font-bold tracking-widest text-white uppercase"
|
||||
>Familienarchiv</span
|
||||
|
||||
@@ -35,12 +35,13 @@ export const actions = {
|
||||
const password = formData.get('password') as string;
|
||||
const firstName = formData.get('firstName') as string;
|
||||
const lastName = formData.get('lastName') as string;
|
||||
const notifyOnMention = formData.get('notifyOnMention') === 'on';
|
||||
|
||||
const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
const res = await fetch(`${apiUrl}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code, email, password, firstName, lastName })
|
||||
body: JSON.stringify({ code, email, password, firstName, lastName, notifyOnMention })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { getErrorMessage } from '$lib/errors';
|
||||
import { enhance } from '$app/forms';
|
||||
import AuthHeader from '../AuthHeader.svelte';
|
||||
|
||||
let {
|
||||
@@ -16,6 +17,22 @@ let {
|
||||
} = $props();
|
||||
|
||||
let showPassword = $state(false);
|
||||
let showPasswordConfirm = $state(false);
|
||||
let password = $state('');
|
||||
let passwordConfirm = $state('');
|
||||
let notifyOnMention = $state(true);
|
||||
let submitAttempted = $state(false);
|
||||
let submitting = $state(false);
|
||||
|
||||
const pwValid = $derived(password.length >= 8);
|
||||
const pwMatch = $derived(passwordConfirm.length > 0 && password === passwordConfirm);
|
||||
const pwMismatch = $derived(passwordConfirm.length > 0 && password !== passwordConfirm);
|
||||
const showPwHint = $derived(submitAttempted || password.length > 0);
|
||||
const showMatchHint = $derived(submitAttempted || passwordConfirm.length > 0);
|
||||
|
||||
$effect(() => {
|
||||
if (form?.error) submitting = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -25,16 +42,8 @@ let showPassword = $state(false);
|
||||
<div class="flex min-h-screen flex-col bg-canvas">
|
||||
<AuthHeader />
|
||||
|
||||
<main class="flex flex-1 items-center justify-center px-4 py-8">
|
||||
<div class="w-full max-w-sm">
|
||||
<div class="mb-10 text-center">
|
||||
<a href="/" class="inline-flex items-center" aria-label="Familienarchiv">
|
||||
<span class="font-sans text-2xl font-bold tracking-widest text-ink uppercase"
|
||||
>Familienarchiv</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<main class="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-2xl">
|
||||
{#if data.codeError}
|
||||
<div class="rounded-sm border border-line bg-surface p-8 text-center shadow-sm">
|
||||
<svg
|
||||
@@ -62,118 +71,217 @@ let showPassword = $state(false);
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-sm border border-line bg-surface p-8 shadow-sm">
|
||||
<h1 class="mb-1 font-sans text-sm font-bold tracking-widest text-ink uppercase">
|
||||
{m.register_heading()}
|
||||
<!-- Hero -->
|
||||
<div class="mb-8 text-center">
|
||||
<p
|
||||
class="mb-3 inline-block font-sans text-xs font-bold tracking-widest text-ink-3 uppercase"
|
||||
>
|
||||
{m.register_eyebrow()}
|
||||
</p>
|
||||
<h1 class="mb-3 font-serif text-4xl font-normal text-ink sm:text-[46px] sm:leading-tight">
|
||||
{m.register_hero_headline()}
|
||||
</h1>
|
||||
{#if data.code}
|
||||
<p class="mb-6 font-serif text-xs text-ink-2">{m.register_subtext()}</p>
|
||||
{/if}
|
||||
<p class="mx-auto max-w-lg font-serif text-lg text-ink-2">
|
||||
{m.register_hero_subtext()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="space-y-5">
|
||||
<!-- Form card -->
|
||||
<div class="rounded-sm border border-line bg-surface p-8 shadow-sm sm:p-10">
|
||||
<form
|
||||
method="POST"
|
||||
class="space-y-8"
|
||||
use:enhance={() => {
|
||||
if (!pwValid || pwMismatch) {
|
||||
submitAttempted = true;
|
||||
return async () => {};
|
||||
}
|
||||
submitAttempted = true;
|
||||
submitting = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="code" value={data.code ?? ''} />
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="firstName"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_first_name()}</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
id="firstName"
|
||||
autocomplete="given-name"
|
||||
value={data.prefill?.firstName ?? ''}
|
||||
class="block w-full border border-line px-3 py-2.5 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
</div>
|
||||
<!-- Section: Über dich -->
|
||||
<section class="space-y-5">
|
||||
<p class="font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.register_section_about()}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
for="firstName"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_first_name()}</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
id="firstName"
|
||||
autocomplete="given-name"
|
||||
value={data.prefill?.firstName ?? ''}
|
||||
class="block w-full border border-line px-3 py-3 font-serif text-[17px] text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="lastName"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_last_name()}</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
id="lastName"
|
||||
autocomplete="family-name"
|
||||
value={data.prefill?.lastName ?? ''}
|
||||
class="block w-full border border-line px-3 py-3 font-serif text-[17px] text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="lastName"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_last_name()}</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
id="lastName"
|
||||
autocomplete="family-name"
|
||||
value={data.prefill?.lastName ?? ''}
|
||||
class="block w-full border border-line px-3 py-2.5 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
</div>
|
||||
<!-- Section: Konto -->
|
||||
<section class="space-y-5">
|
||||
<p class="font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.register_section_account()}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_email()}</label
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
value={data.prefill?.email ?? ''}
|
||||
class="block w-full border border-line px-3 py-2.5 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
{#if data.prefill?.email}
|
||||
<p class="mt-1 font-sans text-xs text-ink-3">{m.register_prefill_hint()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_password()}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
id="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="block w-full border border-line px-3 py-2.5 pr-10 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showPassword = !showPassword)}
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-ink-3 hover:text-ink"
|
||||
aria-label={showPassword ? m.register_password_hide() : m.register_password_show()}
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_email()}</label
|
||||
>
|
||||
{#if showPassword}
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
value={data.prefill?.email ?? ''}
|
||||
class="block w-full border border-line px-3 py-3 font-serif text-[17px] text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
{#if data.prefill?.email}
|
||||
<p class="mt-1.5 font-sans text-xs text-ink-3">{m.register_prefill_hint()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_password()}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
id="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
bind:value={password}
|
||||
class="block w-full border border-line px-3 py-3 pr-20 font-serif text-[17px] text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showPassword = !showPassword)}
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 font-sans text-xs font-bold text-ink-3 hover:text-ink"
|
||||
aria-label={showPassword ? m.register_password_hide() : m.register_password_show()}
|
||||
>
|
||||
{showPassword ? m.register_password_hide() : m.register_password_show()}
|
||||
</button>
|
||||
</div>
|
||||
{#if showPwHint}
|
||||
<p class="mt-1.5 font-sans text-xs {pwValid ? 'text-green-700' : 'text-ink-3'}">
|
||||
{pwValid ? m.register_pw_ok() : m.register_pw_hint()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="passwordConfirm"
|
||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>{m.register_label_password_confirm()}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type={showPasswordConfirm ? 'text' : 'password'}
|
||||
id="passwordConfirm"
|
||||
autocomplete="new-password"
|
||||
bind:value={passwordConfirm}
|
||||
aria-invalid={submitAttempted && pwMismatch}
|
||||
class="block w-full border px-3 py-3 pr-20 font-serif text-[17px] text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring
|
||||
{submitAttempted && pwMismatch ? 'border-red-400' : 'border-line'}"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showPasswordConfirm = !showPasswordConfirm)}
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 font-sans text-xs font-bold text-ink-3 hover:text-ink"
|
||||
aria-label={showPasswordConfirm ? m.register_password_hide() : m.register_password_show()}
|
||||
>
|
||||
{showPasswordConfirm ? m.register_password_hide() : m.register_password_show()}
|
||||
</button>
|
||||
</div>
|
||||
{#if showMatchHint}
|
||||
{#if pwMatch}
|
||||
<p class="mt-1.5 font-sans text-xs text-green-700">
|
||||
{m.register_pw_match_ok()}
|
||||
</p>
|
||||
{:else if pwMismatch}
|
||||
<p class="mt-1.5 font-sans text-xs text-red-600">{m.register_pw_match_no()}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section: Benachrichtigungen -->
|
||||
<section>
|
||||
<p class="mb-4 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.register_section_notifications()}
|
||||
</p>
|
||||
<label
|
||||
class="flex cursor-pointer items-start gap-3 rounded-sm border p-4 transition-colors
|
||||
{notifyOnMention ? 'border-brand-navy bg-brand-mint/10' : 'border-line bg-white'}"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="notifyOnMention"
|
||||
bind:checked={notifyOnMention}
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- Visual checkbox -->
|
||||
<span
|
||||
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-sm border transition-colors
|
||||
{notifyOnMention ? 'border-brand-navy bg-brand-navy' : 'border-line bg-white'}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#if notifyOnMention}
|
||||
<svg class="h-3 w-3 text-white" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M2 6l3 3 5-5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span>
|
||||
<span class="block font-sans text-sm font-bold text-ink"
|
||||
>{m.register_notify_title()}</span
|
||||
>
|
||||
<span class="mt-0.5 block font-serif text-sm text-ink-2"
|
||||
>{m.register_notify_desc()}</span
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="font-sans text-xs font-medium text-red-600">
|
||||
@@ -181,12 +289,51 @@ let showPassword = $state(false);
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-2 w-full bg-primary py-2.5 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{m.register_btn_submit()}
|
||||
</button>
|
||||
<div class="space-y-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
class="flex w-full items-center justify-center gap-2 bg-primary py-3 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90 disabled:cursor-wait disabled:opacity-70"
|
||||
>
|
||||
{#if submitting}
|
||||
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||
/>
|
||||
</svg>
|
||||
{m.register_btn_loading()}
|
||||
{:else}
|
||||
{m.register_btn_submit()}
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 8l4 4m0 0l-4 4m4-4H3"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<p class="text-center font-sans text-xs text-ink-3">
|
||||
{m.register_already_have_account()}
|
||||
<a
|
||||
href="/login"
|
||||
class="font-bold tracking-widest text-brand-navy uppercase transition-colors hover:text-brand-navy/70"
|
||||
>{m.register_sign_in()}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user