"use client"; import { useEffect, useRef, useState } from "react"; import { Icon, Avatar, GoogleMark, MicrosoftMark, AppleMark } from "./icons"; import { buildOAuthUrl, passwordStrength, type LegalDoc } from "./data"; /* ---- spinner ---- */ export function Spinner({ lg }: { lg?: boolean }) { return ; } /* ---- badge ---- */ export function Badge({ tone = "gray", children }: { tone?: "blue" | "green" | "gray" | "amber"; children: React.ReactNode }) { return {children}; } /* ---- back button ---- */ export function StepBack({ onClick, label = "Back" }: { onClick: () => void; label?: string }) { return ( ); } /* ---- account pill ---- */ export function AccountPill({ initials, email, onChange }: { initials: string; email: string; onChange: () => void }) { return (
{email}
); } /* ---- flash note ---- */ export function FlashNote({ tone = "info", children }: { tone?: "info" | "warn" | "error" | "success"; children: React.ReactNode }) { const icon = tone === "error" || tone === "warn" ? "alert" : tone === "success" ? "check" : "info"; return (
{children}
); } /* ---- method row ---- */ export function MethodRow({ icon, title, sub, badge, onClick, }: { icon: string; title: string; sub?: string; badge?: string; onClick: () => void }) { return ( ); } /* ---- remember device ---- */ export function RememberDevice({ checked, onChange, note }: { checked: boolean; onChange: (v: boolean) => void; note?: string }) { return ( ); } /* ---- OTP boxes ---- */ export function OtpBoxes({ length = 6, onComplete, error, onChange, }: { length?: number; onComplete?: (code: string) => void; error?: boolean; onChange?: (code: string) => void }) { const [vals, setVals] = useState(Array(length).fill("")); const refs = useRef>([]); function emit(next: string[]) { setVals(next); const code = next.join(""); onChange?.(code); if (code.length === length && !next.includes("")) onComplete?.(code); } function setAt(i: number, v: string) { const d = v.replace(/\D/g, "").slice(-1); const next = [...vals]; next[i] = d; emit(next); if (d && i < length - 1) refs.current[i + 1]?.focus(); } function onPaste(e: React.ClipboardEvent) { const text = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length); if (!text) return; e.preventDefault(); const next = Array(length).fill(""); text.split("").forEach((c, i) => (next[i] = c)); emit(next); refs.current[Math.min(text.length, length - 1)]?.focus(); } return (
{vals.map((d, i) => ( { refs.current[i] = el; }} className="otp-box" inputMode="numeric" autoComplete="one-time-code" maxLength={1} value={d} autoFocus={i === 0} aria-label={`Digit ${i + 1}`} onChange={(e) => setAt(i, e.target.value)} onKeyDown={(e) => { if (e.key === "Backspace" && !vals[i] && i > 0) refs.current[i - 1]?.focus(); }} /> ))}
); } /* ---- resend link with cooldown ---- */ export function ResendLink({ seconds = 30, onResend }: { seconds?: number; onResend?: () => void }) { const [left, setLeft] = useState(seconds); useEffect(() => { if (left <= 0) return; const t = setTimeout(() => setLeft((l) => l - 1), 1000); return () => clearTimeout(t); }, [left]); if (left > 0) return Resend code in {left}s; return ( ); } /* ---- social buttons (real OAuth or demo mode) ---- */ /** Returns true if it redirected to a real provider; false if demo mode. */ export function startSocial(provider: "google" | "microsoft" | "apple"): boolean { const url = buildOAuthUrl(provider); if (url) { window.location.href = url; return true; } return false; } export function SocialButtons({ onPick, verb = "Continue" }: { onPick: (p: "google") => void; verb?: string }) { // Only Google is offered (Microsoft/Apple intentionally hidden). return (
); } /* ---- password strength meter ---- */ export function PasswordStrength({ pw }: { pw: string }) { const s = passwordStrength(pw); if (!pw) return null; return (
{[0, 1, 2, 3].map((i) => ( ))}
Password strength: {s.label}
    {s.checks.map((c) => (
  • {c.label}
  • ))}
); } /* ---- legal modal (scroll-to-end to accept) ---- */ export function LegalModal({ doc, onClose, onReviewed }: { doc: LegalDoc; onClose: () => void; onReviewed: () => void }) { const [reviewed, setReviewed] = useState(false); const bodyRef = useRef(null); // If the content fits without scrolling, there's nothing to scroll — mark reviewed. useEffect(() => { const el = bodyRef.current; if (el && el.scrollHeight <= el.clientHeight + 12) setReviewed(true); }, []); function onScroll(e: React.UIEvent) { const el = e.currentTarget; if (el.scrollTop + el.clientHeight >= el.scrollHeight - 10 && !reviewed) setReviewed(true); } return (

{doc.title}

{doc.updated}
{doc.sections.map((s) => (

{s.h}

{s.p}

))}
{reviewed ? "Reviewed — you can now accept" : "Scroll to the end to mark as reviewed"}
); } /* ---- cookie banner ---- */ export function CookieBanner() { const [show, setShow] = useState(false); useEffect(() => { try { if (!localStorage.getItem("lup_cookie")) setShow(true); } catch { setShow(true); } }, []); function choose(v: string) { try { localStorage.setItem("lup_cookie", v); } catch { /* ignore */ } setShow(false); } if (!show) return null; return (

We value your privacy

We use essential cookies to sign you in securely, and optional cookies for analytics. See our{" "} .

); }