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.
 
 
 
 
 

273 lines
9.1 KiB

"use client";
import type { HTMLAttributes } from "react";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
import { useI18n } from "@/translations/provider";
import Button from "./button";
const EXIT_ANIMATION_MS = 220;
export type DismissReasonSheetProps = Omit<
HTMLAttributes<HTMLDivElement>,
"title" | "onSubmit"
> & {
closeOnOutside?: boolean;
onClose?: () => void;
onSubmit?: (value: string) => void;
};
export function DismissReasonSheet({
closeOnOutside = true,
onClose,
onSubmit,
className,
...props
}: DismissReasonSheetProps) {
const { dictionary: t } = useI18n();
const options = useMemo(
() => [
t["Not a good personal fit"],
t["No mutual interest"],
t["Different expectations"],
t["No connection felt"],
t["Location not suitable"],
t["Other reasons"],
],
[t],
);
const groupId = useId();
const [isVisible, setIsVisible] = useState(true);
const [isClosing, setIsClosing] = useState(false);
const [selectedReasons, setSelectedReasons] = useState<string[]>([]);
const [reasonText, setReasonText] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
useEffect(() => {
const otherReason = options[options.length - 1];
if (selectedReasons.includes(otherReason)) {
const timeoutId = setTimeout(() => {
textareaRef.current?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}, 100);
return () => clearTimeout(timeoutId);
}
}, [selectedReasons, options]);
const closeSheet = useCallback(() => {
if (isClosing) {
return;
}
setIsClosing(true);
window.setTimeout(() => {
if (isMountedRef.current) {
setIsVisible(false);
}
onClose?.();
}, EXIT_ANIMATION_MS);
}, [isClosing, onClose]);
useHardwareBackHandler(closeSheet, isVisible && !isClosing);
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;
}
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["Dismiss reasons"]}
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={[
"flex h-[80vh] w-full flex-col overflow-hidden rounded-t-[15px] 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 sm:max-w-[375px] animate-in slide-in-from-bottom",
isClosing ? "translate-y-full" : "translate-y-0",
className,
]
.filter(Boolean)
.join(" ")}
>
<div className="mx-auto flex h-full min-h-0 w-full flex-col items-center">
<div className="flex min-h-0 w-full flex-1 flex-col items-center">
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]">
{t["Dismiss reasons"]}
</h2>
<p className="mt-3.5 w-full text-start text-[14px] leading-[1.45] text-[#2C2C2C] dir-auto">
{
t[
"Please provide the full reason for rejecting the submitted item"
]
}
</p>
<fieldset
className="mt-4 flex min-h-0 w-full flex-1 flex-col"
aria-labelledby={groupId}
>
<legend id={groupId} className="sr-only">
{t["Dismiss reasons"]}
</legend>
<div className="flex min-h-0 flex-1 flex-col gap-[14px] overflow-y-auto pr-1">
{options.map((option) => {
const checked = selectedReasons.includes(option);
const showTextArea = option === options[options.length - 1];
return (
<div key={option} className="w-full flex flex-col gap-2">
<button
type="button"
className={[
"flex w-full items-center gap-3 bg-[#ECECEC] px-5 py-[14px] text-start transition-all rounded-[15px]",
checked ? "text-[#171717]" : "text-[#7B7B7B]",
]
.filter(Boolean)
.join(" ")}
onClick={() => {
setSelectedReasons((prev) =>
prev.includes(option)
? prev.filter((r) => r !== option)
: [...prev, option],
);
}}
>
<span
aria-hidden="true"
className={[
"flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border transition-colors",
checked
? "border-[#F0445B] bg-[#F0445B]"
: "border-[#9E9E9E] bg-transparent",
]
.filter(Boolean)
.join(" ")}
>
{checked ? (
<svg
aria-hidden="true"
className="h-4 w-4 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
) : null}
</span>
<span className="min-w-0 flex-1">
<span className="text-[16px] leading-[1.2] font-semibold">
{option}
</span>
</span>
</button>
{checked && showTextArea ? (
<textarea
ref={textareaRef}
className="mt-1 h-[125px] w-full resize-none rounded-[16px] border border-[#454545] bg-transparent px-3 py-3 text-[16px] text-[#171717] outline-none placeholder:text-[#AAAAAA]"
placeholder={
t["Feel free to briefly explain your decision..."]
}
value={reasonText}
onChange={(event) =>
setReasonText(event.target.value)
}
/>
) : null}
</div>
);
})}
</div>
</fieldset>
</div>
<div className="mt-4 w-full shrink-0">
<Button
className="rounded-[15px] shadow-none"
disabled={
selectedReasons.length === 0 ||
(selectedReasons.includes(options[options.length - 1]) &&
reasonText.trim() === "")
}
onClick={() => {
const activeReasons = selectedReasons.map((r) => {
if (r === options[options.length - 1]) {
return reasonText ? `${r}: ${reasonText}` : r;
}
return r;
});
onSubmit?.(activeReasons.join("\n"));
closeSheet();
}}
>
{t["Decline"]}
</Button>
</div>
</div>
</section>
</div>
);
}
export default DismissReasonSheet;