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.
 
 
 
 
 

270 lines
9.2 KiB

"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { CURRENCIES, type CurrencyItem } from "@/data/currencies";
import { useI18n } from "@/translations/provider";
import { useSheetScrollLock } from "./use-sheet-scroll-lock";
const EXIT_ANIMATION_MS = 200;
export type CurrencySheetProps = {
isOpen: boolean;
onClose: () => void;
selectedCurrency: string;
onSelectCurrency: (code: string) => void;
title?: string;
};
export function CurrencySheet({
isOpen,
onClose,
selectedCurrency,
onSelectCurrency,
title,
}: CurrencySheetProps) {
const { dictionary: t, locale } = useI18n();
const [isClosing, setIsClosing] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const listRef = useRef<HTMLDivElement>(null);
const sheetRef = useRef<HTMLElement>(null);
const isRtl = locale === "fa" || locale === "ar" || locale === "ur";
const defaultTitle = isRtl ? "انتخاب ارز" : "Select Currency";
const sheetTitle = title || defaultTitle;
const closeSheet = useCallback(() => {
setIsClosing(true);
window.setTimeout(() => {
onClose();
setIsClosing(false);
setSearchQuery("");
}, EXIT_ANIMATION_MS);
}, [onClose]);
useSheetScrollLock(isOpen && !isClosing, { onBack: closeSheet });
// Escape key handler
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
closeSheet();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, closeSheet]);
const filteredCurrencies = useMemo(() => {
const q = searchQuery.toLowerCase().trim();
if (!q) return CURRENCIES;
return CURRENCIES.filter(
(c) =>
c.code.toLowerCase().includes(q) ||
c.nameEn.toLowerCase().includes(q) ||
c.nameFa.toLowerCase().includes(q) ||
(c.symbol && c.symbol.toLowerCase().includes(q)),
);
}, [searchQuery]);
const handleSelect = (code: string) => {
onSelectCurrency(code);
closeSheet();
};
const searchPlaceholder =
locale === "fa"
? "جستجوی ارز یا کشور..."
: locale === "ar"
? "بحث عن العملة..."
: "Search currency or country...";
const noResultsText =
locale === "fa"
? "ارزی یافت نشد"
: locale === "ar"
? "لم يتم العثور على عملات"
: "No currencies found";
if (!isOpen && !isClosing) return null;
return createPortal(
<div
className={[
"fixed inset-0 z-50 flex items-end justify-center",
isClosing ? "flutter-scrim-exit" : "flutter-scrim",
].join(" ")}
role="dialog"
aria-label={sheetTitle}
onKeyDown={(e) => {
if (e.key === "Escape") closeSheet();
}}
onClick={(event) => {
if (event.target === event.currentTarget) closeSheet();
}}
>
<section
ref={sheetRef}
className={[
"flex w-full flex-col overflow-hidden rounded-t-[22px] bg-white pb-[env(safe-area-inset-bottom)] shadow-[0_-10px_32px_rgba(0,0,0,0.16)] sm:max-w-[420px] h-[82svh] min-h-[82svh] max-h-[82svh]",
isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface",
].join(" ")}
>
{/* Header */}
<div className="flex items-center justify-between px-5 pt-3 pb-3 border-b border-[#F2F4F7]">
<h3 className="text-[17px] font-bold text-[#181818] truncate pr-2">
{sheetTitle}
</h3>
<button
type="button"
onClick={closeSheet}
className="flex size-8 shrink-0 items-center justify-center rounded-full text-[#667085] hover:bg-[#F2F4F7] hover:text-[#181818] transition-colors cursor-pointer"
aria-label="Close"
>
<svg
aria-hidden="true"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
{/* Search Bar */}
<div className="px-5 pt-3.5 pb-2">
<div className="flex h-[46px] w-full items-center gap-2.5 rounded-[14px] bg-[#F2F4F7] px-3.5 transition-colors focus-within:bg-[#EAECF0]">
<svg
aria-hidden="true"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
className="shrink-0 text-[#667085]"
>
<path
d="M8.25 14.25C11.5637 14.25 14.25 11.5637 14.25 8.25C14.25 4.93629 11.5637 2.25 8.25 2.25C4.93629 2.25 2.25 4.93629 2.25 8.25C2.25 11.5637 4.93629 14.25 8.25 14.25Z"
stroke="#667085"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.75 15.75L12.5 12.5"
stroke="#667085"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<input
type="text"
name="currency_sheet_search"
autoComplete="off"
autoCorrect="off"
autoCapitalize="none"
spellCheck="false"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={searchPlaceholder}
className="flex-1 bg-transparent text-[14px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
/>
{searchQuery ? (
<button
type="button"
onClick={() => setSearchQuery("")}
className="text-[#667085] hover:text-[#181818] text-xs font-semibold p-1 cursor-pointer"
>
</button>
) : null}
</div>
</div>
{/* Currencies List */}
<div
ref={listRef}
onTouchStart={(event) => event.stopPropagation()}
onTouchMove={(event) => event.stopPropagation()}
onTouchEnd={(event) => event.stopPropagation()}
className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overscroll-contain px-5 py-3"
>
{filteredCurrencies.length > 0 ? (
filteredCurrencies.map((item: CurrencyItem) => {
const isSelected = item.code === selectedCurrency;
const displayName =
locale === "fa" || locale === "fa-ir"
? item.nameFa
: item.nameEn;
return (
<button
key={item.code}
type="button"
onClick={() => handleSelect(item.code)}
className={[
"flex min-h-[52px] w-full items-center justify-between gap-3 rounded-[14px] border px-3.5 py-2.5 text-start transition-all cursor-pointer",
isSelected
? "bg-[#FFF4F5] border-[#F0445B]/40 text-[#181818] shadow-xs"
: "bg-white hover:bg-[#F8F9FA] border-[#EAECF0] text-[#181818]",
].join(" ")}
>
<div className="flex items-center gap-3 min-w-0">
{/* Radio selection circle */}
<div
className={[
"size-[22px] shrink-0 rounded-full transition-all duration-150 flex items-center justify-center",
isSelected
? "border-[6px] border-[#F0445B] bg-white"
: "border-[2px] border-[#98A2B3] bg-white",
].join(" ")}
/>
{/* Currency name */}
<span
className={[
"text-[15px] leading-tight truncate min-w-0",
isSelected
? "font-bold text-[#181818]"
: "font-medium text-[#344054]",
].join(" ")}
>
{displayName}
</span>
</div>
{/* Currency code badge */}
<span
className={[
"shrink-0 px-2.5 py-1 rounded-[8px] text-[13px] font-bold tracking-wider uppercase font-mono",
isSelected
? "bg-[#F0445B] text-white"
: "bg-[#F2F4F7] text-[#475467]",
].join(" ")}
>
{item.code}
</span>
</button>
);
})
) : (
<div className="py-12 text-center text-[14px] text-[#667085]">
{noResultsText}
</div>
)}
</div>
</section>
</div>,
document.body,
);
}
export default CurrencySheet;