Files
community-rule/app/(app)/invite/_components/ExpiredInviteScreen.tsx
T

150 lines
4.8 KiB
TypeScript

"use client";
import { useId, useState } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "../../../contexts/MessagesContext";
import Login from "../../../components/modals/Login";
import Button from "../../../components/buttons/Button";
import TextInput from "../../../components/controls/TextInput";
import ContentLockup from "../../../components/type/ContentLockup";
import { EMAIL_MAX_LEN } from "../../../../lib/create/isValidCreateFlowSaveEmail";
import { INVITE_EMAIL_PATTERN } from "../../../../lib/create/parseInviteEmails";
import { resendExpiredStakeholderInvite } from "../../../../lib/create/api";
const expiredPageBgClass =
"min-h-[100dvh] bg-[var(--color-surface-invert-brand-primary)]";
export function ExpiredInviteScreen() {
const t = useTranslation("pages.invite");
const router = useRouter();
const searchParams = useSearchParams();
const token = searchParams.get("token") ?? "";
const titleId = useId();
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState("");
const [formError, setFormError] = useState("");
const [submitting, setSubmitting] = useState(false);
const [sent, setSent] = useState(false);
const heading = sent
? { title: t("sentTitle"), description: t("sentBody") }
: { title: t("expiredTitle"), description: t("expiredDescription") };
async function sendLink() {
const trimmed = email.trim().toLowerCase();
if (!INVITE_EMAIL_PATTERN.test(trimmed)) {
setEmailError(t("emailInvalid"));
return;
}
if (token.length < 10) {
setFormError(t("missingToken"));
return;
}
setEmailError("");
setFormError("");
setSubmitting(true);
const result = await resendExpiredStakeholderInvite(token, trimmed);
setSubmitting(false);
if (result.ok === false) {
if (result.retryAfterMs) {
const seconds = Math.max(1, Math.ceil(result.retryAfterMs / 1000));
setFormError(t("rateLimited").replace("{seconds}", String(seconds)));
return;
}
setFormError(result.error || t("genericError"));
return;
}
setSent(true);
}
return (
<div className={expiredPageBgClass}>
<Login
isOpen
usePortal={false}
backdropVariant="solid"
onClose={() => {
router.push("/");
}}
ariaLabelledBy={titleId}
belowCard={
<Link
href="/"
className="text-small-paragraph text-[var(--color-content-invert-tertiary,#2d2d2d)] text-center hover:opacity-90"
>
{t("backToHome")}
</Link>
}
>
<div className="flex flex-col gap-6">
<ContentLockup
titleId={titleId}
title={heading.title}
description={heading.description}
variant="login"
alignment="left"
/>
{formError ? (
<p
role="alert"
className="text-[14px] text-[var(--color-border-default-utility-negative)]"
>
{formError}
</p>
) : null}
{!sent ? (
<form
className="flex flex-col gap-4"
onSubmit={(event) => {
event.preventDefault();
void sendLink();
}}
noValidate
>
<TextInput
label={t("emailLabel")}
placeholder={t("emailPlaceholder")}
type="email"
name="email"
autoComplete="email"
inputMode="email"
value={email}
onChange={(event) => {
setEmail(event.target.value);
setEmailError("");
}}
disabled={submitting}
error={Boolean(emailError)}
showHelpIcon
maxLength={EMAIL_MAX_LEN}
/>
{emailError ? (
<p
role="alert"
className="text-[14px] text-[var(--color-border-default-utility-negative)]"
>
{emailError}
</p>
) : null}
<p className="text-small-paragraph text-[var(--color-content-default-tertiary)]">
{t("notInvitedAddress")}
</p>
<Button
type="submit"
size="large"
buttonType="filled"
palette="default"
disabled={submitting}
className="w-full !justify-center text-center px-[var(--space-400)] py-[var(--space-300)]"
>
{t("sendNewLink")}
</Button>
</form>
) : null}
</div>
</Login>
</div>
);
}