forked from Tadrisino/MathApp_frontend
- Text: kasra/fatha/damma/shadda/sukun/hamza-above and «هی» ezafe removed from every user-visible string (comments untouched); tanwin kept. Stale copy fixed (question packs are no longer sold), awkward onboarding sentences rewritten, سؤال → سوال. plainPersian() in MathText cleans the live tutor stream. - Home: the mascot + speech bubble header is gone; the selected companion hops beside the current island and sits at every third bend (CSS transform animations only). - Checkout: order summary, payment-method rows with Zarinpal/Bazaar icons and radio, amount on the pay button, lock trust line, darker overlay. paymentMethods in config.js per channel (web: zarinpal, app: zarinpal+bazaar by default). - Profile: «ورود با حساب دیگر» removed (it was logout); new «حساب کاربری» sheet shows phone/ID and holds «حذف حساب کاربری»; logout is a red row. Delete sheet tells Bazaar subscribers to cancel renewal in Bazaar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import { useEffect, type ReactNode } from 'react';
|
|
import { AnimatePresence, motion, useDragControls } from 'motion/react';
|
|
import { X } from 'lucide-react';
|
|
import { useBackStep } from '@/nav/Navigator';
|
|
|
|
/**
|
|
* برگهای که از پایین بالا میآید؛ با زدنِ پسزمینه، Escape، یا کشیدنِ دستگیره/سرِ برگه به پایین بسته میشود.
|
|
* کشیدن فقط از بالای برگه شروع میشود تا با اسکرولِ محتوای بلندِ برگه قاطی نشود.
|
|
*/
|
|
export function Sheet({
|
|
open,
|
|
onClose,
|
|
title,
|
|
children,
|
|
}: {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
title?: ReactNode;
|
|
children: ReactNode;
|
|
}) {
|
|
const drag = useDragControls();
|
|
// دکمهی برگشتِ گوشی اول برگه را میبندد، نه صفحهی زیرش را.
|
|
useBackStep(open, onClose);
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [open, onClose]);
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{open && (
|
|
<div className="absolute inset-0 z-50 flex flex-col justify-end">
|
|
<motion.div
|
|
className="absolute inset-0 bg-[#14142a]/55"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={onClose}
|
|
/>
|
|
<motion.div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
className="relative max-h-[85%] overflow-y-auto rounded-t-[28px] bg-white px-5 pt-3"
|
|
style={{ paddingBottom: 'calc(2rem + env(safe-area-inset-bottom))' }}
|
|
initial={{ y: '100%' }}
|
|
animate={{ y: 0 }}
|
|
exit={{ y: '100%' }}
|
|
transition={{ type: 'spring', stiffness: 320, damping: 32 }}
|
|
drag="y"
|
|
dragControls={drag}
|
|
dragListener={false}
|
|
dragConstraints={{ top: 0, bottom: 0 }}
|
|
dragElastic={{ top: 0, bottom: 0.9 }}
|
|
onDragEnd={(_, info) => {
|
|
if (info.offset.y > 90 || info.velocity.y > 600) onClose();
|
|
}}
|
|
>
|
|
<div className="-mx-5 -mt-3 cursor-grab touch-none px-5 pb-3 pt-3" onPointerDown={(e) => drag.start(e)}>
|
|
<div className="mx-auto h-1.5 w-10 rounded-full bg-line" />
|
|
</div>
|
|
{title && (
|
|
<div className="mb-4 flex touch-none items-center justify-between gap-3" onPointerDown={(e) => drag.start(e)}>
|
|
<h2 className="text-base font-black text-ink">{title}</h2>
|
|
<button type="button" aria-label="بستن" onClick={onClose} className="rounded-full bg-soft p-1.5 text-muted">
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{children}
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|