Web InnoventixFreeCode

Error Expired

Original · free

link/session expired page

byWeb InnoventixReact + Tailwind
errorexpiredpages
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/error-expired.json
error-expired.tsx
"use client";

import {
  useEffect,
  useRef,
  useState,
  type ChangeEvent,
  type FormEvent,
} from "react";
import { motion, useReducedMotion } from "motion/react";

type SendState = "idle" | "sending" | "sent";

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValidEmail = (value: string): boolean => EMAIL_RE.test(value.trim());

const RESEND_COOLDOWN = 45;

export default function ErrorExpired() {
  const reduce = useReducedMotion();

  const [email, setEmail] = useState<string>("");
  const [error, setError] = useState<string | null>(null);
  const [status, setStatus] = useState<SendState>("idle");
  const [sentTo, setSentTo] = useState<string>("");
  const [cooldown, setCooldown] = useState<number>(0);
  const [showReasons, setShowReasons] = useState<boolean>(false);

  const sendTimerRef = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (sendTimerRef.current !== null) window.clearTimeout(sendTimerRef.current);
    };
  }, []);

  // Single interval that runs only while a cooldown is active.
  useEffect(() => {
    if (cooldown <= 0) return;
    const id = window.setInterval(() => {
      setCooldown((current) => {
        if (current <= 1) {
          window.clearInterval(id);
          return 0;
        }
        return current - 1;
      });
    }, 1000);
    return () => window.clearInterval(id);
    // Re-run only when crossing the active/inactive boundary.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cooldown === 0]);

  const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
    setEmail(event.target.value);
    if (error) setError(null);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
    event.preventDefault();
    if (status === "sending" || cooldown > 0) return;
    if (!isValidEmail(email)) {
      setError("Enter a valid email address so we can reach you.");
      return;
    }
    setError(null);
    setStatus("sending");
    sendTimerRef.current = window.setTimeout(() => {
      setSentTo(email.trim());
      setStatus("sent");
      setCooldown(RESEND_COOLDOWN);
    }, 1150);
  };

  const busy = status === "sending";
  const buttonLabel =
    busy
      ? "Sending secure link…"
      : status === "sent"
        ? cooldown > 0
          ? `Resend available in ${cooldown}s`
          : "Resend the link"
        : "Email me a new link";
  const buttonDisabled = busy || cooldown > 0;

  const grains = [
    { cx: 92, cy: 148, delay: "0s" },
    { cx: 104, cy: 152, delay: "0.5s" },
    { cx: 100, cy: 143, delay: "1s" },
  ];

  const focusRing =
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-20 lg:px-8 lg:py-24 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes expr-spin { to { transform: rotate(360deg); } }
        @keyframes expr-pulse {
          0%, 100% { opacity: .30; transform: scale(1); }
          50% { opacity: .55; transform: scale(1.06); }
        }
        @keyframes expr-float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-10px); }
        }
        @keyframes expr-grain {
          0%, 100% { transform: translateY(0); opacity: .55; }
          50% { transform: translateY(2.5px); opacity: .95; }
        }
        @keyframes expr-sweep {
          0% { transform: translateX(-130%) skewX(-16deg); }
          100% { transform: translateX(230%) skewX(-16deg); }
        }
        .expr-spin { animation: expr-spin 22s linear infinite; }
        .expr-pulse { animation: expr-pulse 4s ease-in-out infinite; }
        .expr-float { animation: expr-float 6s ease-in-out infinite; }
        .expr-grain { animation: expr-grain 2.6s ease-in-out infinite; }
        .expr-sweep { animation: expr-sweep 6s linear infinite; }
        @media (prefers-reduced-motion: reduce) {
          .expr-spin, .expr-pulse, .expr-float, .expr-grain, .expr-sweep {
            animation: none !important;
          }
        }
      `}</style>

      {/* Ambient background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="absolute -left-24 top-8 h-72 w-72 rounded-full bg-amber-300/25 blur-3xl dark:bg-amber-500/10" />
        <div className="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-500/10" />
        <div
          className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
          style={{
            backgroundImage:
              "linear-gradient(to right, rgba(100,116,139,0.10) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,0.10) 1px, transparent 1px)",
            backgroundSize: "56px 56px",
            maskImage: "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent)",
            WebkitMaskImage: "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent)",
          }}
        />
      </div>

      <div className="relative mx-auto grid w-full max-w-5xl items-center gap-10 lg:grid-cols-[0.85fr_1.15fr] lg:gap-16">
        {/* Visual column */}
        <motion.div
          initial={reduce ? false : { opacity: 0, scale: 0.94 }}
          animate={reduce ? {} : { opacity: 1, scale: 1 }}
          transition={{ duration: 0.6, ease: "easeOut" }}
          className="relative mx-auto flex w-full max-w-xs flex-col items-center lg:max-w-none"
        >
          <div className="relative aspect-square w-64 sm:w-72">
            {/* Pulsing halo */}
            <div className="expr-pulse absolute inset-0 rounded-full bg-gradient-to-br from-amber-400/40 to-rose-400/30 blur-2xl dark:from-amber-500/25 dark:to-rose-500/20" />

            <div className="expr-float absolute inset-0">
              <svg
                viewBox="0 0 200 200"
                className="h-full w-full drop-shadow-xl"
                role="img"
                aria-label="An hourglass with all of the sand run out to the bottom"
              >
                <defs>
                  <linearGradient id="expr-sand" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0%" stopColor="#fcd34d" />
                    <stop offset="55%" stopColor="#f59e0b" />
                    <stop offset="100%" stopColor="#d97706" />
                  </linearGradient>
                  <linearGradient id="expr-glass" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0%" stopColor="#e2e8f0" stopOpacity="0.55" />
                    <stop offset="100%" stopColor="#94a3b8" stopOpacity="0.18" />
                  </linearGradient>
                </defs>

                {/* Rotating dashed time ring */}
                <circle
                  className="expr-spin"
                  cx="100"
                  cy="100"
                  r="90"
                  fill="none"
                  stroke="#f59e0b"
                  strokeWidth="1.5"
                  strokeDasharray="3 11"
                  strokeLinecap="round"
                  opacity="0.7"
                  style={{ transformOrigin: "100px 100px", animationDuration: "22s" }}
                />

                {/* Frame caps */}
                <rect x="42" y="34" width="116" height="11" rx="5.5" fill="#64748b" />
                <rect x="42" y="155" width="116" height="11" rx="5.5" fill="#64748b" />

                {/* Upper chamber (empty) */}
                <path
                  d="M52 45 L148 45 L100 100 Z"
                  fill="url(#expr-glass)"
                  stroke="#94a3b8"
                  strokeWidth="2.5"
                  strokeLinejoin="round"
                />
                {/* Lower chamber (glass) */}
                <path
                  d="M100 100 L148 155 L52 155 Z"
                  fill="url(#expr-glass)"
                  stroke="#94a3b8"
                  strokeWidth="2.5"
                  strokeLinejoin="round"
                />

                {/* Settled sand in the lower chamber */}
                <path
                  d="M56 153 L144 153 L118 121 Q100 110 82 121 Z"
                  fill="url(#expr-sand)"
                />
                <path
                  d="M56 153 L144 153 L118 121 Q100 110 82 121 Z"
                  fill="none"
                  stroke="#b45309"
                  strokeWidth="1.4"
                  strokeOpacity="0.35"
                  strokeLinejoin="round"
                />

                {/* A few loose grains near the neck */}
                {grains.map((g, i) => (
                  <circle
                    key={i}
                    className="expr-grain"
                    cx={g.cx}
                    cy={g.cy}
                    r="2.1"
                    fill="#fbbf24"
                    style={{ animationDelay: g.delay, animationDuration: "2.6s" }}
                  />
                ))}

                {/* Neck highlight */}
                <circle cx="100" cy="100" r="3.4" fill="#cbd5e1" />
              </svg>
            </div>
          </div>

          <div className="mt-6 flex flex-col items-center gap-2 text-center">
            <span className="inline-flex items-center gap-2 rounded-full border border-amber-300/70 bg-amber-100/70 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300">
              <span className="relative flex h-2 w-2">
                <span className="expr-pulse absolute inline-flex h-full w-full rounded-full bg-amber-500" />
                <span className="relative inline-flex h-2 w-2 rounded-full bg-amber-500" />
              </span>
              Link expired
            </span>
            <p className="font-mono text-xs text-slate-500 dark:text-slate-400">
              Reference EXP-7F3A · lapsed 18 minutes ago
            </p>
          </div>
        </motion.div>

        {/* Content card */}
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 20 }}
          animate={reduce ? {} : { opacity: 1, y: 0 }}
          transition={{ duration: 0.55, ease: "easeOut", delay: reduce ? 0 : 0.12 }}
          className="relative overflow-hidden rounded-3xl border border-slate-200 bg-white/90 p-6 shadow-2xl shadow-slate-900/5 backdrop-blur-sm sm:p-9 dark:border-slate-800 dark:bg-slate-900/80 dark:shadow-black/40"
        >
          {/* Top sheen sweep */}
          <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
            <div className="expr-sweep absolute -top-1 left-0 h-px w-1/2 bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent" />
          </div>

          <p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
            Session security
          </p>
          <h1 className="mt-3 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
            Your sign-in link has expired
          </h1>
          <p className="mt-4 max-w-prose text-base leading-relaxed text-slate-600 dark:text-slate-300">
            For your protection, magic links stay valid for just{" "}
            <span className="font-semibold text-slate-800 dark:text-slate-100">15 minutes</span>.
            This one was opened after it lapsed, so it can no longer verify your account. Request a
            fresh link below and you&rsquo;ll be back in seconds.
          </p>

          <form onSubmit={handleSubmit} noValidate className="mt-7">
            <label
              htmlFor="expr-email"
              className="block text-sm font-medium text-slate-700 dark:text-slate-200"
            >
              Where should we send the new link?
            </label>
            <div className="mt-2 flex flex-col gap-3 sm:flex-row">
              <div className="relative flex-1">
                <span aria-hidden="true" className="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500">
                  <svg viewBox="0 0 24 24" fill="none" className="h-5 w-5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
                    <rect x="3" y="5" width="18" height="14" rx="2.5" />
                    <path d="m3.5 7 8.5 6 8.5-6" />
                  </svg>
                </span>
                <input
                  id="expr-email"
                  type="email"
                  inputMode="email"
                  autoComplete="email"
                  placeholder="you@company.com"
                  value={email}
                  onChange={handleChange}
                  aria-invalid={error ? true : undefined}
                  aria-describedby={error ? "expr-email-error" : "expr-email-hint"}
                  className={`w-full rounded-xl border bg-white py-3 pl-11 pr-3 text-sm text-slate-900 placeholder:text-slate-400 transition-colors dark:bg-slate-950/60 dark:text-slate-100 dark:placeholder:text-slate-500 ${focusRing} ${
                    error
                      ? "border-rose-400 dark:border-rose-500/70"
                      : "border-slate-300 hover:border-slate-400 dark:border-slate-700 dark:hover:border-slate-600"
                  }`}
                />
              </div>
              <button
                type="submit"
                disabled={buttonDisabled}
                className={`inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition-colors hover:bg-indigo-500 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500 disabled:shadow-none dark:disabled:bg-slate-700 dark:disabled:text-slate-400 ${focusRing}`}
              >
                {busy && (
                  <svg viewBox="0 0 24 24" className="expr-spin h-4 w-4" fill="none" style={{ animationDuration: "0.8s" }} aria-hidden="true">
                    <circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" opacity="0.3" />
                    <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
                  </svg>
                )}
                {buttonLabel}
              </button>
            </div>

            {error ? (
              <p
                id="expr-email-error"
                role="alert"
                className="mt-2 flex items-center gap-1.5 text-sm font-medium text-rose-600 dark:text-rose-400"
              >
                <svg viewBox="0 0 20 20" className="h-4 w-4 shrink-0" fill="currentColor" aria-hidden="true">
                  <path fillRule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm1-11a1 1 0 1 0-2 0v4a1 1 0 1 0 2 0V7Zm-1 7.5a1.1 1.1 0 1 0 0-2.2 1.1 1.1 0 0 0 0 2.2Z" clipRule="evenodd" />
                </svg>
                {error}
              </p>
            ) : (
              <p id="expr-email-hint" className="mt-2 text-xs text-slate-500 dark:text-slate-400">
                We&rsquo;ll never share your address. New links expire 15 minutes after they arrive.
              </p>
            )}

            {/* Live success region */}
            <div aria-live="polite" className="mt-4">
              {status === "sent" && (
                <div className="flex items-start gap-3 rounded-xl border border-emerald-300/70 bg-emerald-50 p-3.5 text-sm text-emerald-800 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300">
                  <svg viewBox="0 0 24 24" className="mt-0.5 h-5 w-5 shrink-0" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="M20 6 9 17l-5-5" />
                  </svg>
                  <span>
                    A new link is on its way to{" "}
                    <span className="font-semibold">{sentTo}</span>. It usually lands within a
                    minute — check spam if it hasn&rsquo;t arrived.
                  </span>
                </div>
              )}
            </div>
          </form>

          {/* Secondary actions */}
          <div className="mt-7 flex flex-wrap items-center gap-x-6 gap-y-3 border-t border-slate-200 pt-6 text-sm dark:border-slate-800">
            <button
              type="button"
              className={`inline-flex items-center gap-1.5 rounded-md font-semibold text-slate-700 transition-colors hover:text-indigo-600 dark:text-slate-200 dark:hover:text-indigo-400 ${focusRing}`}
            >
              <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <path d="M19 12H5" />
                <path d="m12 19-7-7 7-7" />
              </svg>
              Back to sign in
            </button>
            <a
              href="mailto:support@example.com"
              className={`inline-flex items-center gap-1.5 rounded-md font-medium text-slate-500 transition-colors hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 ${focusRing}`}
            >
              <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <path d="M12 17h.01" />
                <path d="M12 13.5a1.5 1.5 0 0 1 1-1.4 2.6 2.6 0 1 0-3.2-3.3" />
                <circle cx="12" cy="12" r="9" />
              </svg>
              Contact support
            </a>
          </div>

          {/* Why did this happen — disclosure */}
          <div className="mt-5">
            <button
              type="button"
              onClick={() => setShowReasons((prev) => !prev)}
              aria-expanded={showReasons}
              aria-controls="expr-reasons"
              className={`inline-flex items-center gap-1.5 rounded-md text-sm font-medium text-slate-500 transition-colors hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 ${focusRing}`}
            >
              <svg
                viewBox="0 0 24 24"
                className={`h-4 w-4 transition-transform duration-200 ${showReasons ? "rotate-90" : ""}`}
                fill="none"
                stroke="currentColor"
                strokeWidth="2"
                strokeLinecap="round"
                strokeLinejoin="round"
                aria-hidden="true"
              >
                <path d="m9 18 6-6-6-6" />
              </svg>
              Why did this happen?
            </button>
            {showReasons && (
              <ul
                id="expr-reasons"
                className="mt-3 space-y-2 border-l-2 border-slate-200 pl-4 text-sm leading-relaxed text-slate-600 dark:border-slate-700 dark:text-slate-300"
              >
                <li>The link sat unopened for longer than its 15-minute window.</li>
                <li>You already used it once — each link works a single time.</li>
                <li>A newer link was requested, which retires every earlier one.</li>
                <li>Your device clock is far out of sync with our servers.</li>
              </ul>
            )}
          </div>
        </motion.div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →