feat: redesign login page with left-right split layout and OAuth buttons

This commit is contained in:
2026-04-03 13:18:05 +08:00
parent a7027c8aaa
commit db4e5540ad

View File

@@ -1,6 +1,9 @@
import { useState } from 'react'; import { useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } 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 Login() { export default function Login() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -11,7 +14,8 @@ export default function Login() {
const { login } = useAuth(); const { login } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const redirectTo = searchParams.get('redirect') || '/'; const redirectTo = searchParams.get('redirect') || '/dashboard';
const { t } = useI18n();
const validate = () => { const validate = () => {
const errors: { email?: string; password?: string } = {}; const errors: { email?: string; password?: string } = {};
@@ -43,82 +47,104 @@ export default function Login() {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-bg-primary relative overflow-hidden"> <div className="min-h-screen flex">
{/* Subtle grid background */} {/* Left panel — branding (hidden on mobile) */}
<div className="absolute inset-0" style={{ <AuthBranding />
backgroundImage: `linear-gradient(var(--border-muted) 1px, transparent 1px), linear-gradient(90deg, var(--border-muted) 1px, transparent 1px)`,
backgroundSize: '48px 48px',
}} />
{/* Radial fade */}
<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 */}
{/* Brand */} <div className="flex-1 flex items-center justify-center p-6 bg-bg-primary relative overflow-hidden">
<div className="text-center mb-8"> {/* Subtle grid background */}
<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 (visible when left panel is hidden) */}
<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]">Sign in to AgentFox</h1>
<p className="text-[13px] text-text-muted mt-1">API documentation for LLMs</p>
</div>
{/* Card */} {/* Title (desktop) */}
<div className="card p-6 shadow-md"> <div className="hidden lg:block mb-8">
{error && ( <h1 className="text-2xl font-semibold text-text-primary tracking-[-0.01em]">{t('auth.login.title')}</h1>
<div className="mb-4 p-3 rounded-lg bg-danger-muted flex items-center gap-2"> </div>
<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">Email</label>
<input
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); if (fieldErrors.email) setFieldErrors(prev => ({ ...prev, email: undefined })); }}
className={`input-base ${fieldErrors.email ? 'border-danger! focus:border-danger! focus:shadow-[0_0_0_3px_var(--danger-muted)]!' : ''}`}
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); if (fieldErrors.password) setFieldErrors(prev => ({ ...prev, password: undefined })); }}
className={`input-base ${fieldErrors.password ? 'border-danger! focus:border-danger! focus:shadow-[0_0_0_3px_var(--danger-muted)]!' : ''}`}
placeholder="Enter your password"
/>
{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> Signing in...</>
) : 'Sign In'}
</button>
</form>
</div>
<p className="text-center text-[13px] text-text-muted mt-6"> {/* Card */}
Don't have an account?{' '} <div className="card p-6 shadow-md">
<Link to="/register" className="text-accent hover:underline font-medium">Sign Up</Link> {error && (
</p> <div className="mb-4 p-3 rounded-lg bg-danger-muted flex items-center gap-2">
<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.login.email')}</label>
<input
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); if (fieldErrors.email) setFieldErrors(prev => ({ ...prev, email: undefined })); }}
className={`input-base ${fieldErrors.email ? 'border-danger! focus:border-danger! focus:shadow-[0_0_0_3px_var(--danger-muted)]!' : ''}`}
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.login.password')}</label>
<input
type="password"
value={password}
onChange={(e) => { setPassword(e.target.value); if (fieldErrors.password) setFieldErrors(prev => ({ ...prev, password: undefined })); }}
className={`input-base ${fieldErrors.password ? 'border-danger! focus:border-danger! focus:shadow-[0_0_0_3px_var(--danger-muted)]!' : ''}`}
placeholder="Enter your password"
/>
{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.login.submitting')}</>
) : t('auth.login.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.login.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.login.noAccount')}{' '}
<Link to="/register" className="text-accent hover:underline font-medium">{t('auth.login.signUp')}</Link>
</p>
</div>
</div> </div>
</div> </div>
); );