zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { useState } from "zeb/react";
import Button from "@/components/ui/button";
import Card from "@/components/ui/card";
import CardContent from "@/components/ui/card-content";
import CardDescription from "@/components/ui/card-description";
import CardTitle from "@/components/ui/card-title";
import Field from "@/components/ui/field";
import Input from "@/components/ui/input";

export const page = {
  html: {
    lang: "en",
  },
  body: {
    className: "min-h-screen bg-background text-foreground font-sans",
  },
  navigation: "history",
};

export function getPage(input) {
  return {
    head: {
      title: input?.seo?.title ?? "Change Password",
      description: input?.seo?.description ?? "Choose a new password",
    },
  };
}

export default function Page(input) {
  const [currentPassword, setCurrentPassword] = useState("");
  const [newPassword, setNewPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState("");
  const [tone, setTone] = useState("muted");
  const api = input?.password_api ?? "/api/profile/password";
  const forced = Boolean(input?.forced);

  const save = async (event) => {
    event.preventDefault();
    if (newPassword !== confirmPassword) {
      setMessage("New password and confirmation do not match.");
      setTone("error");
      return;
    }
    setBusy(true);
    setMessage("");
    setTone("muted");
    try {
      const response = await fetch(api, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        body: JSON.stringify({
          current_password: currentPassword,
          new_password: newPassword,
        }),
      });
      if (response.status === 401) {
        window.location.href = "/login";
        return;
      }
      const data = await response.json().catch(() => ({}));
      if (!response.ok || data?.ok === false) {
        const err =
          data?.error?.message ?? data?.error ?? "Failed to change the password";
        setMessage(String(err));
        setTone("error");
        return;
      }
      setMessage("Password changed. Redirecting...");
      setTone("ok");
      window.location.href = "/home";
    } catch (_) {
      setMessage("Failed to change the password.");
      setTone("error");
    } finally {
      setBusy(false);
    }
  };

  const messageClass =
    tone === "ok" ? "text-emerald-700" : tone === "error" ? "text-red-600" : "text-gray-500";

  return (
    <main className="flex min-h-screen items-center justify-center px-5 py-10">
      <section className="w-full max-w-md">
        <header className="mb-6 text-center">
          <h1 className="text-2xl font-black tracking-tighter text-gray-900">
            Change password
          </h1>
          <p className="mt-2 text-sm text-gray-500">
            Signed in as{" "}
            <span className="font-medium text-gray-700">{input?.owner}</span>
          </p>
        </header>
        {forced ? (
          <p className="mb-4 rounded-[10px] border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
            This account still uses its generated bootstrap password. Choose a
            new one to continue — every other page redirects here until you do.
          </p>
        ) : null}
        <Card>
          <CardContent className="py-5">
            <CardTitle className="text-lg">New credential</CardTitle>
            <CardDescription className="mt-1">
              Verify the current password, then choose its replacement. Every
              other signed-in session of this account is ended.
            </CardDescription>
            <form className="mt-5 flex flex-col gap-4" onSubmit={save}>
              <Field label="Current password" id="account-current-password">
                <Input
                  id="account-current-password"
                  type="password"
                  value={currentPassword}
                  onInput={(event) => setCurrentPassword(event.currentTarget.value)}
                  autoComplete="current-password"
                  required
                />
              </Field>
              <Field label="New password" id="account-new-password">
                <Input
                  id="account-new-password"
                  type="password"
                  value={newPassword}
                  onInput={(event) => setNewPassword(event.currentTarget.value)}
                  autoComplete="new-password"
                  required
                />
              </Field>
              <Field label="Confirm new password" id="account-confirm-password">
                <Input
                  id="account-confirm-password"
                  type="password"
                  value={confirmPassword}
                  onInput={(event) => setConfirmPassword(event.currentTarget.value)}
                  autoComplete="new-password"
                  required
                />
              </Field>
              <div className="flex flex-wrap items-center gap-3 pt-1">
                <Button type="submit" variant="primary" disabled={busy}>
                  {busy ? "Changing..." : "Change password"}
                </Button>
                {forced ? null : (
                  <Button as="a" href="/profile" variant="outline">
                    Back to profile
                  </Button>
                )}
                {message ? (
                  <span className={`text-sm ${messageClass}`}>{message}</span>
                ) : null}
              </div>
            </form>
          </CardContent>
        </Card>
        {input?.app_version ? (
          <p className="mt-6 text-center text-[11px] text-muted-foreground">
            v{input.app_version}
          </p>
        ) : null}
      </section>
    </main>
  );
}