feat: redesign register page with left-right split layout and OAuth buttons
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../lib/auth';
|
import { useAuth } from '../lib/auth';
|
||||||
|
import { useI18n } from '../lib/i18n';
|
||||||
|
import AuthBranding from '../components/AuthBranding';
|
||||||
|
import OAuthButtons from '../components/OAuthButtons';
|
||||||
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
@@ -11,6 +14,7 @@ export default function Register() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const clearFieldError = (field: string) => {
|
const clearFieldError = (field: string) => {
|
||||||
if (fieldErrors[field as keyof typeof fieldErrors]) {
|
if (fieldErrors[field as keyof typeof fieldErrors]) {
|
||||||
@@ -20,9 +24,7 @@ export default function Register() {
|
|||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const errors: { name?: string; email?: string; password?: string } = {};
|
const errors: { name?: string; email?: string; password?: string } = {};
|
||||||
if (!name.trim()) {
|
if (!name.trim()) errors.name = 'Name is required';
|
||||||
errors.name = 'Name is required';
|
|
||||||
}
|
|
||||||
if (!email.trim()) {
|
if (!email.trim()) {
|
||||||
errors.email = 'Email is required';
|
errors.email = 'Email is required';
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
@@ -55,94 +57,119 @@ export default function Register() {
|
|||||||
const errorInputClass = 'border-danger! focus:border-danger! focus:shadow-[0_0_0_3px_var(--danger-muted)]!';
|
const errorInputClass = 'border-danger! focus:border-danger! focus:shadow-[0_0_0_3px_var(--danger-muted)]!';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-bg-primary relative overflow-hidden">
|
<div className="min-h-screen flex">
|
||||||
<div className="absolute inset-0" style={{
|
{/* Left panel — branding (hidden on mobile) */}
|
||||||
backgroundImage: `linear-gradient(var(--border-muted) 1px, transparent 1px), linear-gradient(90deg, var(--border-muted) 1px, transparent 1px)`,
|
<AuthBranding />
|
||||||
backgroundSize: '48px 48px',
|
|
||||||
}} />
|
|
||||||
<div className="absolute inset-0" style={{
|
|
||||||
background: `radial-gradient(ellipse at center, transparent 0%, var(--bg-primary) 70%)`,
|
|
||||||
}} />
|
|
||||||
|
|
||||||
<div className="w-full max-w-[360px] mx-4 relative animate-slide-up">
|
{/* Right panel — form */}
|
||||||
<div className="text-center mb-8">
|
<div className="flex-1 flex items-center justify-center p-6 bg-bg-primary relative overflow-hidden">
|
||||||
<div className="w-11 h-11 rounded-xl bg-accent mx-auto flex items-center justify-center mb-4 shadow-md">
|
<div className="absolute inset-0" style={{
|
||||||
<svg className="w-5 h-5 text-white" viewBox="0 0 20 20" fill="currentColor">
|
backgroundImage: `linear-gradient(var(--border-muted) 1px, transparent 1px), linear-gradient(90deg, var(--border-muted) 1px, transparent 1px)`,
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
backgroundSize: '48px 48px',
|
||||||
</svg>
|
}} />
|
||||||
|
<div className="absolute inset-0" style={{
|
||||||
|
background: `radial-gradient(ellipse at center, transparent 0%, var(--bg-primary) 70%)`,
|
||||||
|
}} />
|
||||||
|
|
||||||
|
<div className="w-full max-w-[400px] relative animate-slide-up">
|
||||||
|
{/* Mobile-only brand */}
|
||||||
|
<div className="lg:hidden text-center mb-8">
|
||||||
|
<div className="w-11 h-11 rounded-xl bg-accent mx-auto flex items-center justify-center mb-4 shadow-md">
|
||||||
|
<svg className="w-5 h-5 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M12 2L2 7l10 5 10-5-10-5z" />
|
||||||
|
<path d="M2 17l10 5 10-5" />
|
||||||
|
<path d="M2 12l10 5 10-5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-semibold text-text-primary tracking-[-0.01em]">{t('auth.productName')}</h1>
|
||||||
|
<p className="text-[13px] text-text-muted mt-1">{t('auth.slogan')}</p>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-semibold text-text-primary tracking-[-0.01em]">Create your account</h1>
|
|
||||||
<p className="text-[13px] text-text-muted mt-1">Get started with AgentFox</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card p-6 shadow-md">
|
{/* Title (desktop) */}
|
||||||
{error && (
|
<div className="hidden lg:block mb-8">
|
||||||
<div className="mb-4 p-3 rounded-lg bg-danger-muted flex items-center gap-2">
|
<h1 className="text-2xl font-semibold text-text-primary tracking-[-0.01em]">{t('auth.register.title')}</h1>
|
||||||
<svg className="w-4 h-4 text-danger shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><circle cx="12" cy="12" r="10" /><path d="M15 9l-6 6m0-6l6 6" /></svg>
|
<p className="text-[13px] text-text-muted mt-1">{t('auth.register.subtitle')}</p>
|
||||||
<span className="text-danger text-[13px]">{error}</span>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<form onSubmit={handleSubmit} noValidate className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-[13px] font-medium text-text-secondary mb-1.5">Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => { setName(e.target.value); clearFieldError('name'); }}
|
|
||||||
className={`input-base ${fieldErrors.name ? errorInputClass : ''}`}
|
|
||||||
placeholder="Your name"
|
|
||||||
/>
|
|
||||||
{fieldErrors.name && (
|
|
||||||
<p className="mt-1.5 text-[12px] text-danger flex items-center gap-1">
|
|
||||||
<svg className="w-3 h-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><circle cx="12" cy="12" r="10" /><path d="M12 8v4m0 4h.01" /></svg>
|
|
||||||
{fieldErrors.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-[13px] font-medium text-text-secondary mb-1.5">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => { setEmail(e.target.value); clearFieldError('email'); }}
|
|
||||||
className={`input-base ${fieldErrors.email ? errorInputClass : ''}`}
|
|
||||||
placeholder="you@example.com"
|
|
||||||
/>
|
|
||||||
{fieldErrors.email && (
|
|
||||||
<p className="mt-1.5 text-[12px] text-danger flex items-center gap-1">
|
|
||||||
<svg className="w-3 h-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><circle cx="12" cy="12" r="10" /><path d="M12 8v4m0 4h.01" /></svg>
|
|
||||||
{fieldErrors.email}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-[13px] font-medium text-text-secondary mb-1.5">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => { setPassword(e.target.value); clearFieldError('password'); }}
|
|
||||||
className={`input-base ${fieldErrors.password ? errorInputClass : ''}`}
|
|
||||||
placeholder="At least 8 characters"
|
|
||||||
/>
|
|
||||||
{fieldErrors.password && (
|
|
||||||
<p className="mt-1.5 text-[12px] text-danger flex items-center gap-1">
|
|
||||||
<svg className="w-3 h-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><circle cx="12" cy="12" r="10" /><path d="M12 8v4m0 4h.01" /></svg>
|
|
||||||
{fieldErrors.password}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button type="submit" disabled={loading} className="btn-primary w-full">
|
|
||||||
{loading ? (
|
|
||||||
<><svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg> Creating account...</>
|
|
||||||
) : 'Create Account'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-center text-[13px] text-text-muted mt-6">
|
<div className="card p-6 shadow-md">
|
||||||
Already have an account?{' '}
|
{error && (
|
||||||
<Link to="/login" className="text-accent hover:underline font-medium">Sign In</Link>
|
<div className="mb-4 p-3 rounded-lg bg-danger-muted flex items-center gap-2">
|
||||||
</p>
|
<svg className="w-4 h-4 text-danger shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><circle cx="12" cy="12" r="10" /><path d="M15 9l-6 6m0-6l6 6" /></svg>
|
||||||
|
<span className="text-danger text-[13px]">{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmit} noValidate className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[13px] font-medium text-text-secondary mb-1.5">{t('auth.register.name')}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => { setName(e.target.value); clearFieldError('name'); }}
|
||||||
|
className={`input-base ${fieldErrors.name ? errorInputClass : ''}`}
|
||||||
|
placeholder="Your name"
|
||||||
|
/>
|
||||||
|
{fieldErrors.name && (
|
||||||
|
<p className="mt-1.5 text-[12px] text-danger flex items-center gap-1">
|
||||||
|
<svg className="w-3 h-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><circle cx="12" cy="12" r="10" /><path d="M12 8v4m0 4h.01" /></svg>
|
||||||
|
{fieldErrors.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[13px] font-medium text-text-secondary mb-1.5">{t('auth.register.email')}</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => { setEmail(e.target.value); clearFieldError('email'); }}
|
||||||
|
className={`input-base ${fieldErrors.email ? errorInputClass : ''}`}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
{fieldErrors.email && (
|
||||||
|
<p className="mt-1.5 text-[12px] text-danger flex items-center gap-1">
|
||||||
|
<svg className="w-3 h-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><circle cx="12" cy="12" r="10" /><path d="M12 8v4m0 4h.01" /></svg>
|
||||||
|
{fieldErrors.email}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[13px] font-medium text-text-secondary mb-1.5">{t('auth.register.password')}</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => { setPassword(e.target.value); clearFieldError('password'); }}
|
||||||
|
className={`input-base ${fieldErrors.password ? errorInputClass : ''}`}
|
||||||
|
placeholder="At least 8 characters"
|
||||||
|
/>
|
||||||
|
{fieldErrors.password && (
|
||||||
|
<p className="mt-1.5 text-[12px] text-danger flex items-center gap-1">
|
||||||
|
<svg className="w-3 h-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><circle cx="12" cy="12" r="10" /><path d="M12 8v4m0 4h.01" /></svg>
|
||||||
|
{fieldErrors.password}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={loading} className="btn-primary w-full">
|
||||||
|
{loading ? (
|
||||||
|
<><svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg> {t('auth.register.submitting')}</>
|
||||||
|
) : t('auth.register.submit')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 my-5">
|
||||||
|
<div className="flex-1 h-px bg-border-default" />
|
||||||
|
<span className="text-[12px] text-text-muted">{t('auth.register.or')}</span>
|
||||||
|
<div className="flex-1 h-px bg-border-default" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OAuth buttons */}
|
||||||
|
<OAuthButtons />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-[13px] text-text-muted mt-6">
|
||||||
|
{t('auth.register.hasAccount')}{' '}
|
||||||
|
<Link to="/login" className="text-accent hover:underline font-medium">{t('auth.register.signIn')}</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user