EventBuzz_Website/src/pages/RegisterOtpPage.tsx

238 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useMemo, useState } from "react";
import { Calendar, MapPin } from "lucide-react";
import { useLocation, useNavigate } from "react-router-dom";
import { AuthShell } from "../components/auth/AuthShell";
import { EventBuzzLogo } from "../components/EventBuzzLogo";
import { postVisitorStartOtp, postVisitorVerifyOtp } from "../lib/api";
import { setAttendeeSession } from "../lib/attendeeAuth";
import { notifyRewardFromApiData } from "../lib/rewardCelebration";
import { buildPostAuthUrl, readStoredVisitorContext } from "../types/visitorContext";
type OtpLocationState = {
eventId?: string;
email?: string;
inviteToken?: string;
firstName?: string;
lastName?: string;
phone?: string;
displayName?: string;
};
function formatEventSchedule(startIso: string, endIso: string): string {
const s = new Date(startIso);
const e = new Date(endIso);
if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())) return "";
const opts: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" };
if (s.toDateString() === e.toDateString()) {
return s.toLocaleDateString(undefined, opts);
}
return `${s.toLocaleDateString(undefined, opts)} – ${e.toLocaleDateString(undefined, opts)}`;
}
export function RegisterOtpPage() {
const navigate = useNavigate();
const location = useLocation();
const st = (location.state as OtpLocationState | null) ?? {};
const eventId = st.eventId ?? "";
const email = st.email ?? "";
const ctx = readStoredVisitorContext();
const event = ctx?.event;
const [digits, setDigits] = useState<string[]>(() => Array(6).fill(""));
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [resendCooldown, setResendCooldown] = useState(0);
useEffect(() => {
if (resendCooldown <= 0) return;
const t = window.setTimeout(() => setResendCooldown((c) => c - 1), 1000);
return () => window.clearTimeout(t);
}, [resendCooldown]);
const schedule = useMemo(() => {
if (!event?.startDate || !event?.endDate) return "";
const a =
typeof event.startDate === "string"
? event.startDate
: new Date(event.startDate as unknown as Date).toISOString();
const b =
typeof event.endDate === "string"
? event.endDate
: new Date(event.endDate as unknown as Date).toISOString();
return formatEventSchedule(a, b);
}, [event]);
if (!eventId || !email) {
return (
<AuthShell>
<div className="px-4 text-center text-sm">
<p className="text-[#24282f]">Missing session. Start from the login page.</p>
<button type="button" className="mt-4 text-[#1e72ef] underline" onClick={() => navigate("/login")}>
Back to login
</button>
</div>
</AuthShell>
);
}
function setDigit(i: number, v: string) {
const d = v.replace(/\D/g, "").slice(-1);
const next = [...digits];
next[i] = d;
setDigits(next);
if (d && i < 5) {
document.getElementById(`otp-${i + 1}`)?.focus();
}
}
async function handleVerify(e: React.FormEvent) {
e.preventDefault();
const otp = digits.join("");
if (otp.length !== 6) {
setError("Enter the 6-digit code.");
return;
}
setError(null);
setLoading(true);
try {
const data = await postVisitorVerifyOtp(eventId, {
email,
otp,
inviteToken: st.inviteToken,
firstName: st.firstName,
lastName: st.lastName,
phone: st.phone,
displayName: st.displayName,
});
setAttendeeSession(data.token, data.participant);
notifyRewardFromApiData(data);
if (data.pendingApproval) {
navigate("/pending-approval");
return;
}
if (data.needsProfile) {
navigate("/complete-profile");
return;
}
navigate(buildPostAuthUrl(ctx));
} catch (err) {
setError(err instanceof Error ? err.message : "Verification failed.");
} finally {
setLoading(false);
}
}
async function handleResend() {
if (resendCooldown > 0 || !ctx) return;
setError(null);
try {
await postVisitorStartOtp(
eventId,
email,
{ type: ctx.type, sessionId: ctx.sessionId, trackId: ctx.trackId },
st.inviteToken,
);
setResendCooldown(45);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not resend.");
}
}
return (
<AuthShell>
<div className="flex w-full max-w-[354px] flex-col items-center">
<div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]">
<div className="rounded-xl bg-white">
<div className="px-6 pb-2 pt-6">
<div className="flex h-[103px] items-center justify-center rounded-xl border border-[#f2f2f7] px-4">
{event?.logoUrl ? (
<img
src={event.logoUrl}
alt=""
className="max-h-[72px] w-full max-w-[240px] object-contain"
/>
) : (
<EventBuzzLogo variant="color" className="max-h-[72px] w-auto object-center" />
)}
</div>
</div>
<form onSubmit={handleVerify} className="flex flex-col gap-8 px-6 pb-8 pt-2">
<div className="flex flex-col items-center gap-4 text-center">
<p className="font-inter text-base font-semibold text-[#24282f]">Enter code</p>
<div className="min-w-0 space-y-1">
<p className="font-inter text-xl font-bold text-[#24282f]">{event?.name ?? "Event"}</p>
</div>
{schedule ? (
<div className="flex flex-wrap items-center justify-center gap-2 text-xs text-[#24282f]">
<Calendar className="size-4 shrink-0" strokeWidth={1.75} />
<span className="font-inter">{schedule}</span>
{event?.location?.trim() ? (
<>
<span className="hidden h-3 w-px bg-[rgba(36,40,47,0.5)] sm:inline" aria-hidden />
<MapPin className="size-4 shrink-0" strokeWidth={1.75} />
<span className="font-inter">{event.location.trim()}</span>
</>
) : null}
</div>
) : null}
</div>
<div className="flex flex-col gap-4 text-center">
<div className="rounded-lg border border-dashed border-[#34c759] bg-[#ebffee] px-2 py-2 text-base leading-5 text-[#009951]">
Code sent to your email
</div>
<div className="text-xs text-[rgba(36,40,47,0.87)]">
<p className="mb-1">Sent to:</p>
<p className="font-semibold text-[#24282f]">{email}</p>
</div>
</div>
<div className="flex flex-col gap-4">
<p className="font-inter text-base font-semibold text-[#24282f]">Enter OTP</p>
<div className="flex justify-center gap-2">
{digits.map((d, i) => (
<input
key={i}
id={`otp-${i}`}
inputMode="numeric"
maxLength={1}
value={d}
onChange={(e) => setDigit(i, e.target.value)}
className="size-11 rounded-lg border border-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] text-center font-inter text-lg font-semibold text-[#24282f]"
/>
))}
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<button
type="submit"
disabled={loading}
className="w-full rounded-full border border-[rgba(214,200,200,0.6)] bg-[#ef8d1d] py-3 text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)] disabled:opacity-60"
>
{loading ? "Verifying…" : "Verify OTP"}
</button>
<div className="flex items-center justify-center gap-4 text-sm">
<button
type="button"
onClick={() => navigate("/login")}
className="font-inter text-[#1e72ef] underline"
>
Change email
</button>
<button
type="button"
disabled={resendCooldown > 0}
onClick={() => void handleResend()}
className="font-inter font-semibold text-[#1e72ef] underline disabled:opacity-40"
>
{resendCooldown > 0 ? `Resend (${resendCooldown}s)` : "Resend code"}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</AuthShell>
);
}