This commit is contained in:
Emmanuel Rojas committed 2026-09-30 14:45:10 -05:00
1 parent 90125aede9
commit 2d6c29a1f3
404 files changed
+126983 -85

No files matched your search

@@ -0,0 +1,111 @@
"use client";
import { Icon } from "@iconify/react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { iconStorefrontFill } from "../../_core/icons";
import { fetchUsers, login } from "../_services/auth.service";
import type { AuthUser } from "../../_core/types/auth-user";
import { PasswordForm } from "./password-form";
import { UserPicker } from "./user-picker";
export function UserLoginScreen() {
const router = useRouter();
const reduceMotion = useReducedMotion();
const [users, setUsers] = useState<AuthUser[] | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [selected, setSelected] = useState<AuthUser | null>(null);
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
fetchUsers()
.then(setUsers)
.catch(() => setLoadError("No se pudo cargar la lista de usuarios."));
}, []);
function selectUser(user: AuthUser) {
setSelected(user);
setError(null);
}
function backToPicker() {
setSelected(null);
setError(null);
}
async function handleSubmit(password: string) {
if (!selected) return;
setSubmitting(true);
setError(null);
try {
const user = await login(selected.id, password);
sessionStorage.setItem("pdv:user", JSON.stringify(user));
router.push(user.role === "admin" ? "/admin" : "/pos");
} catch {
setError("Contraseña incorrecta. Intenta de nuevo.");
} finally {
setSubmitting(false);
}
}
const step = selected ? "password" : "picker";
const slide = reduceMotion ? 0 : 16;
return (
<div className="flex min-h-dvh w-full items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground">
<div className="flex w-full max-w-3xl flex-col items-center gap-10">
<div className="flex flex-col items-center gap-2">
<div className="flex size-12 items-center justify-center rounded-2xl bg-accent-soft">
<Icon icon={iconStorefrontFill} width={24} height={24} className="text-accent-soft-foreground" aria-hidden="true" />
</div>
<AnimatePresence mode="wait" initial={false}>
<motion.h1
key={step}
initial={{ opacity: 0, y: reduceMotion ? 0 : -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: reduceMotion ? 0 : -6 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="text-2xl font-light tracking-wide text-foreground/80"
>
{step === "password" ? "Ingresa tu contraseña" : "¿Quién va a usar el PDV?"}
</motion.h1>
</AnimatePresence>
</div>
<AnimatePresence mode="wait" initial={false}>
{step === "picker" ? (
<motion.div
key="picker"
initial={{ opacity: 0, x: -slide }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -slide }}
transition={{ duration: 0.22, ease: "easeOut" }}
className="flex w-full justify-center"
>
<UserPicker users={users} loadError={loadError} onSelect={selectUser} />
</motion.div>
) : (
<motion.div
key="password"
initial={{ opacity: 0, x: slide }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: slide }}
transition={{ duration: 0.22, ease: "easeOut" }}
className="flex w-full justify-center"
>
<PasswordForm
user={selected as AuthUser}
error={error}
submitting={submitting}
onSubmit={handleSubmit}
onBack={backToPicker}
/>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}