252 lines
9.1 KiB
TypeScript
252 lines
9.1 KiB
TypeScript
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
import { useEffect, useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { Boxes, Eye, EyeOff, KeyRound, Loader2, LogIn, UserPlus } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { criarConta, entrar, obterSessaoAtual, normalizarNome, recuperarSenha } from "@/lib/auth";
|
|
|
|
export const Route = createFileRoute("/auth")({
|
|
head: () => ({
|
|
meta: [
|
|
{ title: "Entrar | Coleta de Caixas" },
|
|
{
|
|
name: "description",
|
|
content:
|
|
"Acesse sua conta para registrar o início e o fim das coletas de caixas. Novas contas precisam de aprovação de um administrador.",
|
|
},
|
|
{ property: "og:title", content: "Entrar | Coleta de Caixas" },
|
|
{
|
|
property: "og:description",
|
|
content:
|
|
"Acesse sua conta para registrar o início e o fim das coletas de caixas. Novas contas precisam de aprovação de um administrador.",
|
|
},
|
|
{ property: "og:type", content: "website" },
|
|
{ name: "twitter:card", content: "summary" },
|
|
],
|
|
}),
|
|
component: AuthPage,
|
|
});
|
|
|
|
function AuthPage() {
|
|
const navigate = useNavigate();
|
|
const [modo, setModo] = useState<"entrar" | "criar" | "recuperar">("entrar");
|
|
const [nome, setNome] = useState("");
|
|
const [senha, setSenha] = useState("");
|
|
const [confirmarSenha, setConfirmarSenha] = useState("");
|
|
const [mostrarSenha, setMostrarSenha] = useState(false);
|
|
const [carregando, setCarregando] = useState(false);
|
|
const [verificando, setVerificando] = useState(true);
|
|
const precisaConfirmarSenha = modo !== "entrar";
|
|
|
|
useEffect(() => {
|
|
let cancelado = false;
|
|
void obterSessaoAtual().then((sessao) => {
|
|
if (cancelado) return;
|
|
if (sessao) void navigate({ to: "/coleta", replace: true });
|
|
else setVerificando(false);
|
|
});
|
|
return () => {
|
|
cancelado = true;
|
|
};
|
|
}, [navigate]);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
const usuario = normalizarNome(nome);
|
|
if (usuario.length < 3 || senha.length < 6) {
|
|
toast.error("Informe um nome com pelo menos 3 caracteres e senha com 6 ou mais.");
|
|
return;
|
|
}
|
|
if (precisaConfirmarSenha && senha !== confirmarSenha) {
|
|
toast.error("As senhas digitadas não coincidem.");
|
|
return;
|
|
}
|
|
setCarregando(true);
|
|
try {
|
|
if (modo === "criar") {
|
|
const resultado = await criarConta({ data: { nome: usuario, senha } });
|
|
toast.success(
|
|
resultado.aprovado
|
|
? "Conta criada e liberada. Faça login."
|
|
: "Conta criada! Aguarde a liberação de um administrador.",
|
|
);
|
|
} else if (modo === "recuperar") {
|
|
await recuperarSenha({ data: { nome: usuario, senha } });
|
|
toast.success("Senha redefinida. Aguarde a aprovação de um administrador.");
|
|
setModo("entrar");
|
|
} else {
|
|
await entrar({ data: { nome: usuario, senha } });
|
|
void navigate({ to: "/coleta", replace: true });
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
const msg = (error as { message?: string })?.message ?? "";
|
|
toast.error(
|
|
msg.includes("incorretos")
|
|
? "Nome de usuário ou senha incorretos."
|
|
: msg.includes("não encontrado")
|
|
? "Nome de usuário não encontrado."
|
|
: msg.includes("já existe")
|
|
? "Este nome de usuário já existe. Faça login."
|
|
: "Não foi possível concluir. Tente novamente.",
|
|
);
|
|
} finally {
|
|
setCarregando(false);
|
|
}
|
|
}
|
|
|
|
if (verificando) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-background">
|
|
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
|
|
<div className="w-full max-w-sm space-y-6 rounded-2xl border border-border bg-card p-6 shadow-soft">
|
|
<div className="space-y-2 text-center">
|
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10">
|
|
<Boxes className="h-6 w-6 text-primary" />
|
|
</div>
|
|
<h1 className="text-xl font-semibold text-foreground">Coleta de Caixas</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
{modo === "entrar"
|
|
? "Entre com seu nome de usuário e senha."
|
|
: modo === "criar"
|
|
? "Crie sua conta — o acesso é liberado por um administrador."
|
|
: "Defina uma nova senha. Depois disso, a conta volta para aprovação do administrador."}
|
|
</p>
|
|
</div>
|
|
|
|
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="nome">Nome de usuário</Label>
|
|
<Input
|
|
id="nome"
|
|
type="text"
|
|
autoComplete="username"
|
|
autoCapitalize="none"
|
|
value={nome}
|
|
onChange={(e) => setNome(e.target.value)}
|
|
placeholder="joao.silva"
|
|
className="h-12 rounded-xl"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="senha">Senha</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="senha"
|
|
type={mostrarSenha ? "text" : "password"}
|
|
autoComplete={modo === "entrar" ? "current-password" : "new-password"}
|
|
value={senha}
|
|
onChange={(e) => setSenha(e.target.value)}
|
|
placeholder="Mínimo de 6 caracteres"
|
|
className="h-12 rounded-xl pr-12"
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute top-[14px] right-3 text-muted-foreground"
|
|
onClick={() => setMostrarSenha((atual) => !atual)}
|
|
aria-label={mostrarSenha ? "Ocultar senha" : "Mostrar senha"}
|
|
>
|
|
{mostrarSenha ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{precisaConfirmarSenha ? (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="confirmar-senha">Confirmar senha</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="confirmar-senha"
|
|
type={mostrarSenha ? "text" : "password"}
|
|
autoComplete="new-password"
|
|
value={confirmarSenha}
|
|
onChange={(e) => setConfirmarSenha(e.target.value)}
|
|
placeholder="Repita a senha"
|
|
className="h-12 rounded-xl pr-12"
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute top-[14px] right-3 text-muted-foreground"
|
|
onClick={() => setMostrarSenha((atual) => !atual)}
|
|
aria-label={mostrarSenha ? "Ocultar senha" : "Mostrar senha"}
|
|
>
|
|
{mostrarSenha ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<Button
|
|
type="submit"
|
|
className="h-12 w-full rounded-xl text-base font-semibold"
|
|
disabled={carregando}
|
|
>
|
|
{carregando ? (
|
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
) : modo === "entrar" ? (
|
|
<LogIn className="h-5 w-5" />
|
|
) : modo === "criar" ? (
|
|
<UserPlus className="h-5 w-5" />
|
|
) : (
|
|
<KeyRound className="h-5 w-5" />
|
|
)}
|
|
{modo === "entrar"
|
|
? "Entrar"
|
|
: modo === "criar"
|
|
? "Criar conta"
|
|
: "Recuperar senha"}
|
|
</Button>
|
|
</form>
|
|
|
|
<div className="space-y-2 text-center text-sm">
|
|
{modo !== "entrar" ? (
|
|
<button
|
|
type="button"
|
|
className="w-full text-muted-foreground underline-offset-4 hover:underline"
|
|
onClick={() => {
|
|
setModo("entrar");
|
|
setConfirmarSenha("");
|
|
}}
|
|
>
|
|
Já tenho conta — entrar
|
|
</button>
|
|
) : null}
|
|
{modo !== "criar" ? (
|
|
<button
|
|
type="button"
|
|
className="w-full text-muted-foreground underline-offset-4 hover:underline"
|
|
onClick={() => {
|
|
setModo("criar");
|
|
setConfirmarSenha("");
|
|
}}
|
|
>
|
|
Não tenho conta — criar agora
|
|
</button>
|
|
) : null}
|
|
{modo !== "recuperar" ? (
|
|
<button
|
|
type="button"
|
|
className="w-full text-muted-foreground underline-offset-4 hover:underline"
|
|
onClick={() => {
|
|
setModo("recuperar");
|
|
setConfirmarSenha("");
|
|
}}
|
|
>
|
|
Esqueci minha senha
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|