style: upgrade sign-up page UI to match sign-in page aesthetics
- Add brand gradient title, status badge, and descriptive subtitle - Polished form inputs: h-12 rounded-xl with icons matching sign-in - Password fields in 2-column grid layout on desktop - Brand gradient submit button with hover/active animations - Styled verification code row with matching heights - Consistent uppercase tracking labels throughout - "Already registered?" section with link matching sign-in's footer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+102
-58
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import type { z } from 'zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Loader2, User, Mail, KeyRound, ShieldCheck, UserPlus } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -212,88 +212,121 @@ export function SignUpForm({
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={cn('grid gap-4', className)}
|
||||
className={cn('space-y-5', className)}
|
||||
{...props}
|
||||
>
|
||||
{/* Username Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='username'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Username')}</FormLabel>
|
||||
<FormLabel className='text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
||||
{t('Username')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('Enter your username')} {...field} />
|
||||
<div className='relative'>
|
||||
<User className='pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground' />
|
||||
<Input
|
||||
placeholder={t('Enter your username')}
|
||||
autoComplete='username'
|
||||
className='h-12 rounded-xl border-[color-mix(in_oklch,var(--primary)_28%,var(--border))] bg-[color-mix(in_oklch,var(--background)_82%,transparent)] pl-10 text-sm shadow-[inset_0_1px_0_color-mix(in_oklch,var(--primary)_18%,transparent)] focus-visible:ring-primary/40'
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Password Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Password')}</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
placeholder={t('Enter password (8-20 characters)')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className='grid gap-5 sm:grid-cols-2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
||||
{t('Password')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className='relative'>
|
||||
<KeyRound className='pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground' />
|
||||
<PasswordInput
|
||||
placeholder={t('8-20 characters')}
|
||||
autoComplete='new-password'
|
||||
className='h-12 rounded-xl border-[color-mix(in_oklch,var(--primary)_28%,var(--border))] bg-[color-mix(in_oklch,var(--background)_82%,transparent)] pl-10 text-sm shadow-[inset_0_1px_0_color-mix(in_oklch,var(--primary)_18%,transparent)] focus-visible:ring-primary/40'
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Confirm Password Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='confirmPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Confirm password')}</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder={t('Confirm password')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='confirmPassword'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
||||
{t('Confirm password')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className='relative'>
|
||||
<ShieldCheck className='pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground' />
|
||||
<PasswordInput
|
||||
placeholder={t('Confirm password')}
|
||||
autoComplete='new-password'
|
||||
className='h-12 rounded-xl border-[color-mix(in_oklch,var(--primary)_28%,var(--border))] bg-[color-mix(in_oklch,var(--background)_82%,transparent)] pl-10 text-sm shadow-[inset_0_1px_0_color-mix(in_oklch,var(--primary)_18%,transparent)] focus-visible:ring-primary/40'
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email Verification Section */}
|
||||
{emailVerificationRequired && (
|
||||
<>
|
||||
{/* Email Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('Email (required for verification)')}
|
||||
<FormLabel className='text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
||||
{t('Email')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('name@example.com')}
|
||||
type='email'
|
||||
{...field}
|
||||
/>
|
||||
<div className='relative'>
|
||||
<Mail className='pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground' />
|
||||
<Input
|
||||
placeholder={t('name@example.com')}
|
||||
type='email'
|
||||
autoComplete='email'
|
||||
className='h-12 rounded-xl border-[color-mix(in_oklch,var(--primary)_28%,var(--border))] bg-[color-mix(in_oklch,var(--background)_82%,transparent)] pl-10 text-sm shadow-[inset_0_1px_0_color-mix(in_oklch,var(--primary)_18%,transparent)] focus-visible:ring-primary/40'
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Verification Code Field */}
|
||||
<div className='flex items-end gap-2'>
|
||||
<div className='flex items-end gap-3'>
|
||||
<div className='flex-1'>
|
||||
<label className='mb-2 block text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
||||
{t('Verification code')}
|
||||
</label>
|
||||
<Input
|
||||
placeholder={t('Verification code')}
|
||||
placeholder={t('Enter code')}
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
autoComplete='one-time-code'
|
||||
className='h-12 rounded-xl border-[color-mix(in_oklch,var(--primary)_28%,var(--border))] bg-[color-mix(in_oklch,var(--background)_82%,transparent)] text-sm shadow-[inset_0_1px_0_color-mix(in_oklch,var(--primary)_18%,transparent)] focus-visible:ring-primary/40'
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@@ -301,6 +334,7 @@ export function SignUpForm({
|
||||
type='button'
|
||||
disabled={isLoading || isSendingCode || isActive || !emailValue}
|
||||
onClick={handleSendVerificationCode}
|
||||
className='h-12 shrink-0 rounded-xl border-[color-mix(in_oklch,var(--primary)_28%,var(--border))] px-5 text-sm font-semibold'
|
||||
>
|
||||
{isActive ? (
|
||||
t('Resend ({{seconds}}s)', { seconds: secondsLeft })
|
||||
@@ -312,14 +346,11 @@ export function SignUpForm({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Turnstile */}
|
||||
{isTurnstileEnabled && (
|
||||
<div className='mt-2'>
|
||||
<Turnstile
|
||||
siteKey={turnstileSiteKey}
|
||||
onVerify={setTurnstileToken}
|
||||
/>
|
||||
</div>
|
||||
<Turnstile
|
||||
siteKey={turnstileSiteKey}
|
||||
onVerify={setTurnstileToken}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -331,15 +362,28 @@ export function SignUpForm({
|
||||
className='mt-1'
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
type='submit'
|
||||
className='h-12 w-full justify-center gap-2 rounded-xl text-sm font-semibold text-white shadow-[0_18px_48px_-18px_rgba(123,107,227,0.65)] transition-transform hover:translate-y-[-1px] hover:shadow-[0_22px_56px_-18px_rgba(123,107,227,0.75)] active:translate-y-0 active:scale-[0.99]'
|
||||
style={{
|
||||
backgroundImage: 'var(--gradient-brand-btn)',
|
||||
backgroundColor: 'var(--brand)',
|
||||
border: '1px solid rgba(255,255,255,0.16)',
|
||||
}}
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
>
|
||||
{isLoading ? <Loader2 className='h-4 w-4 animate-spin' /> : null}
|
||||
{isLoading ? (
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
) : (
|
||||
<UserPlus className='h-4 w-4' />
|
||||
)}
|
||||
{t('Create account')}
|
||||
</Button>
|
||||
|
||||
<p className='text-center text-[11px] tracking-[0.12em] text-muted-foreground uppercase'>
|
||||
{t('Verified via Heicode identity service')}
|
||||
</p>
|
||||
|
||||
{oauthRegisterEnabled && (
|
||||
<OAuthProviders
|
||||
status={status}
|
||||
|
||||
+42
-13
@@ -1,5 +1,6 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowUpRight } from 'lucide-react'
|
||||
import { useStatus } from '@/hooks/use-status'
|
||||
import { AuthLayout } from '../auth-layout'
|
||||
import { TermsFooter } from '../components/terms-footer'
|
||||
@@ -11,25 +12,53 @@ export function SignUp() {
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div className='w-full space-y-8'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-center text-2xl font-semibold tracking-tight sm:text-left'>
|
||||
{t('Create an account')}
|
||||
<div className='space-y-8'>
|
||||
<div className='space-y-3'>
|
||||
<span
|
||||
className='inline-flex items-center gap-2 rounded-full px-3 py-1 text-[11px] font-semibold tracking-[0.18em] uppercase'
|
||||
style={{
|
||||
border: '1px solid rgba(123,107,227,0.35)',
|
||||
backgroundColor: 'rgba(123,107,227,0.10)',
|
||||
color: 'var(--brand)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className='inline-block h-1.5 w-1.5 rounded-full'
|
||||
style={{ backgroundColor: 'var(--brand)' }}
|
||||
/>
|
||||
{t('New account')}
|
||||
</span>
|
||||
<h2
|
||||
className='text-3xl font-semibold tracking-tight sm:text-4xl'
|
||||
style={{
|
||||
backgroundImage: 'var(--gradient-brand)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
}}
|
||||
>
|
||||
{t('Create your account')}
|
||||
</h2>
|
||||
<p className='text-muted-foreground text-left text-sm sm:text-base'>
|
||||
{t('Already have an account?')}{' '}
|
||||
<Link
|
||||
to='/sign-in'
|
||||
className='hover:text-primary font-medium underline underline-offset-4'
|
||||
>
|
||||
{t('Sign in')}
|
||||
</Link>
|
||||
.
|
||||
<p className='text-sm text-muted-foreground sm:text-base'>
|
||||
{t('Register to access the Heicode platform. Email verification is required to complete your account setup.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SignUpForm />
|
||||
|
||||
<div className='space-y-3 border-t border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] pt-5'>
|
||||
<p className='text-xs font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||
{t('Already registered?')}
|
||||
</p>
|
||||
<Link
|
||||
to='/sign-in'
|
||||
className='inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline'
|
||||
>
|
||||
{t('Sign in to your workspace')}
|
||||
<ArrowUpRight className='h-3.5 w-3.5' />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<TermsFooter
|
||||
variant='sign-up'
|
||||
status={status}
|
||||
|
||||
Reference in New Issue
Block a user