156 lines
5.8 KiB
TypeScript
156 lines
5.8 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { Check, Clock3, Copy, Gift, X } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
CUPON_PENDIENTE_KEY,
|
|
PROMOCION_OCULTA_KEY,
|
|
PROMOCION_VISTA_KEY,
|
|
fechaLocalHoy,
|
|
obtenerPromocionActiva,
|
|
type PromocionActiva,
|
|
} from "@/lib/promocionesService";
|
|
|
|
function tiempoRestante(fin: string) {
|
|
const diferencia = Math.max(0, new Date(fin).getTime() - Date.now());
|
|
const horas = Math.floor(diferencia / 3_600_000);
|
|
const minutos = Math.floor((diferencia % 3_600_000) / 60_000);
|
|
const segundos = Math.floor((diferencia % 60_000) / 1000);
|
|
return { horas, minutos, segundos };
|
|
}
|
|
|
|
export function PromocionFlashModal() {
|
|
const [promocion, setPromocion] = useState<PromocionActiva | null>(null);
|
|
const [abierta, setAbierta] = useState(false);
|
|
const [noMostrar, setNoMostrar] = useState(false);
|
|
const [copiado, setCopiado] = useState(false);
|
|
const [ahora, setAhora] = useState(Date.now());
|
|
|
|
useEffect(() => {
|
|
let vigente = true;
|
|
const hoy = fechaLocalHoy();
|
|
if (
|
|
window.localStorage.getItem(PROMOCION_OCULTA_KEY) === hoy ||
|
|
window.sessionStorage.getItem(PROMOCION_VISTA_KEY) === hoy
|
|
)
|
|
return;
|
|
obtenerPromocionActiva().then((resultado) => {
|
|
if (!vigente) return;
|
|
setPromocion(resultado);
|
|
setAbierta(true);
|
|
});
|
|
return () => {
|
|
vigente = false;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!abierta) return;
|
|
const intervalo = window.setInterval(() => setAhora(Date.now()), 1000);
|
|
return () => window.clearInterval(intervalo);
|
|
}, [abierta]);
|
|
|
|
const restante = useMemo(
|
|
() => (promocion ? tiempoRestante(promocion.terminaEn) : { horas: 0, minutos: 0, segundos: 0 }),
|
|
[promocion, ahora],
|
|
);
|
|
|
|
const cerrar = () => {
|
|
if (noMostrar) window.localStorage.setItem(PROMOCION_OCULTA_KEY, fechaLocalHoy());
|
|
window.sessionStorage.setItem(PROMOCION_VISTA_KEY, fechaLocalHoy());
|
|
setAbierta(false);
|
|
};
|
|
|
|
const copiar = async () => {
|
|
if (!promocion) return;
|
|
try {
|
|
await navigator.clipboard.writeText(promocion.codigo);
|
|
} catch {
|
|
// El código sigue visible para copiarlo manualmente si el navegador bloquea el portapapeles.
|
|
}
|
|
setCopiado(true);
|
|
window.setTimeout(() => setCopiado(false), 1600);
|
|
};
|
|
|
|
const aplicar = () => {
|
|
if (!promocion) return;
|
|
window.sessionStorage.setItem(CUPON_PENDIENTE_KEY, promocion.codigo);
|
|
cerrar();
|
|
window.location.assign("/reserva");
|
|
};
|
|
|
|
if (!abierta || !promocion) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-ink/65 p-4 backdrop-blur-sm animate-in fade-in-0">
|
|
<section
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="titulo-promocion"
|
|
className="relative max-h-[90vh] w-[95vw] max-w-md overflow-y-auto rounded-3xl bg-card p-5 shadow-lift animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 sm:p-6"
|
|
>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={cerrar}
|
|
aria-label="Cerrar promoción"
|
|
className="absolute right-3 top-3 z-10 rounded-xl"
|
|
>
|
|
<X />
|
|
</Button>
|
|
|
|
<div className="grid size-12 place-items-center rounded-2xl bg-brand text-brand-foreground">
|
|
<Gift className="size-6" />
|
|
</div>
|
|
<p className="mt-5 text-xs font-bold uppercase tracking-wide text-secondary">Oferta flash</p>
|
|
<h2 id="titulo-promocion" className="mt-1 break-words pr-8 text-xl font-bold leading-tight sm:text-2xl">
|
|
{promocion.titulo}
|
|
</h2>
|
|
<p className="mt-2 break-words text-sm text-muted-foreground">{promocion.descripcion}</p>
|
|
|
|
<div className="mt-5 rounded-2xl bg-muted p-4">
|
|
<p className="flex items-center justify-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
<Clock3 className="size-4" /> La oferta termina en
|
|
</p>
|
|
<div className="mt-3 grid grid-cols-3 gap-2" aria-live="polite">
|
|
{[
|
|
[restante.horas, "Horas"],
|
|
[restante.minutos, "Min"],
|
|
[restante.segundos, "Seg"],
|
|
].map(([valor, etiqueta]) => (
|
|
<div key={etiqueta} className="rounded-2xl bg-card px-2 py-3 text-center shadow-soft">
|
|
<p className="text-xl font-bold tabular-nums">{String(valor).padStart(2, "0")}</p>
|
|
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">{etiqueta}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2 rounded-2xl border border-dashed border-primary/40 bg-accent/50 p-3">
|
|
<div className="min-w-0">
|
|
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">Tu código</p>
|
|
<p className="truncate font-mono text-lg font-bold text-primary">{promocion.codigo}</p>
|
|
</div>
|
|
<Button type="button" variant="outline" onClick={copiar} className="shrink-0 rounded-xl">
|
|
{copiado ? <Check /> : <Copy />}
|
|
{copiado ? "Copiado" : "Copiar código"}
|
|
</Button>
|
|
</div>
|
|
|
|
<Button type="button" size="lg" onClick={aplicar} className="mt-4 h-12 w-full rounded-2xl font-bold">
|
|
Aplicar cupón ahora
|
|
</Button>
|
|
|
|
<label className="mt-4 flex cursor-pointer items-center justify-center gap-2 text-xs text-muted-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={noMostrar}
|
|
onChange={(evento) => setNoMostrar(evento.target.checked)}
|
|
className="size-4 rounded border-border accent-primary"
|
|
/>
|
|
No volver a mostrar hoy
|
|
</label>
|
|
</section>
|
|
</div>
|
|
);
|
|
} |