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.
 
 
 
 
 

202 lines
7.0 KiB

"use client";
import type { ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import Button from "./button";
import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220;
export type HelpModalProps = {
isOpen: boolean;
onClose: () => void;
title?: ReactNode;
description?: ReactNode;
buttonText?: ReactNode;
};
export function HelpModal({
isOpen,
onClose,
title: _title,
description,
buttonText,
}: HelpModalProps) {
const { dictionary: t } = useI18n();
const [mounted, setMounted] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
setMounted(true);
return () => {
isMountedRef.current = false;
};
}, []);
// When isOpen changes from outside, reset isClosing
useEffect(() => {
if (isOpen) {
setIsClosing(false);
}
}, [isOpen]);
const resolvedTitle = "Tips";
const resolvedDescription =
description ??
t[
"Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance."
] ??
"Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance.";
const resolvedButtonText = buttonText ?? t["Got it"] ?? "Got it";
const closeSheet = useCallback(() => {
if (isClosing) return;
setIsClosing(true);
window.setTimeout(() => {
if (isMountedRef.current) {
setIsClosing(false);
}
onClose();
}, EXIT_ANIMATION_MS);
}, [isClosing, onClose]);
// Lock body scroll
useEffect(() => {
if (!isOpen || !mounted) 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;
};
}, [isOpen, mounted]);
if (!isOpen || !mounted) return null;
return createPortal(
<div
className={[
"fixed inset-0 z-[100] flex items-end justify-center transition-all duration-[220ms] animate-in fade-in",
isClosing
? "bg-[#171717]/0 opacity-0"
: "bg-[#171717]/55 opacity-100",
].join(" ")}
role="dialog"
aria-modal="true"
aria-label={String(resolvedTitle)}
tabIndex={-1}
onClick={(e) => {
if (e.target === e.currentTarget) closeSheet();
}}
onKeyDown={(e) => {
if (
e.target === e.currentTarget &&
(e.key === "Escape" || e.key === "Enter" || e.key === " ")
) {
e.preventDefault();
closeSheet();
}
}}
>
<section
className={[
"w-full sm:max-w-[375px] rounded-t-[15px] bg-[#F9F8F8] px-4 pb-5 pt-4 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",
].join(" ")}
>
<div className="mx-auto flex flex-col items-center">
<h2 className="w-full text-[14px] leading-[1.3] font-bold tracking-[-0.02em] text-[#171717]">
{resolvedTitle}
</h2>
{(() => {
const parseBoldText = (text: string) => {
const parts = text.split(/(\*\*[^*]+\*\*)/g);
return parts.map((part, index) => {
if (part.startsWith("**") && part.endsWith("**")) {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: parts array is static
<strong key={index} className="font-bold text-[#171717]">
{part.slice(2, -2)}
</strong>
);
}
return part;
});
};
const items = Array.isArray(resolvedDescription)
? resolvedDescription
: typeof resolvedDescription === "string" &&
resolvedDescription.includes("\n")
? resolvedDescription
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean)
: null;
if (items && Array.isArray(items)) {
return (
<ul className="mt-3.5 w-full text-[12px] leading-[1.6] text-[#4D4D4D] text-start space-y-2">
{items.map((item, i) => {
let cleanItem = item.trim();
let isHeader = false;
if (cleanItem.startsWith("###")) {
cleanItem = cleanItem.replace(/^###\s*/, "");
isHeader = true;
} else {
cleanItem = cleanItem.replace(/^[*+-]\s*/, "");
}
if (isHeader) {
return (
<li
// biome-ignore lint/suspicious/noArrayIndexKey: list is static
key={i}
className="block w-full font-bold text-[#171717] mt-3 first:mt-0 text-[13px]"
>
{parseBoldText(cleanItem)}
</li>
);
}
return (
// biome-ignore lint/suspicious/noArrayIndexKey: list is static
<li key={i} className="flex items-start gap-2.5 w-full">
<span className="shrink-0 mt-1.5 size-1.5 rounded-full bg-[#4D4D4D]" />
<span className="flex-1 min-w-0">
{parseBoldText(cleanItem)}
</span>
</li>
);
})}
</ul>
);
}
return (
<div className="mt-3.5 w-full text-[12px] leading-[1.6] text-[#4D4D4D] text-start">
{typeof resolvedDescription === "string"
? parseBoldText(resolvedDescription)
: resolvedDescription}
</div>
);
})()}
<div className="mt-5 w-full">
<Button onClick={closeSheet}>{resolvedButtonText}</Button>
</div>
</div>
</section>
</div>,
document.body,
);
}
export default HelpModal;