Update and refine alert modals

This commit is contained in:
adilallo
2026-04-26 08:08:02 -06:00
parent 0ce05372bf
commit 9962f44ff1
15 changed files with 508 additions and 157 deletions
+21 -5
View File
@@ -11,6 +11,7 @@ import { useCreateFlow } from "./context/CreateFlowContext";
import { parseCreateFlowScreenFromPathname } from "./utils/flowSteps";
import { saveDraftToServer } from "../../../lib/create/api";
import messages from "../../../messages/en/index";
import Alert from "../../components/modals/Alert";
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
@@ -139,12 +140,27 @@ export function PostLoginDraftTransfer({
if (!transferError) return null;
const [titleLine, ...rest] = transferError.split(/\n\n+/);
const title = (titleLine ?? transferError).trim();
const description = rest.join("\n\n").trim() || undefined;
return (
<div
role="alert"
className="mx-auto max-w-[640px] px-5 py-3 text-center font-inter text-sm text-[var(--color-border-default-utility-negative)]"
>
{transferError}
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-[150] flex justify-center px-5 md:bottom-6">
<div className="pointer-events-auto w-full max-w-[640px]">
<Alert
type="banner"
status="danger"
size="s"
title={title}
description={description}
hasBodyText={Boolean(description)}
hasLeadingIcon
onClose={() => {
setTransferError(null);
}}
className="w-full"
/>
</div>
</div>
);
}
+14 -6
View File
@@ -11,6 +11,7 @@ import {
import { useCreateFlow } from "./context/CreateFlowContext";
import { fetchDraftFromServer } from "../../../lib/create/api";
import messages from "../../../messages/en/index";
import Alert from "../../components/modals/Alert";
import {
isValidStep,
parseCreateFlowScreenFromPathname,
@@ -119,12 +120,19 @@ export function SignedInDraftHydration({
if (!loadingHydration) return null;
return (
<div
role="status"
aria-live="polite"
className="w-full shrink-0 px-[var(--spacing-measures-spacing-500,20px)] py-[var(--spacing-measures-spacing-200,8px)] md:px-[var(--measures-spacing-1800,64px)] text-center font-inter text-sm text-[var(--color-text-default-secondary,#a3a3a3)]"
>
{messages.create.draftHydration.loadingSavedProgress}
<div className="pointer-events-none fixed left-0 right-0 top-14 z-[170] flex justify-center px-[var(--spacing-measures-spacing-500,20px)] pt-2 md:top-16 md:px-[var(--measures-spacing-1800,64px)]">
<div className="pointer-events-auto w-full max-w-[960px]">
<Alert
type="banner"
status="default"
size="s"
title={messages.create.draftHydration.loadingSavedProgress}
hasBodyText={false}
hasLeadingIcon={false}
hasTrailingIcon={false}
className="w-full"
/>
</div>
</div>
);
}
@@ -86,19 +86,28 @@ export default function ReviewTemplatePage({ params }: PageProps) {
if (error || !template) {
return (
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
<>
<div
className={`flex shrink-0 flex-col gap-4 pb-8 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
className="pointer-events-none fixed left-0 right-0 top-14 z-[120] flex justify-center px-5 pt-3 md:top-20 md:px-12"
aria-live="polite"
>
<Alert
type="banner"
status="danger"
title={t("errors.loadFailed")}
description={error ?? t("errors.notFound")}
className="w-full"
/>
<div className="pointer-events-auto w-full max-w-[960px]">
<Alert
type="banner"
status="danger"
title={t("errors.loadFailed")}
description={error ?? t("errors.notFound")}
className="w-full"
/>
</div>
</div>
</CreateFlowStepShell>
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
<div
className={`min-h-[40vh] shrink-0 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
aria-hidden
/>
</CreateFlowStepShell>
</>
);
}
+25 -2
View File
@@ -62,6 +62,7 @@ export default function ProfilePageClient() {
const [emailChangeModalError, setEmailChangeModalError] = useState<
string | null
>(null);
const [emailChangeRequestSent, setEmailChangeRequestSent] = useState(false);
const [profileSuccessMessage, setProfileSuccessMessage] = useState<
string | null
>(null);
@@ -131,6 +132,7 @@ export default function ProfilePageClient() {
setActionError(null);
setProfileSuccessMessage(null);
setEmailChangeModalError(null);
setEmailChangeRequestSent(false);
setEmailChangeInput(user.email);
setEmailChangeOpen(true);
}, [user]);
@@ -138,8 +140,25 @@ export default function ProfilePageClient() {
const handleCloseEmailChange = useCallback(() => {
if (emailChangeBusy) return;
setEmailChangeOpen(false);
setEmailChangeRequestSent(false);
}, [emailChangeBusy]);
const handleDismissProfileSuccess = useCallback(() => {
setProfileSuccessMessage(null);
}, []);
const handleDismissActionError = useCallback(() => {
setActionError(null);
}, []);
const handleDismissRulesError = useCallback(() => {
setRulesError(false);
}, []);
const handleDismissEmailChangeModalError = useCallback(() => {
setEmailChangeModalError(null);
}, []);
const handleSubmitEmailChange = useCallback(async () => {
const trimmed = emailChangeInput.trim();
if (!trimmed || emailChangeBusy) return;
@@ -157,8 +176,7 @@ export default function ProfilePageClient() {
setEmailChangeModalError(res.error);
}
} else {
setEmailChangeOpen(false);
setProfileSuccessMessage(t("emailChangeRequestSent"));
setEmailChangeRequestSent(true);
}
}, [emailChangeBusy, emailChangeInput, t]);
@@ -318,7 +336,12 @@ export default function ProfilePageClient() {
emailChangeValue={emailChangeInput}
onEmailChangeValueChange={(value) => setEmailChangeInput(value)}
emailChangeBusy={emailChangeBusy}
emailChangeRequestSent={emailChangeRequestSent}
emailChangeModalError={emailChangeModalError}
onDismissProfileSuccess={handleDismissProfileSuccess}
onDismissActionError={handleDismissActionError}
onDismissRulesError={handleDismissRulesError}
onDismissEmailChangeModalError={handleDismissEmailChangeModalError}
onOpenEmailChange={handleOpenEmailChange}
onCloseEmailChange={handleCloseEmailChange}
onSubmitEmailChange={handleSubmitEmailChange}
@@ -6,7 +6,9 @@ import RuleCard from "../../../components/cards/RuleCard";
import TextInput from "../../../components/controls/TextInput";
import List from "../../../components/layout/List";
import type { ListItem, ListSize } from "../../../components/layout/List";
import Icon from "../../../components/asset/Icon";
import Dialog from "../../../components/modals/Dialog";
import Alert from "../../../components/modals/Alert";
import HeaderLockup from "../../../components/type/HeaderLockup";
import { useTranslation } from "../../../contexts/MessagesContext";
import type { CreateFlowState } from "../../create/types";
@@ -49,7 +51,9 @@ export type ProfilePageViewProps = {
emailChangeValue: string;
onEmailChangeValueChange: (value: string) => void;
emailChangeBusy: boolean;
emailChangeRequestSent: boolean;
emailChangeModalError: string | null;
onDismissEmailChangeModalError: () => void;
onOpenEmailChange: () => void;
onCloseEmailChange: () => void;
onSubmitEmailChange: () => void;
@@ -65,6 +69,9 @@ export type ProfilePageViewProps = {
onOpenDeleteAccount: () => void;
onCloseDeleteAccount: () => void;
onConfirmDeleteAccount: () => void;
onDismissProfileSuccess: () => void;
onDismissActionError: () => void;
onDismissRulesError: () => void;
};
/**
@@ -171,7 +178,9 @@ export function ProfilePageView({
emailChangeValue,
onEmailChangeValueChange,
emailChangeBusy,
emailChangeRequestSent,
emailChangeModalError,
onDismissEmailChangeModalError,
onOpenEmailChange,
onCloseEmailChange,
onSubmitEmailChange,
@@ -187,8 +196,12 @@ export function ProfilePageView({
onOpenDeleteAccount,
onCloseDeleteAccount,
onConfirmDeleteAccount,
onDismissProfileSuccess,
onDismissActionError,
onDismissRulesError,
}: ProfilePageViewProps) {
const t = useTranslation("pages.profile");
const tLogin = useTranslation("pages.login");
const titleId = useId();
const welcomeTitle = t("welcomeTitle").replace(/\{\{name\}\}/g, userEmail);
const welcomeBody =
@@ -277,30 +290,6 @@ export function ProfilePageView({
)}
</header>
{profileSuccessMessage ? (
<p
className="rounded-lg border border-[var(--color-border-default-secondary)] bg-[var(--color-surface-default-secondary)] px-4 py-3 font-inter text-sm text-[var(--color-content-default-primary)]"
role="status"
>
{profileSuccessMessage}
</p>
) : null}
{actionError ? (
<p
className="rounded-lg border border-[var(--color-border-default-secondary)] bg-[var(--color-surface-default-tertiary)] px-4 py-3 font-inter text-sm text-[var(--color-content-default-primary)]"
role="alert"
>
{actionError}
</p>
) : null}
{rulesError ? (
<p className="font-inter text-sm text-[var(--color-content-default-tertiary)]">
{t("actionError")}
</p>
) : null}
<div className="flex flex-col gap-8 lg:flex-row lg:flex-nowrap lg:items-start lg:gap-8">
<section
className="flex min-w-0 w-full flex-col gap-3 lg:min-w-0 lg:flex-1 lg:gap-6"
@@ -519,53 +508,136 @@ export function ProfilePageView({
if (!emailChangeBusy) onCloseEmailChange();
}}
backdropVariant="blurredYellow"
title={t("emailChangeModalTitle")}
description={t("emailChangeModalDescription")}
title={
emailChangeRequestSent
? tLogin("successTitle")
: t("emailChangeModalTitle")
}
description={
emailChangeRequestSent
? tLogin("successBody")
: t("emailChangeModalDescription")
}
footer={
<>
emailChangeRequestSent ? (
<Button
type="button"
size="medium"
buttonType="outline"
palette="default"
onClick={onCloseEmailChange}
disabled={emailChangeBusy}
>
{t("emailChangeCancel")}
{t("emailChangeConfirmationClose")}
</Button>
<Button
type="button"
size="medium"
buttonType="filled"
palette="default"
onClick={onSubmitEmailChange}
disabled={emailChangeBusy}
>
{t("emailChangeSubmit")}
</Button>
</>
) : (
<>
<Button
type="button"
size="medium"
buttonType="outline"
palette="default"
onClick={onCloseEmailChange}
disabled={emailChangeBusy}
>
{t("emailChangeCancel")}
</Button>
<Button
type="button"
size="medium"
buttonType="filled"
palette="default"
onClick={onSubmitEmailChange}
disabled={emailChangeBusy}
>
{t("emailChangeSubmit")}
</Button>
</>
)
}
>
{emailChangeModalError ? (
<p
className="font-inter text-sm text-[var(--color-content-default-primary)]"
role="alert"
>
{emailChangeModalError}
</p>
) : null}
<TextInput
type="email"
inputSize="medium"
label={t("emailChangeNewEmailLabel")}
placeholder={t("emailChangeNewEmailPlaceholder")}
value={emailChangeValue}
onChange={(e) => onEmailChangeValueChange(e.target.value)}
disabled={emailChangeBusy}
error={Boolean(emailChangeModalError)}
autoComplete="email"
/>
{emailChangeRequestSent ? (
<div className="flex flex-col gap-3 pt-2">
<div className="relative flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-[var(--color-surface-inverse-brand-primary)]">
<Icon name="mail" size={22} aria-hidden />
</div>
</div>
) : (
<TextInput
type="email"
inputSize="medium"
label={t("emailChangeNewEmailLabel")}
placeholder={t("emailChangeNewEmailPlaceholder")}
value={emailChangeValue}
onChange={(e) => onEmailChangeValueChange(e.target.value)}
disabled={emailChangeBusy}
error={Boolean(emailChangeModalError)}
autoComplete="email"
/>
)}
</Dialog>
{(profileSuccessMessage || actionError || rulesError) && (
<div
className="pointer-events-none fixed left-0 right-0 top-[41px] z-[60] flex justify-center px-4 pt-3 md:px-8 lg:top-[85px] lg:px-16 xl:top-[89px]"
aria-live="polite"
>
<div className="pointer-events-auto flex w-full max-w-[640px] flex-col gap-2">
{profileSuccessMessage ? (
<Alert
type="banner"
status="positive"
size="s"
title={profileSuccessMessage}
hasBodyText={false}
hasLeadingIcon
onClose={onDismissProfileSuccess}
className="w-full"
/>
) : null}
{actionError ? (
<Alert
type="banner"
status="danger"
size="s"
title={actionError}
hasBodyText={false}
hasLeadingIcon
onClose={onDismissActionError}
className="w-full"
/>
) : null}
{rulesError ? (
<Alert
type="banner"
status="warning"
size="s"
title={t("rulesLoadBannerTitle")}
description={t("rulesLoadBannerDescription")}
hasLeadingIcon
onClose={onDismissRulesError}
className="w-full"
/>
) : null}
</div>
</div>
)}
{emailChangeOpen && emailChangeModalError ? (
<div className="pointer-events-none fixed inset-x-0 top-6 z-[10001] flex justify-center px-4 md:top-10">
<div className="pointer-events-auto w-full max-w-[min(480px,calc(100%-2rem))]">
<Alert
type="banner"
status="danger"
size="s"
title={emailChangeModalError}
hasBodyText={false}
hasLeadingIcon
onClose={onDismissEmailChangeModalError}
className="w-full"
/>
</div>
</div>
) : null}
</>
);
}
+82 -21
View File
@@ -1,52 +1,117 @@
"use client";
/**
* Figma: "Modal / Alert" (6351-14646)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=6351-14646
*/
import { memo } from "react";
import { AlertView } from "./Alert.view";
import type { AlertProps } from "./Alert.types";
function layoutFor(
type: NonNullable<AlertProps["type"]>,
size: NonNullable<AlertProps["size"]>,
): {
containerClasses: string;
titleClasses: string;
descriptionClasses: string;
} {
if (type === "toast") {
const padH =
size === "s"
? "px-[var(--space-500)]"
: "px-[var(--space-1200)]";
const containerClasses = `flex gap-[var(--space-300)] items-center ${padH} pb-[var(--space-500)] pt-[var(--space-400)] rounded-tl-[var(--radius-200,8px)] rounded-tr-[var(--radius-200,8px)] border-solid`;
if (size === "s") {
return {
containerClasses,
titleClasses:
"font-inter text-[14px] leading-[18px] font-medium tracking-[0%]",
descriptionClasses:
"font-inter text-[14px] leading-[20px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
};
}
return {
containerClasses,
titleClasses:
"font-inter text-[18px] leading-[24px] font-medium tracking-[0%]",
descriptionClasses:
"font-inter text-[18px] leading-[1.3] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
};
}
if (size === "s") {
return {
containerClasses:
"flex gap-[var(--space-300)] items-center p-[var(--space-300)] rounded-[var(--radius-200,8px)] border-solid",
titleClasses:
"font-inter text-[14px] leading-[18px] font-medium tracking-[0%]",
descriptionClasses:
"font-inter text-[14px] leading-[20px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
};
}
return {
containerClasses:
"flex gap-[var(--space-300)] items-center px-[var(--space-600)] py-[var(--space-400)] rounded-[var(--radius-200,8px)] border-solid",
titleClasses:
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
descriptionClasses:
"font-inter text-[16px] leading-[24px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
};
}
const AlertContainer = memo<AlertProps>(
({
title,
description,
status: statusProp = "default",
type: typeProp = "toast",
size: sizeProp = "m",
hasLeadingIcon = true,
hasBodyText = true,
hasTrailingIcon: hasTrailingIconProp,
onClose,
className = "",
}) => {
const status = statusProp;
const type = typeProp;
// Determine background and border colors based on status and type
const size = sizeProp;
const getStatusStyles = () => {
switch (status) {
case "positive":
return {
background: "bg-[var(--color-kiwi-kiwi0)]",
background:
"bg-[var(--color-surface-invert-positive-secondary,var(--color-kiwi-kiwi0))]",
borderColor:
type === "toast"
? "var(--color-border-invert-positive-primary)"
: undefined,
titleColor: "text-[var(--color-content-invert-primary)]",
descriptionColor: "text-[var(--color-content-invert-secondary)]",
descriptionColor:
"text-[var(--color-content-invert-secondary)]",
iconColor: "var(--color-kiwi-kiwi500)",
closeButtonIconColor: "var(--color-content-invert-primary)",
};
case "warning":
return {
background: "bg-[var(--color-yellow-yellow0)]",
background:
"bg-[var(--color-surface-invert-warning-secondary,var(--color-yellow-yellow0))]",
borderColor:
type === "toast"
? "var(--color-border-invert-warning-primary)"
: undefined,
titleColor: "text-[var(--color-content-invert-primary)]",
descriptionColor: "text-[var(--color-content-invert-secondary)]",
descriptionColor:
"text-[var(--color-content-invert-secondary)]",
iconColor: "var(--color-yellow-yellow500)",
closeButtonIconColor: "var(--color-content-invert-primary)",
};
case "danger":
return {
background: "bg-[var(--color-red-red0)]",
background:
"bg-[var(--color-surface-invert-negative-secondary,var(--color-red-red0))]",
borderColor:
type === "toast"
? "var(--color-border-invert-negative-primary)"
@@ -67,18 +132,14 @@ const AlertContainer = memo<AlertProps>(
titleColor: "text-[var(--color-content-default-primary)]",
descriptionColor: "text-[var(--color-content-default-primary)]",
iconColor: "var(--color-content-default-brand-primary)",
closeButtonIconColor: "var(--color-content-default-brand-primary)",
closeButtonIconColor:
"var(--color-content-default-brand-primary)",
};
}
};
const statusStyles = getStatusStyles();
const containerClasses = `flex gap-[var(--space-300)] items-center ${
type === "toast"
? `pb-[var(--space-500)] pt-[var(--space-400)] px-[var(--space-1200)] rounded-tl-[var(--radius-200,8px)] rounded-tr-[var(--radius-200,8px)]`
: `px-[var(--spacing-scale-024)] py-[var(--spacing-scale-016)] rounded-[var(--radius-200,8px)]`
} ${statusStyles.background} border-solid`;
const layout = layoutFor(type, size);
const containerStyle =
type === "toast" && statusStyles.borderColor
@@ -88,15 +149,14 @@ const AlertContainer = memo<AlertProps>(
}
: undefined;
const titleClasses =
type === "banner"
? `font-inter text-[16px] leading-[20px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full`
: `font-inter text-[18px] leading-[24px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full`;
const containerClasses = `${layout.containerClasses} ${statusStyles.background}`;
const descriptionClasses =
type === "banner"
? `font-inter text-[16px] leading-[24px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]`
: `font-inter text-[18px] leading-[23.4px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]`;
const titleClasses = `${layout.titleClasses} ${statusStyles.titleColor} relative shrink-0 w-full`;
const descriptionClasses = `${layout.descriptionClasses} ${statusStyles.descriptionColor} relative shrink-0 w-full`;
const hasTrailingIcon =
hasTrailingIconProp ?? Boolean(onClose);
const showClose = hasTrailingIcon && Boolean(onClose);
return (
<AlertView
@@ -106,6 +166,7 @@ const AlertContainer = memo<AlertProps>(
type={type}
hasLeadingIcon={hasLeadingIcon}
hasBodyText={hasBodyText}
hasTrailingIcon={showClose}
className={className}
containerClasses={containerClasses}
containerStyle={containerStyle}
@@ -1,7 +1,11 @@
import type { AlertSizeValue } from "../../../../lib/propNormalization";
export type AlertStatusValue = "default" | "positive" | "warning" | "danger";
export type AlertTypeValue = "toast" | "banner";
export type { AlertSizeValue };
export interface AlertProps {
title: string;
description?: string;
@@ -13,6 +17,11 @@ export interface AlertProps {
* Alert type.
*/
type?: AlertTypeValue;
/**
* Density / typography scale (Figma Modal Alert S | M).
* @default "m"
*/
size?: AlertSizeValue;
/**
* Whether to show the leading icon (Figma prop).
* @default true
@@ -23,6 +32,11 @@ export interface AlertProps {
* @default true
*/
hasBodyText?: boolean;
/**
* Trailing dismiss control (Figma `hasTrailingIcon`).
* When omitted, defaults to `true` when `onClose` is provided, else `false`.
*/
hasTrailingIcon?: boolean;
onClose?: () => void;
className?: string;
}
@@ -34,6 +48,7 @@ export interface AlertViewProps {
type: "toast" | "banner";
hasLeadingIcon: boolean;
hasBodyText: boolean;
hasTrailingIcon: boolean;
className: string;
containerClasses: string;
containerStyle?: React.CSSProperties;
+33 -30
View File
@@ -8,6 +8,7 @@ export function AlertView({
type: _type,
hasLeadingIcon,
hasBodyText,
hasTrailingIcon,
className,
containerClasses,
containerStyle,
@@ -54,40 +55,42 @@ export function AlertView({
<p className={descriptionClasses}>{description}</p>
)}
</div>
<Button
buttonType="ghost"
palette="default"
size="large"
onClick={onClose}
ariaLabel="Close alert"
className="shrink-0 [&_svg_path]:transition-colors [&_svg_path]:duration-200 hover:[&_svg_path]:fill-[var(--color-content-default-primary)]"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{hasTrailingIcon && onClose ? (
<Button
buttonType="ghost"
palette="default"
size="large"
onClick={onClose}
ariaLabel="Close alert"
className="shrink-0 [&_svg_path]:transition-colors [&_svg_path]:duration-200 hover:[&_svg_path]:fill-[var(--color-content-default-primary)]"
>
<mask
id="mask0_21296_8285"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="0"
y="0"
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="20" height="20" fill="#D9D9D9" />
</mask>
<g mask="url(#mask0_21296_8285)">
<path
d="M5.33327 15.5448L4.45508 14.6666L9.12174 9.99993L4.45508 5.33327L5.33327 4.45508L9.99993 9.12174L14.6666 4.45508L15.5448 5.33327L10.8781 9.99993L15.5448 14.6666L14.6666 15.5448L9.99993 10.8781L5.33327 15.5448Z"
fill={closeButtonIconColor}
/>
</g>
</svg>
</Button>
<mask
id="mask0_21296_8285"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="20"
height="20"
>
<rect width="20" height="20" fill="#D9D9D9" />
</mask>
<g mask="url(#mask0_21296_8285)">
<path
d="M5.33327 15.5448L4.45508 14.6666L9.12174 9.99993L4.45508 5.33327L5.33327 4.45508L9.99993 9.12174L14.6666 4.45508L15.5448 5.33327L10.8781 9.99993L15.5448 14.6666L14.6666 15.5448L9.99993 10.8781L5.33327 15.5448Z"
fill={closeButtonIconColor}
/>
</g>
</svg>
</Button>
) : null}
</div>
);
}
+35 -21
View File
@@ -7,6 +7,7 @@ import { useTranslation } from "../../../contexts/MessagesContext";
import Button from "../../buttons/Button";
import TextInput from "../../controls/TextInput";
import ContentLockup from "../../type/ContentLockup";
import Alert from "../Alert";
import { requestMagicLink } from "../../../../lib/create/api";
import { safeInternalPath } from "../../../../lib/safeInternalPath";
import { setTransferPendingFlag } from "../../../(app)/create/utils/anonymousDraftStorage";
@@ -55,7 +56,6 @@ export default function LoginForm({
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const formAlertId = useId();
const emailErrorId = useId();
const [email, setEmail] = useState("");
@@ -166,26 +166,40 @@ export default function LoginForm({
/>
</div>
{urlErrorMessage ? (
<p
role="alert"
aria-live="polite"
className="text-center font-inter text-[14px] leading-[20px] text-[var(--color-border-default-utility-negative)]"
>
{urlErrorMessage}
</p>
) : null}
{formError ? (
<p
id={formAlertId}
role="alert"
aria-live="polite"
className="font-inter text-[14px] leading-[20px] text-[var(--color-border-default-utility-negative)]"
>
{formError}
</p>
) : null}
{(urlErrorMessage || formError) && (
<div className="pointer-events-none fixed inset-x-0 top-4 z-[10000] flex justify-center px-4 md:top-6">
<div className="pointer-events-auto flex w-full max-w-[560px] flex-col gap-2">
{urlErrorMessage ? (
<Alert
type="banner"
status="danger"
size="s"
title={urlErrorMessage}
hasBodyText={false}
hasLeadingIcon
onClose={() => {
stripErrorQuery();
}}
className="w-full"
/>
) : null}
{formError ? (
<Alert
type="banner"
status="danger"
size="s"
title={formError}
hasBodyText={false}
hasLeadingIcon
onClose={() => {
setFormError("");
}}
className="w-full"
/>
) : null}
</div>
</div>
)}
{!sent ? (
<form