409 lines
17 KiB
TypeScript
409 lines
17 KiB
TypeScript
import { useState, type FormEvent } from 'react'
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardContent,
|
|
CardFooter,
|
|
Button,
|
|
Input,
|
|
Chip,
|
|
ChipLabel,
|
|
Alert,
|
|
AlertTitle,
|
|
AlertDescription,
|
|
} from '@heroui/react'
|
|
import {
|
|
Mail,
|
|
Lock,
|
|
User,
|
|
ArrowRight,
|
|
Eye,
|
|
EyeOff,
|
|
ArrowLeft,
|
|
ShieldCheck,
|
|
} from 'lucide-react'
|
|
import { useAuth } from '../contexts/AuthContext'
|
|
|
|
export const AuthPage = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
const initialMode = searchParams.get('mode') === 'register' ? 'register' : 'login'
|
|
const initialEmail = searchParams.get('email') || ''
|
|
const [mode, setMode] = useState<'login' | 'register'>(initialMode)
|
|
|
|
// Form states
|
|
const [name, setName] = useState('')
|
|
const [email, setEmail] = useState(initialEmail)
|
|
const [password, setPassword] = useState('')
|
|
const [confirmPassword, setConfirmPassword] = useState('')
|
|
const [showPassword, setShowPassword] = useState(false)
|
|
|
|
// Status & UI
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
|
|
|
const { login, register } = useAuth()
|
|
const navigate = useNavigate()
|
|
|
|
const switchMode = (newMode: 'login' | 'register') => {
|
|
setMode(newMode)
|
|
setSearchParams({ mode: newMode })
|
|
setErrorMessage(null)
|
|
setSuccessMessage(null)
|
|
}
|
|
|
|
const handleLogin = async (e: FormEvent) => {
|
|
e.preventDefault()
|
|
setErrorMessage(null)
|
|
setIsLoading(true)
|
|
|
|
try {
|
|
const response = await login(email, password)
|
|
if (!response.organizations || response.organizations.length === 0) {
|
|
navigate('/organizations')
|
|
} else {
|
|
navigate('/dashboard')
|
|
}
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : 'Erro ao realizar login. Verifique suas credenciais.'
|
|
setErrorMessage(msg)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleRegister = async (e: FormEvent) => {
|
|
e.preventDefault()
|
|
setErrorMessage(null)
|
|
|
|
if (password.length < 6) {
|
|
setErrorMessage('A senha deve conter no mínimo 6 caracteres.')
|
|
return
|
|
}
|
|
|
|
if (password !== confirmPassword) {
|
|
setErrorMessage('As senhas digitadas não coincidem.')
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
|
|
try {
|
|
await register(name, email, password)
|
|
setSuccessMessage('Conta criada com sucesso! Faça login para acessar seu workspace.')
|
|
setMode('login')
|
|
setSearchParams({ mode: 'login' })
|
|
setPassword('')
|
|
setConfirmPassword('')
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : 'Erro ao criar conta. Tente novamente.'
|
|
setErrorMessage(msg)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-app-bg text-text-main flex flex-col justify-center items-center px-4 py-12 relative overflow-hidden font-sans">
|
|
{/* Decorative ambient gradients */}
|
|
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none -z-10">
|
|
<div className="absolute -top-32 -left-32 w-96 h-96 bg-accent/15 blur-[120px] rounded-full" />
|
|
<div className="absolute -bottom-32 -right-32 w-96 h-96 bg-brand/15 blur-[120px] rounded-full" />
|
|
</div>
|
|
|
|
{/* Top back navigation */}
|
|
<div className="w-full max-w-md mb-6 flex items-center justify-between">
|
|
<Link
|
|
to="/"
|
|
className="inline-flex items-center gap-2 text-xs font-semibold text-text-muted hover:text-brand transition-colors p-2 rounded-xl hover:bg-surface-muted"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Voltar para a página inicial
|
|
</Link>
|
|
<Chip className="bg-brand-soft text-brand text-xs font-semibold px-2 py-0.5 border border-brand/20">
|
|
<ChipLabel>v3.0</ChipLabel>
|
|
</Chip>
|
|
</div>
|
|
|
|
{/* Main Auth Card */}
|
|
<Card className="w-full max-w-md bg-surface border border-border-soft rounded-3xl p-6 sm:p-8 shadow-panel relative z-10 transition-all">
|
|
{/* Card Header & Brand */}
|
|
<CardHeader className="p-0 pb-6 flex flex-col items-center text-center space-y-3">
|
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-accent to-brand flex items-center justify-center shadow-md shadow-accent/20">
|
|
<span className="text-2xl select-none">🐝</span>
|
|
</div>
|
|
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-text-main">
|
|
Bee<span className="text-accent">Task</span>
|
|
</h1>
|
|
<p className="text-xs sm:text-sm text-text-muted mt-1">
|
|
{mode === 'login'
|
|
? 'Acesse seu painel com segurança e foco'
|
|
: 'Crie seu workspace corporativo em segundos'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Mode Switcher Tabs */}
|
|
<div className="w-full grid grid-cols-2 p-1 bg-surface-muted rounded-xl border border-border-soft mt-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => switchMode('login')}
|
|
className={`py-2 text-xs sm:text-sm font-bold rounded-lg transition-all ${
|
|
mode === 'login'
|
|
? 'bg-surface text-brand shadow-xs border border-border-soft'
|
|
: 'text-text-muted hover:text-text-main'
|
|
}`}
|
|
>
|
|
Entrar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => switchMode('register')}
|
|
className={`py-2 text-xs sm:text-sm font-bold rounded-lg transition-all ${
|
|
mode === 'register'
|
|
? 'bg-surface text-brand shadow-xs border border-border-soft'
|
|
: 'text-text-muted hover:text-text-main'
|
|
}`}
|
|
>
|
|
Criar Conta
|
|
</button>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
{/* Feedback Alerts */}
|
|
<CardContent className="p-0 space-y-5">
|
|
{errorMessage && (
|
|
<Alert className="bg-danger-soft border border-danger/30 text-danger rounded-xl p-3 text-xs flex items-start gap-2">
|
|
<div className="flex-1">
|
|
<AlertTitle className="font-bold">Atenção</AlertTitle>
|
|
<AlertDescription className="text-xs mt-0.5">{errorMessage}</AlertDescription>
|
|
</div>
|
|
</Alert>
|
|
)}
|
|
|
|
{successMessage && (
|
|
<Alert className="bg-success-soft border border-success/30 text-success rounded-xl p-3 text-xs flex items-start gap-2">
|
|
<div className="flex-1">
|
|
<AlertTitle className="font-bold">Sucesso</AlertTitle>
|
|
<AlertDescription className="text-xs mt-0.5">{successMessage}</AlertDescription>
|
|
</div>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Mode Form */}
|
|
{mode === 'login' ? (
|
|
<form onSubmit={handleLogin} className="space-y-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-text-main">
|
|
E-mail Profissional
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
|
<Mail className="w-4 h-4" />
|
|
</div>
|
|
<Input
|
|
type="email"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="seu.email@empresa.com"
|
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-xs font-bold text-text-main">
|
|
Senha de Acesso
|
|
</label>
|
|
<a
|
|
href="#esqueci"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
alert('Entre em contato com o administrador do seu Workspace para redefinir sua senha.')
|
|
}}
|
|
className="text-[11px] font-semibold text-brand hover:underline"
|
|
>
|
|
Esqueceu?
|
|
</a>
|
|
</div>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
|
<Lock className="w-4 h-4" />
|
|
</div>
|
|
<Input
|
|
type={showPassword ? 'text' : 'password'}
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="••••••••"
|
|
className="w-full pl-10 pr-10 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-text-muted hover:text-text-main transition-colors"
|
|
>
|
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
isDisabled={isLoading}
|
|
className="w-full bg-brand hover:bg-brand-strong text-white font-bold text-sm py-3 rounded-xl shadow-lg shadow-brand/20 transition-all duration-200 flex items-center justify-center gap-2 mt-2"
|
|
>
|
|
{isLoading ? (
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
) : (
|
|
<>
|
|
Acessar Workspace
|
|
<ArrowRight className="w-4 h-4" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
</form>
|
|
) : (
|
|
<form onSubmit={handleRegister} className="space-y-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-text-main">
|
|
Nome Completo
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
|
<User className="w-4 h-4" />
|
|
</div>
|
|
<Input
|
|
type="text"
|
|
required
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Ex: Carlos Oliveira"
|
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-text-main">
|
|
E-mail Corporativo
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
|
<Mail className="w-4 h-4" />
|
|
</div>
|
|
<Input
|
|
type="email"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="seu.email@empresa.com"
|
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-text-main">
|
|
Crie uma Senha
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
|
<Lock className="w-4 h-4" />
|
|
</div>
|
|
<Input
|
|
type={showPassword ? 'text' : 'password'}
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Mínimo 6 dígitos"
|
|
className="w-full pl-10 pr-10 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-text-muted hover:text-text-main transition-colors"
|
|
>
|
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-text-main">
|
|
Confirme sua Senha
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
|
<Lock className="w-4 h-4" />
|
|
</div>
|
|
<Input
|
|
type={showPassword ? 'text' : 'password'}
|
|
required
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
placeholder="Repita a senha"
|
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
isDisabled={isLoading}
|
|
className="w-full bg-accent hover:bg-[#b45309] text-white font-bold text-sm py-3 rounded-xl shadow-lg shadow-accent/20 transition-all duration-200 flex items-center justify-center gap-2 mt-2"
|
|
>
|
|
{isLoading ? (
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
) : (
|
|
<>
|
|
Criar Minha Conta
|
|
<ArrowRight className="w-4 h-4" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</CardContent>
|
|
|
|
{/* Footer Links & Info */}
|
|
<CardFooter className="p-0 pt-6 mt-6 border-t border-border-soft flex flex-col items-center space-y-3 text-center">
|
|
<p className="text-xs text-text-muted">
|
|
{mode === 'login' ? (
|
|
<>
|
|
Ainda não tem conta no Bee Task?{' '}
|
|
<button
|
|
type="button"
|
|
onClick={() => switchMode('register')}
|
|
className="font-bold text-brand hover:underline"
|
|
>
|
|
Cadastre-se grátis
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
Já possui uma conta ativa?{' '}
|
|
<button
|
|
type="button"
|
|
onClick={() => switchMode('login')}
|
|
className="font-bold text-brand hover:underline"
|
|
>
|
|
Fazer login
|
|
</button>
|
|
</>
|
|
)}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-center gap-2 text-[11px] text-text-muted font-medium pt-1">
|
|
<ShieldCheck className="w-3.5 h-3.5 text-brand" />
|
|
<span>Autenticação segura com JWT Quarkus & MinIO S3</span>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|