"use client"; import { useId, useLayoutEffect, useRef, useState, type CSSProperties, type FormEvent } from "react"; import "./strength-ring-sign-up.css"; /** * Strength Ring Sign-up * The password field's border is the strength meter. As the password gets * better, a gradient line draws itself round the field from the top-left * corner, gaining colour as it goes; when every rule is met it closes the * loop and floods the field's edge with light for a moment. The rules below * tick off one by one, each with a check that draws itself. */ type Props = { product?: string; /** Called with the new account. Resolve to finish; reject with a message to show it. */ onCreate?: (a: { name: string; email: string; password: string }) => Promise; signInHref?: string; theme?: "paper" | "night"; motion?: "full" | "reduced"; className?: string; }; const R = 14; // field corner radius /** A rounded-rect outline starting just after the top-left corner, clockwise. */ function outline(w: number, h: number) { const i = 0.75, r = R - i, x0 = i, y0 = i, x1 = w - i, y1 = h - i; return `M ${x0 + r} ${y0} H ${x1 - r} A ${r} ${r} 0 0 1 ${x1} ${y0 + r} V ${y1 - r} A ${r} ${r} 0 0 1 ${x1 - r} ${y1} H ${x0 + r} A ${r} ${r} 0 0 1 ${x0} ${y1 - r} V ${y0 + r} A ${r} ${r} 0 0 1 ${x0 + r} ${y0}`; } const LEVELS = ["Too short", "Weak", "Fair", "Good", "Strong"] as const; function assess(pw: string, name: string, email: string) { const local = email.split("@")[0]?.toLowerCase() ?? ""; const first = name.trim().split(/\s+/)[0]?.toLowerCase() ?? ""; const lower = pw.toLowerCase(); const rules = [ { id: "len", text: "At least 12 characters", ok: pw.length >= 12 }, { id: "num", text: "A number", ok: /\d/.test(pw) }, { id: "mix", text: "Upper and lower case, or a symbol", ok: (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) || /[^A-Za-z0-9]/.test(pw) }, { id: "own", text: "Not your name or email", ok: pw.length > 0 && !(local.length > 2 && lower.includes(local)) && !(first.length > 2 && lower.includes(first)) }, ]; const met = rules.filter((r) => r.ok).length; // Most of the ring is the rules; the rest is length, so it keeps moving as you type. const p = pw.length === 0 ? 0 : Math.min(1, (met / rules.length) * 0.78 + Math.min(pw.length, 16) / 16 * 0.22); const level = pw.length === 0 ? -1 : met === rules.length ? 4 : pw.length < 8 ? 0 : Math.max(1, met - (pw.length < 12 ? 1 : 0)); return { rules, p: met === rules.length ? 1 : Math.min(p, 0.94), level, strong: met === rules.length }; } const demoCreate = () => new Promise((ok) => setTimeout(ok, 1300)); export function StrengthRingSignUp({ product = "Vitrine", onCreate = demoCreate, signInHref = "#", theme = "paper", motion = "full", className = "" }: Props) { const uid = useId().replace(/:/g, ""); const field = useRef(null); const [box, setBox] = useState({ w: 0, h: 0 }); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [pw, setPw] = useState(""); const [show, setShow] = useState(false); const [phase, setPhase] = useState<"idle" | "busy" | "done">("idle"); const [error, setError] = useState<{ field: "name" | "email" | "pw" | "form"; text: string } | null>(null); useLayoutEffect(() => { const el = field.current; if (!el) return; const read = () => setBox({ w: el.offsetWidth, h: el.offsetHeight }); read(); const ro = new ResizeObserver(read); ro.observe(el); return () => ro.disconnect(); }, [phase]); const s = assess(pw, name, email); const submit = async (e: FormEvent) => { e.preventDefault(); if (!name.trim()) return setError({ field: "name", text: "Add your name so your team knows who joined." }); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setError({ field: "email", text: "That email doesn't look complete — check for a missing @ or domain." }); if (!s.strong) return setError({ field: "pw", text: "Almost — tick off the rules below first." }); setError(null); setPhase("busy"); try { await onCreate({ name: name.trim(), email, password: pw }); setPhase("done"); } catch (err) { setPhase("idle"); setError({ field: "form", text: err instanceof Error ? err.message : "Something went wrong. Try again in a moment." }); } }; if (phase === "done") { return (

Welcome to {product}, {name.trim().split(/\s+/)[0]}

We sent a link to {email}. Open it on any device to confirm the address.

); } const err = (f: "name" | "email" | "pw") => (error?.field === f ? `${uid}-err` : undefined); return (

Create your {product} account

Free for teams of up to five. No card needed.

{s.level >= 0 ? LEVELS[s.level] : ""}
{box.w > 0 && ( )}
    {s.rules.map((r) => (
  • {r.text} {r.ok ? ", done" : ", not yet"}
  • ))}
{error && }

Already have an account? Sign in

); }