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.
 
 
 
 
 

307 lines
7.5 KiB

"use client";
import Image, { type StaticImageData } from "next/image";
import type { HTMLAttributes, ReactNode } from "react";
import { useEffect, useState } from "react";
import Button from "./button";
import { LoadingSkeleton } from "./loading-skeleton";
import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220;
type InformationSheetPresetIcon =
| "play"
| "warning"
| "coin"
| "check"
| "stash_play-solid.svg"
| "warning.svg"
| "coin.svg"
| "Vectorcheck.svg";
type InformationSheetIcon =
| InformationSheetPresetIcon
| string
| {
src: string | StaticImageData;
alt?: string;
width?: number;
height?: number;
};
export type InformationSheetProps = Omit<
HTMLAttributes<HTMLDivElement>,
"children" | "title"
> & {
icon?: InformationSheetIcon | null;
title?: ReactNode | ((controls: { close: () => void }) => ReactNode);
description?: ReactNode;
buttons?: ReactNode | ((controls: { close: () => void }) => ReactNode);
closeOnOutside?: boolean;
onClose?: () => void;
isLoading?: boolean;
};
const DEFAULT_ICON = {
src: "/assets/images/stash_play-solid.svg",
alt: "Play",
width: 50,
height: 50,
} as const;
const ICON_PRESETS: Record<
InformationSheetPresetIcon,
{
src: string;
alt: string;
width: number;
height: number;
}
> = {
play: DEFAULT_ICON,
"stash_play-solid.svg": DEFAULT_ICON,
warning: {
src: "/assets/images/Vector.svg",
alt: "Warning",
width: 36,
height: 36,
},
"warning.svg": {
src: "/assets/images/Vector.svg",
alt: "Warning",
width: 36,
height: 36,
},
coin: {
src: "/assets/images/Inner Plugdsain Iframe.svg",
alt: "Coin",
width: 50,
height: 50,
},
"coin.svg": {
src: "/assets/images/Inner Plugdsain Iframe.svg",
alt: "Coin",
width: 50,
height: 50,
},
check: {
src: "/assets/images/Vectofdasr.svg",
alt: "Check",
width: 36,
height: 36,
},
"Vectorcheck.svg": {
src: "/assets/images/Vectorcheck.svg",
alt: "Check",
width: 36,
height: 36,
},
};
function resolveIcon(icon: InformationSheetIcon | null | undefined) {
if (icon === null) {
return null;
}
if (!icon) {
return DEFAULT_ICON;
}
if (typeof icon === "string") {
return (
ICON_PRESETS[icon as InformationSheetPresetIcon] ?? {
src: icon,
alt: "Information",
width: DEFAULT_ICON.width,
height: DEFAULT_ICON.height,
}
);
}
return {
src: icon.src,
alt: icon.alt ?? "Information",
width: icon.width ?? DEFAULT_ICON.width,
height: icon.height ?? DEFAULT_ICON.height,
};
}
export function InformationSheet({
icon,
title,
description,
buttons,
closeOnOutside = true,
onClose,
className,
isLoading = false,
...props
}: InformationSheetProps) {
const { locale, dictionary: t } = useI18n();
const isRtl =
locale === "fa" ||
locale === "ar" ||
locale === "ur" ||
locale === "he" ||
locale === "ks";
const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false);
const resolvedIcon = resolveIcon(icon);
const closeSheet = () => {
if (isClosing) {
return;
}
setIsClosing(true);
};
const controls = { close: closeSheet };
const resolvedTitle = typeof title === "function" ? title(controls) : title;
const resolvedButtons =
typeof buttons === "string" ? (
<Button onClick={closeSheet}>{buttons}</Button>
) : typeof buttons === "function" ? (
buttons(controls)
) : (
buttons
);
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;
}
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["Information sheet"]}
tabIndex={-1}
dir={isRtl ? "rtl" : "ltr"}
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-[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",
isClosing || isEntering ? "translate-y-full" : "translate-y-0",
className,
]
.filter(Boolean)
.join(" ")}
>
<div className="mx-auto flex flex-col items-center w-full">
{isLoading ? (
<>
<LoadingSkeleton className="h-[50px] w-[50px] rounded-full" />
<LoadingSkeleton className="mt-3.5 h-[20px] w-[140px]" />
<div className="mt-3 flex flex-col items-center gap-2 w-full">
<LoadingSkeleton className="h-[12px] w-[220px]" />
<LoadingSkeleton className="h-[12px] w-[160px]" />
</div>
<div className="mt-4 flex w-full gap-3">
<LoadingSkeleton className="h-[52px] flex-1 rounded-[11px]" />
</div>
</>
) : (
<>
{resolvedIcon ? (
<Image
src={resolvedIcon.src}
alt={resolvedIcon.alt}
width={resolvedIcon.width}
height={resolvedIcon.height}
priority
/>
) : null}
<h2
className={[
resolvedIcon ? "mt-2.5" : "mt-0",
"w-full group-16 leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]",
].join(" ")}
>
{resolvedTitle}
</h2>
{description ? (
<div className="mt-3 group-12 text-[#4D4D4D]">{description}</div>
) : null}
{resolvedButtons ? (
<div className="mt-4 flex w-full flex-col gap-3">
{resolvedButtons}
</div>
) : null}
</>
)}
</div>
</section>
</div>
);
}
export default InformationSheet;