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.
390 lines
14 KiB
390 lines
14 KiB
"use client";
|
|
|
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
import { useI18n } from "@/translations/provider";
|
|
import SwipeButton from "./swipe-button";
|
|
|
|
const EXIT_ANIMATION_MS = 220;
|
|
|
|
export type FemaleOutcomeSheetProps = {
|
|
closeOnOutside?: boolean;
|
|
onClose?: () => void;
|
|
onSubmit?: (status: "success" | "failure", reason?: string) => void;
|
|
};
|
|
|
|
export function FemaleOutcomeSheet({
|
|
closeOnOutside = true,
|
|
onClose,
|
|
onSubmit,
|
|
}: FemaleOutcomeSheetProps) {
|
|
const { dictionary: t } = useI18n();
|
|
const groupId = useId();
|
|
|
|
const [isVisible, setIsVisible] = useState(true);
|
|
const [isEntering, setIsEntering] = useState(true);
|
|
const [isClosing, setIsClosing] = useState(false);
|
|
|
|
// Outcome selection state: "ongoing" (Option A) or "canceled" (Option B)
|
|
const [outcome, setOutcome] = useState<"ongoing" | "canceled">("ongoing");
|
|
|
|
// Rejection/Cancellation reason states
|
|
const reasons = 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 [selectedReasons, setSelectedReasons] = useState<string[]>([]);
|
|
const [reasonText, setReasonText] = useState("");
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
useEffect(() => {
|
|
const otherReason = reasons[reasons.length - 1];
|
|
if (selectedReasons.includes(otherReason)) {
|
|
const timeoutId = setTimeout(() => {
|
|
textareaRef.current?.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "nearest",
|
|
});
|
|
}, 100);
|
|
return () => clearTimeout(timeoutId);
|
|
}
|
|
}, [selectedReasons, reasons]);
|
|
|
|
const closeSheet = () => {
|
|
if (isClosing) {
|
|
return;
|
|
}
|
|
setIsClosing(true);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const frameId = window.requestAnimationFrame(() => {
|
|
setIsEntering(false);
|
|
});
|
|
|
|
return () => {
|
|
window.cancelAnimationFrame(frameId);
|
|
};
|
|
}, []);
|
|
|
|
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]);
|
|
|
|
useEffect(() => {
|
|
if (!isClosing) {
|
|
return;
|
|
}
|
|
|
|
const timeoutId = window.setTimeout(() => {
|
|
setIsVisible(false);
|
|
onClose?.();
|
|
}, EXIT_ANIMATION_MS);
|
|
|
|
return () => {
|
|
window.clearTimeout(timeoutId);
|
|
};
|
|
}, [isClosing, onClose]);
|
|
|
|
if (!isVisible) {
|
|
return null;
|
|
}
|
|
|
|
const isCanceledSwipeDisabled =
|
|
selectedReasons.length === 0 ||
|
|
(selectedReasons.includes(reasons[reasons.length - 1]) &&
|
|
reasonText.trim() === "");
|
|
|
|
return (
|
|
<div
|
|
className={[
|
|
"fixed inset-0 z-50 flex items-end justify-center transition-all duration-[220ms]",
|
|
isClosing || isEntering
|
|
? "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
|
|
className={[
|
|
"flex max-h-[85vh] w-full flex-col overflow-hidden rounded-t-[34px] bg-[#F9F8F8] p-4 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]",
|
|
isClosing || isEntering ? "translate-y-full" : "translate-y-0",
|
|
]
|
|
.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["What was the outcome of your contact?"]}
|
|
</h2>
|
|
|
|
<p className="mt-3.5 w-full text-start text-[14px] leading-[1.45] text-[#2C2C2C] dir-auto font-medium">
|
|
{
|
|
t[
|
|
"We hope the acquaintance process is going well. Please let us know if you want to continue the acquaintance process or if the match has been canceled."
|
|
]
|
|
}
|
|
</p>
|
|
|
|
<fieldset
|
|
className="mt-5 flex min-h-0 w-full flex-1 flex-col"
|
|
aria-labelledby={groupId}
|
|
>
|
|
<legend id={groupId} className="sr-only">
|
|
{t["What was the outcome of your contact?"]}
|
|
</legend>
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col gap-[14px] overflow-y-auto pr-1">
|
|
{/* Option A: Ongoing acquaintance */}
|
|
<label
|
|
className={[
|
|
"flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-left border transition-all",
|
|
outcome === "ongoing"
|
|
? "bg-[#E8F8F3] border-[#00AC78]/30"
|
|
: "bg-[#ECECEC] border-transparent",
|
|
].join(" ")}
|
|
>
|
|
<input
|
|
checked={outcome === "ongoing"}
|
|
className="sr-only"
|
|
name="outcome-status"
|
|
type="radio"
|
|
value="ongoing"
|
|
onChange={() => {
|
|
setOutcome("ongoing");
|
|
}}
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
className={[
|
|
"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border transition-colors",
|
|
outcome === "ongoing"
|
|
? "border-[#00AC78] bg-transparent"
|
|
: "border-[#9E9E9E] bg-transparent",
|
|
].join(" ")}
|
|
>
|
|
{outcome === "ongoing" ? (
|
|
<span className="h-3 w-3 rounded-full bg-[#00AC78]" />
|
|
) : null}
|
|
</span>
|
|
<span className="text-[15px] leading-snug font-bold text-[#262626] flex-1">
|
|
{
|
|
t[
|
|
"We are in the acquaintance/proposal process and nothing is finalized yet"
|
|
]
|
|
}
|
|
</span>
|
|
</label>
|
|
|
|
{/* Option B: Canceled */}
|
|
<label
|
|
className={[
|
|
"flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-left border transition-all",
|
|
outcome === "canceled"
|
|
? "bg-[#FFECEF] border-[#F0445B]/30"
|
|
: "bg-[#ECECEC] border-transparent",
|
|
].join(" ")}
|
|
>
|
|
<input
|
|
checked={outcome === "canceled"}
|
|
className="sr-only"
|
|
name="outcome-status"
|
|
type="radio"
|
|
value="canceled"
|
|
onChange={() => {
|
|
setOutcome("canceled");
|
|
}}
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
className={[
|
|
"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border transition-colors",
|
|
outcome === "canceled"
|
|
? "border-[#F0445B] bg-transparent"
|
|
: "border-[#9E9E9E] bg-transparent",
|
|
].join(" ")}
|
|
>
|
|
{outcome === "canceled" ? (
|
|
<span className="h-3 w-3 rounded-full bg-[#F0445B]" />
|
|
) : null}
|
|
</span>
|
|
<span className="text-[15px] leading-snug font-bold text-[#262626] flex-1">
|
|
{t.Canceled}
|
|
</span>
|
|
</label>
|
|
|
|
{/* Reasons List (if Canceled is chosen) */}
|
|
{outcome === "canceled" ? (
|
|
<div className="mt-3 flex flex-col gap-3 pl-2 text-start animate-fadeIn">
|
|
<p className="text-[13px] font-bold text-[#555] mb-1">
|
|
{t["Please select the reason for cancellation:"]}
|
|
</p>
|
|
<div className="flex flex-col gap-[10px]">
|
|
{reasons.map((option) => {
|
|
const isReasonChecked =
|
|
selectedReasons.includes(option);
|
|
const isOtherReason =
|
|
option === reasons[reasons.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-[#EAEAEA] px-4 py-[12px] text-start transition-all rounded-[12px]",
|
|
isReasonChecked
|
|
? "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-5 w-5 shrink-0 items-center justify-center rounded-[5px] border transition-colors",
|
|
isReasonChecked
|
|
? "border-[#F0445B] bg-[#F0445B]"
|
|
: "border-[#9E9E9E] bg-transparent",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
>
|
|
{isReasonChecked ? (
|
|
<svg
|
|
aria-hidden="true"
|
|
className="h-3.5 w-3.5 text-white"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth={3.5}
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M5 13l4 4L19 7"
|
|
/>
|
|
</svg>
|
|
) : null}
|
|
</span>
|
|
|
|
<span className="min-w-0 flex-1">
|
|
<span className="text-[14px] leading-none font-bold">
|
|
{option}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
|
|
{isReasonChecked && isOtherReason ? (
|
|
<textarea
|
|
ref={textareaRef}
|
|
className="mt-1 h-[90px] w-full resize-none rounded-[12px] border border-[#BFBFBF] bg-transparent px-3 py-2 text-[14px] 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>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
|
|
{/* SwipeButton Confirmation Area */}
|
|
<div className="mt-6 w-full shrink-0">
|
|
{outcome === "ongoing" ? (
|
|
<SwipeButton
|
|
theme="green"
|
|
text={t["Continue"]}
|
|
onCancel={closeSheet}
|
|
onSuccess={() => {
|
|
onSubmit?.("success");
|
|
closeSheet();
|
|
}}
|
|
/>
|
|
) : (
|
|
<SwipeButton
|
|
theme="default"
|
|
disabled={isCanceledSwipeDisabled}
|
|
text={t["Confirm"]}
|
|
onCancel={closeSheet}
|
|
onSuccess={() => {
|
|
const activeReasons = selectedReasons.map((r) => {
|
|
if (r === reasons[reasons.length - 1]) {
|
|
return reasonText ? `${r}: ${reasonText}` : r;
|
|
}
|
|
return r;
|
|
});
|
|
onSubmit?.("failure", activeReasons.join("\n"));
|
|
closeSheet();
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default FemaleOutcomeSheet;
|