You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
6.6 KiB
205 lines
6.6 KiB
"use client";
|
|
|
|
import type { HTMLAttributes } from "react";
|
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
import { useI18n } from "@/translations/provider";
|
|
|
|
const EXIT_ANIMATION_MS = 220;
|
|
|
|
export type OutcomeSelectionSheetProps = Omit<
|
|
HTMLAttributes<HTMLDivElement>,
|
|
"title" | "onSubmit"
|
|
> & {
|
|
closeOnOutside?: boolean;
|
|
onClose?: () => void;
|
|
onSubmit?: (status: "success" | "failure") => void;
|
|
};
|
|
|
|
export function OutcomeSelectionSheet({
|
|
closeOnOutside = true,
|
|
onClose,
|
|
onSubmit,
|
|
className,
|
|
...props
|
|
}: OutcomeSelectionSheetProps) {
|
|
const { dictionary: t } = useI18n();
|
|
const groupId = useId();
|
|
const [isVisible, setIsVisible] = useState(true);
|
|
const [isClosing, setIsClosing] = useState(false);
|
|
const [selectedStatus, setSelectedStatus] = useState<"success" | "failure">(
|
|
"success",
|
|
);
|
|
const isMountedRef = useRef(true);
|
|
|
|
useEffect(() => {
|
|
isMountedRef.current = true;
|
|
return () => {
|
|
isMountedRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
const closeSheet = useCallback(() => {
|
|
if (isClosing) {
|
|
return;
|
|
}
|
|
setIsClosing(true);
|
|
window.setTimeout(() => {
|
|
if (isMountedRef.current) {
|
|
setIsVisible(false);
|
|
}
|
|
onClose?.();
|
|
}, EXIT_ANIMATION_MS);
|
|
}, [isClosing, onClose]);
|
|
|
|
useEffect(() => {
|
|
if (!isVisible) {
|
|
return;
|
|
}
|
|
|
|
const previousBodyOverflow = document.body.style.overflow;
|
|
const previousHtmlOverflow = document.documentElement.style.overflow;
|
|
|
|
document.body.style.overflow = "hidden";
|
|
document.documentElement.style.overflow = "hidden";
|
|
|
|
return () => {
|
|
document.body.style.overflow = previousBodyOverflow;
|
|
document.documentElement.style.overflow = previousHtmlOverflow;
|
|
};
|
|
}, [isVisible]);
|
|
|
|
if (!isVisible) {
|
|
return null;
|
|
}
|
|
|
|
const options = [
|
|
{ value: "success" as const, label: t["We reached an agreement"] },
|
|
{ value: "failure" as const, label: t["We did not reach an agreement"] },
|
|
];
|
|
|
|
return (
|
|
<div
|
|
className={[
|
|
"fixed inset-0 z-50 flex items-end justify-center transition-all duration-[220ms] animate-in fade-in",
|
|
isClosing
|
|
? "bg-[#171717]/0 opacity-0"
|
|
: "bg-[#171717]/55 opacity-100",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t["What was the outcome of your contact?"]}
|
|
tabIndex={-1}
|
|
onClick={(event) => {
|
|
if (closeOnOutside && event.target === event.currentTarget) {
|
|
closeSheet();
|
|
}
|
|
}}
|
|
onKeyDown={(event) => {
|
|
if (
|
|
closeOnOutside &&
|
|
event.target === event.currentTarget &&
|
|
(event.key === "Escape" || event.key === "Enter" || event.key === " ")
|
|
) {
|
|
event.preventDefault();
|
|
closeSheet();
|
|
}
|
|
}}
|
|
>
|
|
<section
|
|
{...props}
|
|
className={[
|
|
"w-full sm:max-w-[375px] rounded-t-[34px] bg-[#F9F8F8] p-3.5 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform animate-in slide-in-from-bottom",
|
|
isClosing ? "translate-y-full" : "translate-y-0",
|
|
className,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
>
|
|
<div className="mx-auto flex flex-col items-center">
|
|
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]">
|
|
{t["What was the outcome of your contact?"]}
|
|
</h2>
|
|
|
|
<div
|
|
className="mt-5 w-full"
|
|
role="radiogroup"
|
|
aria-labelledby={groupId}
|
|
>
|
|
<span id={groupId} className="sr-only">
|
|
{t["What was the outcome of your contact?"]}
|
|
</span>
|
|
|
|
<div className="flex flex-col gap-[14px]">
|
|
{options.map((option) => {
|
|
const checked = selectedStatus === option.value;
|
|
|
|
return (
|
|
<label
|
|
key={option.value}
|
|
className="flex cursor-pointer items-center gap-3 rounded-[14px] bg-[#ECECEC] px-[14px] py-[18px] text-left"
|
|
>
|
|
<input
|
|
checked={checked}
|
|
className="sr-only"
|
|
name="outcome-status"
|
|
type="radio"
|
|
value={option.value}
|
|
onChange={() => {
|
|
setSelectedStatus(option.value);
|
|
}}
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
className={[
|
|
"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border",
|
|
checked ? "border-[#F0445B]" : "border-[#9E9E9E]",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
>
|
|
{checked ? (
|
|
<span className="h-3 w-3 rounded-full bg-[#F0445B]" />
|
|
) : null}
|
|
</span>
|
|
<span className="text-[16px] leading-none font-semibold text-[#262626]">
|
|
{option.label}
|
|
</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 grid w-full grid-cols-[33fr_67fr] gap-3">
|
|
<button
|
|
type="button"
|
|
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
|
|
onClick={closeSheet}
|
|
>
|
|
<div className="inline-flex w-full items-center justify-center rounded-[14px] border border-[#9A9A9A] bg-[#F7F7F7] px-2 h-[52px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
|
|
<span className="truncate">{t["Cancel"]}</span>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
|
|
onClick={() => {
|
|
onSubmit?.(selectedStatus);
|
|
closeSheet();
|
|
}}
|
|
>
|
|
<div className="inline-flex w-full items-center justify-center rounded-[14px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
|
|
<span className="truncate">{t["Submit"]}</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default OutcomeSelectionSheet;
|