SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
3.1 KB · 67 lines tsx
Raw Blame History
1"use client";23import { useState } from "react";4import { Copy, Check, KeyRound, TriangleAlert } from "lucide-react";5import { Button } from "@/components/ui";6import { cn } from "@/lib/utils";78const NOTICE = "Save this recovery code. Spinza does not collect your email address. If you lose your password and recovery code, your account cannot be recovered.";910/** Prominent recovery-code display with copy button and a save-confirmation gate. */11export function RecoveryCodePanel({ code, title = "Your recovery code", notice = NOTICE, continueLabel = "Continue", onContinue, loading, className, hideHeader }: { code: string; title?: string; notice?: string; continueLabel?: string; onContinue: () => void; loading?: boolean; className?: string; hideHeader?: boolean }) {12  const [copied, setCopied] = useState(false);13  const [saved, setSaved] = useState(false);1415  const copy = async () => {16    try {17      await navigator.clipboard.writeText(code);18      setCopied(true);19      setTimeout(() => setCopied(false), 1800);20    } catch {21      // Clipboard unavailable (insecure context): user can still select the code.22    }23  };2425  return (26    <div className={cn("space-y-5", className)}>27      {hideHeader ? null : (28        <div className="flex items-center gap-3">29          <span className="grid h-11 w-11 shrink-0 place-items-center rounded-md bg-accent text-bg">30            <KeyRound className="h-5 w-5" />31          </span>32          <div>33            <h1 className="text-xl font-semibold tracking-tight">{title}</h1>34            <p className="text-[13px] text-fg-3">This code is the only way to reset your password.</p>35          </div>36        </div>37      )}3839      <div className="metal rounded-lg p-4">40        <div className="eyebrow mb-2">Recovery code</div>41        <div className="flex items-center justify-between gap-3">42          <code className="min-w-0 select-all whitespace-nowrap font-mono text-[17px] font-semibold tracking-[0.03em] text-accent-2 sm:text-2xl sm:tracking-[0.06em]" aria-label={`Recovery code ${code}`}>43            {code}44          </code>45          <Button type="button" variant="secondary" size="icon" onClick={copy} aria-label="Copy recovery code" className="shrink-0">46            {copied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}47          </Button>48        </div>49      </div>5051      <div className="flex gap-3 rounded-md border border-accent/30 bg-accent-soft p-4 text-[13px] leading-relaxed text-fg-2" role="note">52        <TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-accent" />53        <p>{notice}</p>54      </div>5556      <label className="flex cursor-pointer items-start gap-3 rounded-md border border-line p-3.5 tap hover:bg-surface">57        <input type="checkbox" checked={saved} onChange={(e) => setSaved(e.target.checked)} className="mt-0.5 h-5 w-5 shrink-0 accent-[#c9a961]" />58        <span className="text-[15px] font-medium">I saved my recovery code</span>59      </label>6061      <Button size="lg" variant="accent" className="w-full" disabled={!saved} loading={loading} onClick={onContinue}>62        {continueLabel}63      </Button>64    </div>65  );66}67