Web InnoventixFreeCode

Error Maintenance

Original · free

scheduled maintenance page

byWeb InnoventixReact + Tailwind
errormaintenancepages
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-maintenance.json
error-maintenance.tsx
"use client";

import { useEffect, useId, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type TaskStatus = "done" | "active" | "pending";

interface Task {
  id: string;
  label: string;
  detail: string;
  status: TaskStatus;
}

const TASKS: Task[] = [
  {
    id: "db",
    label: "Database migration to PostgreSQL 16",
    detail: "Schema upgraded, 4.2M rows verified",
    status: "done",
  },
  {
    id: "search",
    label: "Rebuilding search & vector indexes",
    detail: "12 indexes reindexed and validated",
    status: "done",
  },
  {
    id: "cdn",
    label: "Warming CDN cache across 14 regions",
    detail: "9 of 14 regions live",
    status: "active",
  },
  {
    id: "smoke",
    label: "Final health checks & smoke tests",
    detail: "Queued — starts after cache warm-up",
    status: "pending",
  },
];

const STATUS_META: Record<
  TaskStatus,
  { word: string; ring: string; text: string; dot: string }
> = {
  done: {
    word: "Complete",
    ring: "ring-emerald-500/25 bg-emerald-50 dark:bg-emerald-500/10",
    text: "text-emerald-700 dark:text-emerald-300",
    dot: "bg-emerald-500",
  },
  active: {
    word: "In progress",
    ring: "ring-amber-500/25 bg-amber-50 dark:bg-amber-500/10",
    text: "text-amber-700 dark:text-amber-300",
    dot: "bg-amber-500",
  },
  pending: {
    word: "Pending",
    ring: "ring-slate-400/20 bg-slate-100 dark:bg-slate-800/60",
    text: "text-slate-500 dark:text-slate-400",
    dot: "bg-slate-400 dark:bg-slate-500",
  },
};

function Gear({
  size,
  teeth,
  spin,
  tone,
}: {
  size: number;
  teeth: number;
  spin: "em-gear-a" | "em-gear-b";
  tone: "indigo" | "violet";
}) {
  const arr = Array.from({ length: teeth }, (_, i) => i);
  const toothFill =
    tone === "indigo"
      ? "fill-indigo-500 dark:fill-indigo-400"
      : "fill-violet-500 dark:fill-violet-400";
  const bodyFill =
    tone === "indigo"
      ? "fill-indigo-400/90 dark:fill-indigo-500/80"
      : "fill-violet-400/90 dark:fill-violet-500/80";
  const holeFill = "fill-slate-50 dark:fill-slate-950";
  return (
    <svg
      viewBox="0 0 100 100"
      width={size}
      height={size}
      aria-hidden="true"
      className={spin}
      style={{ transformOrigin: "50% 50%" }}
    >
      {arr.map((i) => {
        const angle = (360 / teeth) * i;
        return (
          <rect
            key={i}
            x={44}
            y={3}
            width={12}
            height={17}
            rx={2.5}
            transform={`rotate(${angle} 50 50)`}
            className={toothFill}
          />
        );
      })}
      <circle cx={50} cy={50} r={34} className={bodyFill} />
      <circle cx={50} cy={50} r={13} className={holeFill} />
      <circle
        cx={50}
        cy={50}
        r={13}
        fill="none"
        strokeWidth={2}
        className="stroke-white/60 dark:stroke-white/10"
      />
    </svg>
  );
}

function StatusIcon({ status }: { status: TaskStatus }) {
  if (status === "done") {
    return (
      <svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
        <circle cx={12} cy={12} r={11} className="fill-emerald-500" />
        <path
          d="M7 12.5l3.2 3.2L17 8.8"
          fill="none"
          stroke="white"
          strokeWidth={2.2}
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
    );
  }
  if (status === "active") {
    return (
      <svg
        viewBox="0 0 24 24"
        className="em-spin-ring h-5 w-5"
        aria-hidden="true"
        style={{ transformOrigin: "50% 50%" }}
      >
        <circle
          cx={12}
          cy={12}
          r={9}
          fill="none"
          strokeWidth={2.4}
          className="stroke-amber-500/25"
        />
        <path
          d="M12 3a9 9 0 0 1 9 9"
          fill="none"
          strokeWidth={2.4}
          strokeLinecap="round"
          className="stroke-amber-500"
        />
      </svg>
    );
  }
  return (
    <svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
      <circle
        cx={12}
        cy={12}
        r={9}
        fill="none"
        strokeWidth={2.2}
        strokeDasharray="3 3.5"
        className="stroke-slate-400 dark:stroke-slate-500"
      />
    </svg>
  );
}

const fmt = (n: number): string => String(n).padStart(2, "0");

export default function ErrorMaintenance() {
  const reduce = useReducedMotion();
  const uid = useId();
  const headingId = `${uid}-heading`;
  const emailFieldId = `${uid}-email`;
  const emailErrId = `${uid}-email-err`;
  const emailOkId = `${uid}-email-ok`;

  const [remaining, setRemaining] = useState<number>(1 * 3600 + 34 * 60 + 12);
  const [progress, setProgress] = useState<number>(62);
  const [sinceCheck, setSinceCheck] = useState<number>(6);
  const [checking, setChecking] = useState<boolean>(false);

  const [email, setEmail] = useState<string>("");
  const [emailState, setEmailState] = useState<"idle" | "error" | "success">(
    "idle"
  );

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

  useEffect(() => {
    const tick = window.setInterval(() => {
      setRemaining((r) => (r > 0 ? r - 1 : 0));
      setSinceCheck((s) => s + 1);
    }, 1000);
    return () => window.clearInterval(tick);
  }, []);

  useEffect(() => {
    const creep = window.setInterval(() => {
      setProgress((p) => (p < 78 ? Math.min(78, p + 1) : p));
    }, 3200);
    return () => window.clearInterval(creep);
  }, []);

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

  const onRefresh = () => {
    if (checking) return;
    setChecking(true);
    checkTimer.current = window.setTimeout(() => {
      setChecking(false);
      setSinceCheck(0);
    }, 900);
  };

  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
    setEmailState(ok ? "success" : "error");
  };

  const hours = Math.floor(remaining / 3600);
  const mins = Math.floor((remaining % 3600) / 60);
  const secs = remaining % 60;
  const units: { v: number; label: string }[] = [
    { v: hours, label: "Hours" },
    { v: mins, label: "Minutes" },
    { v: secs, label: "Seconds" },
  ];

  const checkedLabel =
    sinceCheck < 5
      ? "just now"
      : sinceCheck < 60
        ? `${sinceCheck}s ago`
        : `${Math.floor(sinceCheck / 60)}m ago`;

  const appear = (delay: number) =>
    reduce
      ? {}
      : {
          initial: { opacity: 0, y: 18 },
          animate: { opacity: 1, y: 0 },
          transition: { duration: 0.5, delay, ease: "easeOut" as const },
        };

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

  return (
    <section
      aria-labelledby={headingId}
      className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 text-slate-900 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 dark:text-slate-100"
    >
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 opacity-70 dark:opacity-50"
        style={{
          backgroundImage:
            "radial-gradient(circle at 1px 1px, rgba(99,102,241,0.14) 1px, transparent 0)",
          backgroundSize: "26px 26px",
          maskImage:
            "radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%)",
          WebkitMaskImage:
            "radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%)",
        }}
      />
      <div
        aria-hidden="true"
        className="em-float pointer-events-none absolute left-1/2 top-[-6rem] h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-600/20"
      />

      <div className="relative mx-auto w-full max-w-3xl px-6 py-20 sm:py-28">
        {/* Status badge */}
        <motion.div {...appear(0)} className="flex justify-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-amber-500/30 bg-amber-50 px-4 py-1.5 text-sm font-medium text-amber-700 dark:border-amber-400/25 dark:bg-amber-500/10 dark:text-amber-300">
            <span className="relative flex h-2.5 w-2.5">
              <span className="em-ping absolute inline-flex h-full w-full rounded-full bg-amber-500" />
              <span className="em-dot relative inline-flex h-2.5 w-2.5 rounded-full bg-amber-500" />
            </span>
            Scheduled maintenance in progress
          </span>
        </motion.div>

        {/* Gears */}
        <motion.div
          {...appear(0.05)}
          className="relative mx-auto mt-10 flex h-40 w-56 items-center justify-center"
          aria-hidden="true"
        >
          <div className="absolute left-2 top-3">
            <Gear size={112} teeth={12} spin="em-gear-a" tone="indigo" />
          </div>
          <div className="absolute bottom-2 right-4">
            <Gear size={78} teeth={10} spin="em-gear-b" tone="violet" />
          </div>
        </motion.div>

        {/* Heading */}
        <motion.h1
          {...appear(0.1)}
          id={headingId}
          className="mt-4 text-balance text-center text-4xl font-semibold tracking-tight sm:text-5xl"
        >
          We&rsquo;ll be right back.
        </motion.h1>
        <motion.p
          {...appear(0.15)}
          className="mx-auto mt-4 max-w-xl text-pretty text-center text-base leading-relaxed text-slate-600 dark:text-slate-400 sm:text-lg"
        >
          We&rsquo;re rolling out infrastructure upgrades to make everything
          faster and more reliable. Your data is safe and no action is needed on
          your end.
        </motion.p>

        {/* Maintenance window */}
        <motion.div
          {...appear(0.2)}
          className="mt-6 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-slate-500 dark:text-slate-400"
        >
          <span className="inline-flex items-center gap-1.5">
            <svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
              <rect
                x={3}
                y={5}
                width={18}
                height={16}
                rx={2.5}
                fill="none"
                strokeWidth={1.8}
                className="stroke-slate-400 dark:stroke-slate-500"
              />
              <path
                d="M3 9h18M8 3v4M16 3v4"
                strokeWidth={1.8}
                strokeLinecap="round"
                className="stroke-slate-400 dark:stroke-slate-500"
              />
            </svg>
            Window: Sat, Jul 18 &middot; 02:00&ndash;04:30 UTC
          </span>
          <span className="inline-flex items-center gap-1.5">
            <span className="h-1.5 w-1.5 rounded-full bg-slate-400 dark:bg-slate-500" />
            Incident #MW-2048
          </span>
        </motion.div>

        {/* Card: countdown + progress + tasks */}
        <motion.div
          {...appear(0.28)}
          className="mt-10 rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/60 sm:p-8"
        >
          {/* Countdown */}
          <div className="text-center">
            <p className="text-xs font-semibold uppercase tracking-widest text-slate-500 dark:text-slate-400">
              Estimated time remaining
            </p>
            {remaining > 0 ? (
              <div
                role="timer"
                aria-label={`Estimated time remaining: ${hours} hours, ${mins} minutes, ${secs} seconds`}
                className="mt-4 flex items-stretch justify-center gap-2 sm:gap-3"
              >
                {units.map((u, i) => (
                  <div key={u.label} className="flex items-center gap-2 sm:gap-3">
                    <div className="min-w-[74px] rounded-2xl border border-slate-200 bg-slate-50 px-2 py-3 dark:border-slate-800 dark:bg-slate-950/50 sm:min-w-[88px]">
                      <div className="bg-gradient-to-b from-slate-900 to-slate-600 bg-clip-text text-4xl font-bold tabular-nums text-transparent dark:from-white dark:to-slate-400 sm:text-5xl">
                        {fmt(u.v)}
                      </div>
                      <div className="mt-1 text-[0.65rem] font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">
                        {u.label}
                      </div>
                    </div>
                    {i < units.length - 1 && (
                      <span
                        aria-hidden="true"
                        className="text-3xl font-bold text-slate-300 dark:text-slate-700"
                      >
                        :
                      </span>
                    )}
                  </div>
                ))}
              </div>
            ) : (
              <p className="mt-4 text-2xl font-semibold text-indigo-600 dark:text-indigo-400">
                Finalizing &mdash; almost there&hellip;
              </p>
            )}
          </div>

          {/* Progress */}
          <div className="mt-8">
            <div className="mb-2 flex items-center justify-between text-sm">
              <span className="font-medium text-slate-700 dark:text-slate-300">
                Overall progress
              </span>
              <span className="font-semibold tabular-nums text-indigo-600 dark:text-indigo-400">
                {progress}%
              </span>
            </div>
            <div
              role="progressbar"
              aria-valuenow={progress}
              aria-valuemin={0}
              aria-valuemax={100}
              aria-valuetext={`${progress} percent complete`}
              aria-label="Maintenance completion"
              className="h-3 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
            >
              <div
                className="relative h-full overflow-hidden rounded-full bg-gradient-to-r from-indigo-500 to-violet-500"
                style={{
                  width: `${progress}%`,
                  transition: reduce ? "none" : "width 0.8s ease-out",
                }}
              >
                <span className="em-shimmer absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/50 to-transparent" />
              </div>
            </div>
          </div>

          {/* Tasks */}
          <ul className="mt-8 space-y-2.5">
            {TASKS.map((task) => {
              const meta = STATUS_META[task.status];
              return (
                <li
                  key={task.id}
                  className="flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-3.5 py-3 dark:border-slate-800 dark:bg-slate-950/40"
                >
                  <span className="shrink-0">
                    <StatusIcon status={task.status} />
                  </span>
                  <span className="min-w-0 flex-1">
                    <span className="block truncate text-sm font-medium text-slate-800 dark:text-slate-100">
                      {task.label}
                    </span>
                    <span className="block truncate text-xs text-slate-500 dark:text-slate-400">
                      {task.detail}
                    </span>
                  </span>
                  <span
                    className={`shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${meta.ring} ${meta.text}`}
                  >
                    {meta.word}
                  </span>
                </li>
              );
            })}
          </ul>
        </motion.div>

        {/* Notify form */}
        <motion.div
          {...appear(0.36)}
          className="mx-auto mt-8 max-w-lg text-center"
        >
          <h2 className="text-base font-semibold text-slate-800 dark:text-slate-200">
            Get notified when we&rsquo;re back
          </h2>
          <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
            We&rsquo;ll send a single email the moment service is restored.
          </p>
          <form onSubmit={onSubmit} noValidate className="mt-4">
            <div className="flex flex-col gap-2 sm:flex-row">
              <label htmlFor={emailFieldId} className="sr-only">
                Email address
              </label>
              <input
                id={emailFieldId}
                name="email"
                type="email"
                inputMode="email"
                autoComplete="email"
                value={email}
                onChange={(e) => {
                  setEmail(e.target.value);
                  if (emailState !== "idle") setEmailState("idle");
                }}
                placeholder="you@company.com"
                aria-invalid={emailState === "error"}
                aria-describedby={
                  emailState === "error"
                    ? emailErrId
                    : emailState === "success"
                      ? emailOkId
                      : undefined
                }
                className={`w-full flex-1 rounded-xl border bg-white px-4 py-2.5 text-sm text-slate-900 placeholder:text-slate-400 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 ${focusRing} ${
                  emailState === "error"
                    ? "border-rose-400 dark:border-rose-500/60"
                    : "border-slate-300 dark:border-slate-700"
                }`}
              />
              <button
                type="submit"
                className={`inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 active:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-400 ${focusRing}`}
              >
                Notify me
              </button>
            </div>
            {emailState === "error" && (
              <p
                id={emailErrId}
                role="alert"
                className="mt-2 text-left text-sm text-rose-600 dark:text-rose-400"
              >
                Please enter a valid email address.
              </p>
            )}
            <p
              id={emailOkId}
              aria-live="polite"
              className="mt-2 text-left text-sm text-emerald-600 dark:text-emerald-400"
            >
              {emailState === "success"
                ? "You're on the list — we'll email you the moment we're back."
                : ""}
            </p>
          </form>
        </motion.div>

        {/* Footer actions */}
        <motion.div
          {...appear(0.44)}
          className="mt-10 flex flex-col items-center justify-center gap-4 border-t border-slate-200 pt-8 dark:border-slate-800 sm:flex-row sm:justify-between"
        >
          <div className="flex items-center gap-3">
            <button
              type="button"
              onClick={onRefresh}
              aria-label="Check maintenance status again"
              className={`inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3.5 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 ${focusRing}`}
            >
              <svg
                viewBox="0 0 24 24"
                className={`h-4 w-4 ${checking ? "em-spin-ring" : ""}`}
                aria-hidden="true"
                style={{ transformOrigin: "50% 50%" }}
              >
                <path
                  d="M20 11a8 8 0 1 0-2.3 5.7M20 5v6h-6"
                  fill="none"
                  strokeWidth={1.9}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="stroke-current"
                />
              </svg>
              {checking ? "Checking…" : "Check again"}
            </button>
            <span
              aria-live="polite"
              className="text-xs text-slate-400 dark:text-slate-500"
            >
              Updated {checkedLabel}
            </span>
          </div>

          <div className="flex items-center gap-4 text-sm">
            <a
              href="#status"
              className={`rounded-md font-medium text-indigo-600 underline-offset-4 hover:underline dark:text-indigo-400 ${focusRing}`}
            >
              Live status page
            </a>
            <a
              href="mailto:support@getatlas.io"
              className={`rounded-md font-medium text-slate-600 underline-offset-4 hover:underline dark:text-slate-300 ${focusRing}`}
            >
              Contact support
            </a>
          </div>
        </motion.div>
      </div>

      <style>{`
        @keyframes em-gear-a-kf { to { transform: rotate(360deg); } }
        @keyframes em-gear-b-kf { to { transform: rotate(-360deg); } }
        @keyframes em-spin-kf { to { transform: rotate(360deg); } }
        @keyframes em-pulse-dot { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.45; transform: scale(0.82); } }
        @keyframes em-ping-kf { 0% { transform: scale(1); opacity: 0.55; } 100% { transform: scale(2.4); opacity: 0; } }
        @keyframes em-shimmer-kf { 0% { transform: translateX(-120%); } 100% { transform: translateX(420%); } }
        @keyframes em-float-kf { 0%, 100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, -14px); } }

        .em-gear-a { animation: em-gear-a-kf 14s linear infinite; }
        .em-gear-b { animation: em-gear-b-kf 9s linear infinite; }
        .em-spin-ring { animation: em-spin-kf 1s linear infinite; }
        .em-dot { animation: em-pulse-dot 1.8s ease-in-out infinite; }
        .em-ping { animation: em-ping-kf 1.8s cubic-bezier(0, 0, 0.2, 1) infinite; }
        .em-shimmer { animation: em-shimmer-kf 2.4s ease-in-out infinite; }
        .em-float { animation: em-float-kf 7s ease-in-out infinite; }

        @media (prefers-reduced-motion: reduce) {
          .em-gear-a, .em-gear-b, .em-spin-ring, .em-dot, .em-ping, .em-shimmer, .em-float {
            animation: none !important;
          }
        }
      `}</style>
    </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 →