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
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;
|