From 840c328819c6e5edd91204c3689c99682031f727 Mon Sep 17 00:00:00 2001 From: alisaza Date: Sat, 5 Sep 2026 16:40:50 +0330 Subject: [PATCH] refactor: remove unused icons and dependencies - Deleted several unused icon components including AddDocumentIcon, ArrowRightIcon, AttachIcon, and others to clean up the codebase. - Removed the 'arcaptcha-react' package from package.json and next.config.js as it is no longer needed. - This cleanup helps improve project maintainability and reduces bundle size. --- components/icons/AddDocumentIcon.tsx | 21 - components/icons/ArrowRightIcon.tsx | 16 - components/icons/AttachIcon.tsx | 19 - components/icons/AwardIcon.tsx | 25 - components/icons/BookmarkFillIcon.tsx | 13 - components/icons/CalendarFillIcon.tsx | 2593 ----------------- components/icons/CalendarSecondaryIcon.tsx | 29 - components/icons/ClockFillIcon.tsx | 25 - components/icons/CloseSquareIcon.tsx | 17 - components/icons/ContactChannelIcon.tsx | 182 -- components/icons/CopyIcon.tsx | 15 - components/icons/FaqQuestionMarkIcon.tsx | 24 - components/icons/FireIcon.tsx | 95 - components/icons/GoogleMapsLogoIcon.tsx | 31 - components/icons/HeartIcon.tsx | 19 - components/icons/HeatIcon.tsx | 17 - components/icons/HomeFillIcon.tsx | 15 - components/icons/HomeHashtagTwoToneIcon.tsx | 56 - components/icons/HomeTextIcon.tsx | 334 --- components/icons/IdentityIcon.tsx | 94 - components/icons/InfoIcon.tsx | 25 - components/icons/LinkIcon.tsx | 15 - components/icons/LoginTwoToneIcon.tsx | 45 - components/icons/LogoIcon.tsx | 119 - components/icons/MessageIcon.tsx | 29 - components/icons/MessagesTwoToneIcon.tsx | 37 - components/icons/MuteIcon.tsx | 19 - components/icons/NeshanLogoIcon.tsx | 35 - components/icons/OverviewIcon.tsx | 37 - components/icons/PaymentCardIcon.tsx | 19 - components/icons/QuestionMarkIcon.tsx | 17 - components/icons/SemiColonIcon.tsx | 234 -- components/icons/SendIcon.tsx | 13 - components/icons/SettingsIcon.tsx | 16 - components/icons/ShareIcon.tsx | 26 - components/icons/TagIcon.tsx | 23 - components/icons/TicketDiscountIcon.tsx | 17 - .../icons/TicketDiscountTwoToneIcon.tsx | 56 - components/icons/TicketStarIcon.tsx | 21 - components/icons/TriangleLeftIcon.tsx | 15 - components/icons/UserPlusIcon.tsx | 23 - components/icons/UserTwoToneIcon.tsx | 37 - components/icons/UsersIcon.tsx | 15 - components/icons/VerifiedBadgeIcon.tsx | 28 - components/icons/VerticalDotsIcon.tsx | 34 - components/icons/WalletIcon.tsx | 85 - components/reviews/StarRating.tsx | 69 - config/enamad.ts | 10 - constants/contact.ts | 18 - hooks/useCopyToClipboard.ts | 26 - hooks/useDeferredClientMount.ts | 26 - lib/bookingPayment.ts | 19 - lib/chatSocket.ts | 132 - lib/clientSessionHint.ts | 14 - lib/paymentQuery.ts | 25 - lib/server/eventManagementBootstrap.ts | 30 - next.config.js | 2 +- package.json | 1 - services/chat.ts | 354 --- services/contactMessages.ts | 62 - services/identity.ts | 165 -- services/myEvents.ts | 43 - services/notifications.ts | 84 - services/payments.ts | 189 -- services/settlements.ts | 27 - services/userBlocks.ts | 117 - services/userReports.ts | 131 - services/withdrawals.ts | 146 - validation/identity.ts | 43 - validation/profile.ts | 20 - 70 files changed, 1 insertion(+), 6432 deletions(-) delete mode 100644 components/icons/AddDocumentIcon.tsx delete mode 100644 components/icons/ArrowRightIcon.tsx delete mode 100644 components/icons/AttachIcon.tsx delete mode 100644 components/icons/AwardIcon.tsx delete mode 100644 components/icons/BookmarkFillIcon.tsx delete mode 100644 components/icons/CalendarFillIcon.tsx delete mode 100644 components/icons/CalendarSecondaryIcon.tsx delete mode 100644 components/icons/ClockFillIcon.tsx delete mode 100644 components/icons/CloseSquareIcon.tsx delete mode 100644 components/icons/ContactChannelIcon.tsx delete mode 100644 components/icons/CopyIcon.tsx delete mode 100644 components/icons/FaqQuestionMarkIcon.tsx delete mode 100644 components/icons/FireIcon.tsx delete mode 100644 components/icons/GoogleMapsLogoIcon.tsx delete mode 100644 components/icons/HeartIcon.tsx delete mode 100644 components/icons/HeatIcon.tsx delete mode 100644 components/icons/HomeFillIcon.tsx delete mode 100644 components/icons/HomeHashtagTwoToneIcon.tsx delete mode 100644 components/icons/HomeTextIcon.tsx delete mode 100644 components/icons/IdentityIcon.tsx delete mode 100644 components/icons/InfoIcon.tsx delete mode 100644 components/icons/LinkIcon.tsx delete mode 100644 components/icons/LoginTwoToneIcon.tsx delete mode 100644 components/icons/LogoIcon.tsx delete mode 100644 components/icons/MessageIcon.tsx delete mode 100644 components/icons/MessagesTwoToneIcon.tsx delete mode 100644 components/icons/MuteIcon.tsx delete mode 100644 components/icons/NeshanLogoIcon.tsx delete mode 100644 components/icons/OverviewIcon.tsx delete mode 100644 components/icons/PaymentCardIcon.tsx delete mode 100644 components/icons/QuestionMarkIcon.tsx delete mode 100644 components/icons/SemiColonIcon.tsx delete mode 100644 components/icons/SendIcon.tsx delete mode 100644 components/icons/SettingsIcon.tsx delete mode 100644 components/icons/ShareIcon.tsx delete mode 100644 components/icons/TagIcon.tsx delete mode 100644 components/icons/TicketDiscountIcon.tsx delete mode 100644 components/icons/TicketDiscountTwoToneIcon.tsx delete mode 100644 components/icons/TicketStarIcon.tsx delete mode 100644 components/icons/TriangleLeftIcon.tsx delete mode 100644 components/icons/UserPlusIcon.tsx delete mode 100644 components/icons/UserTwoToneIcon.tsx delete mode 100644 components/icons/UsersIcon.tsx delete mode 100644 components/icons/VerifiedBadgeIcon.tsx delete mode 100644 components/icons/VerticalDotsIcon.tsx delete mode 100644 components/icons/WalletIcon.tsx delete mode 100644 components/reviews/StarRating.tsx delete mode 100644 config/enamad.ts delete mode 100644 constants/contact.ts delete mode 100644 hooks/useCopyToClipboard.ts delete mode 100644 hooks/useDeferredClientMount.ts delete mode 100644 lib/bookingPayment.ts delete mode 100644 lib/chatSocket.ts delete mode 100644 lib/clientSessionHint.ts delete mode 100644 lib/paymentQuery.ts delete mode 100644 lib/server/eventManagementBootstrap.ts delete mode 100644 services/chat.ts delete mode 100644 services/contactMessages.ts delete mode 100644 services/identity.ts delete mode 100644 services/myEvents.ts delete mode 100644 services/notifications.ts delete mode 100644 services/payments.ts delete mode 100644 services/settlements.ts delete mode 100644 services/userBlocks.ts delete mode 100644 services/userReports.ts delete mode 100644 services/withdrawals.ts delete mode 100644 validation/identity.ts delete mode 100644 validation/profile.ts diff --git a/components/icons/AddDocumentIcon.tsx b/components/icons/AddDocumentIcon.tsx deleted file mode 100644 index 7e42910..0000000 --- a/components/icons/AddDocumentIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export default function AddDocumentIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - - ) -} diff --git a/components/icons/ArrowRightIcon.tsx b/components/icons/ArrowRightIcon.tsx deleted file mode 100644 index 541514f..0000000 --- a/components/icons/ArrowRightIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export default function ArrowRightIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/AttachIcon.tsx b/components/icons/AttachIcon.tsx deleted file mode 100644 index b417e56..0000000 --- a/components/icons/AttachIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export default function AttachIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - - ) -} diff --git a/components/icons/AwardIcon.tsx b/components/icons/AwardIcon.tsx deleted file mode 100644 index 7ef415f..0000000 --- a/components/icons/AwardIcon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export default function AwardIcon({ className = 'h-[15px] w-[10px]' }: { className?: string }) { - return ( - - - - - - ) -} diff --git a/components/icons/BookmarkFillIcon.tsx b/components/icons/BookmarkFillIcon.tsx deleted file mode 100644 index eda806e..0000000 --- a/components/icons/BookmarkFillIcon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export default function BookmarkFillIcon({ className = 'size-6' }: { className?: string }) { - return ( - - ) -} diff --git a/components/icons/CalendarFillIcon.tsx b/components/icons/CalendarFillIcon.tsx deleted file mode 100644 index 462807f..0000000 --- a/components/icons/CalendarFillIcon.tsx +++ /dev/null @@ -1,2593 +0,0 @@ -export default function CalendarFillIcon({ className = 'size-[40px]' }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/components/icons/CalendarSecondaryIcon.tsx b/components/icons/CalendarSecondaryIcon.tsx deleted file mode 100644 index 1c1794f..0000000 --- a/components/icons/CalendarSecondaryIcon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -export default function CalendarSecondary({ className = 'size-6' }) { - return ( - - - - - - - ) -} diff --git a/components/icons/ClockFillIcon.tsx b/components/icons/ClockFillIcon.tsx deleted file mode 100644 index a585971..0000000 --- a/components/icons/ClockFillIcon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export default function ClockIcon({ className = 'size-6' }) { - return ( - - - - - - ) -} diff --git a/components/icons/CloseSquareIcon.tsx b/components/icons/CloseSquareIcon.tsx deleted file mode 100644 index e55a7ec..0000000 --- a/components/icons/CloseSquareIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default function CloseSquareIcon({ className = 'size-[12px]' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/ContactChannelIcon.tsx b/components/icons/ContactChannelIcon.tsx deleted file mode 100644 index ac8e76e..0000000 --- a/components/icons/ContactChannelIcon.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import type { ReactNode, SVGProps } from 'react' - -type IconProps = SVGProps - -const baseProps = (className?: string): IconProps => ({ - className, - fill: 'none', - height: 24, - viewBox: '0 0 24 24', - width: 24, - xmlns: 'http://www.w3.org/2000/svg', - 'aria-hidden': true, -}) - -const InstagramChannelIcon = ({ className }: { className?: string }) => ( - - - - - - -) - -const TelegramChannelIcon = ({ className }: { className?: string }) => ( - - - - -) - -const TwitterChannelIcon = ({ className }: { className?: string }) => ( - - - - -) - -const LinkedinChannelIcon = ({ className }: { className?: string }) => ( - - - - -) - -const YoutubeChannelIcon = ({ className }: { className?: string }) => ( - - - - -) - -const WebsiteChannelIcon = ({ className }: { className?: string }) => ( - - - - - -) - -const SupportPhoneChannelIcon = ({ className }: { className?: string }) => ( - - - - -) - -const CHANNEL_ICONS: Record ReactNode> = { - instagram: InstagramChannelIcon, - telegram: TelegramChannelIcon, - twitter: TwitterChannelIcon, - linkedin: LinkedinChannelIcon, - youtube: YoutubeChannelIcon, - website: WebsiteChannelIcon, - support_phone: SupportPhoneChannelIcon, -} - -interface ContactChannelIconProps { - channel: string - className?: string -} - -const ContactChannelIcon = ({ channel, className = 'size-6 text-primary' }: ContactChannelIconProps) => { - const Icon = CHANNEL_ICONS[channel] ?? WebsiteChannelIcon - - return -} - -export default ContactChannelIcon diff --git a/components/icons/CopyIcon.tsx b/components/icons/CopyIcon.tsx deleted file mode 100644 index f3d2611..0000000 --- a/components/icons/CopyIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function CopyIcon({ className = 'size-4' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/FaqQuestionMarkIcon.tsx b/components/icons/FaqQuestionMarkIcon.tsx deleted file mode 100644 index fbfbf47..0000000 --- a/components/icons/FaqQuestionMarkIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** Orange «؟» mark used on consumer event FAQ rows. */ -export default function FaqQuestionMarkIcon({ className = 'size-5' }: { className?: string }) { - return ( - - - - - ) -} diff --git a/components/icons/FireIcon.tsx b/components/icons/FireIcon.tsx deleted file mode 100644 index 308f7b9..0000000 --- a/components/icons/FireIcon.tsx +++ /dev/null @@ -1,95 +0,0 @@ -export default function FireIcon({ className = 'size-[120px]' }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/components/icons/GoogleMapsLogoIcon.tsx b/components/icons/GoogleMapsLogoIcon.tsx deleted file mode 100644 index ac8dad3..0000000 --- a/components/icons/GoogleMapsLogoIcon.tsx +++ /dev/null @@ -1,31 +0,0 @@ -export default function GoogleMapsLogoIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - - - - - ) -} diff --git a/components/icons/HeartIcon.tsx b/components/icons/HeartIcon.tsx deleted file mode 100644 index d99afc1..0000000 --- a/components/icons/HeartIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export default function HeartIcon({ className = 'size-6' }) { - return ( - - - - ) -} diff --git a/components/icons/HeatIcon.tsx b/components/icons/HeatIcon.tsx deleted file mode 100644 index 161973e..0000000 --- a/components/icons/HeatIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default function HeatIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/HomeFillIcon.tsx b/components/icons/HomeFillIcon.tsx deleted file mode 100644 index 1b9f1d5..0000000 --- a/components/icons/HomeFillIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function HomeFillIcon({ className = 'size-[21px]' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/HomeHashtagTwoToneIcon.tsx b/components/icons/HomeHashtagTwoToneIcon.tsx deleted file mode 100644 index cbe32ea..0000000 --- a/components/icons/HomeHashtagTwoToneIcon.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { cn } from '@/lib/cn' - -interface HomeHashtagTwoToneIconProps { - className?: string - /** رنگ لایه اصلی — مثلاً `text-gray-800` */ - primaryClassName?: string - /** رنگ لایه ثانویه — مثلاً `text-primary`؛ پیش‌فرض opacity-40 مثل Iconsax */ - secondaryClassName?: string -} - -export default function HomeHashtagTwoToneIcon({ - className = 'size-6', - primaryClassName, - secondaryClassName, -}: HomeHashtagTwoToneIconProps) { - return ( - - - - - - - - - ) -} diff --git a/components/icons/HomeTextIcon.tsx b/components/icons/HomeTextIcon.tsx deleted file mode 100644 index b578b9e..0000000 --- a/components/icons/HomeTextIcon.tsx +++ /dev/null @@ -1,334 +0,0 @@ -export default function HomeTextIcon({ className = 'size-[300px]' }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/components/icons/IdentityIcon.tsx b/components/icons/IdentityIcon.tsx deleted file mode 100644 index ff7e05f..0000000 --- a/components/icons/IdentityIcon.tsx +++ /dev/null @@ -1,94 +0,0 @@ -export default function IdentityIcon() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/components/icons/InfoIcon.tsx b/components/icons/InfoIcon.tsx deleted file mode 100644 index 6f70979..0000000 --- a/components/icons/InfoIcon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export default function InfoIcon({ className = 'size-[15px]' }: { className?: string }) { - return ( - - - - - - ) -} diff --git a/components/icons/LinkIcon.tsx b/components/icons/LinkIcon.tsx deleted file mode 100644 index 8473046..0000000 --- a/components/icons/LinkIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function LinkIcon({ className = 'size-5' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/LoginTwoToneIcon.tsx b/components/icons/LoginTwoToneIcon.tsx deleted file mode 100644 index f92302c..0000000 --- a/components/icons/LoginTwoToneIcon.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { cn } from '@/lib/cn' - -interface LoginTwoToneIconProps { - className?: string - /** رنگ لایه اصلی — مثلاً `text-gray-800` */ - primaryClassName?: string - /** رنگ لایه ثانویه — مثلاً `text-primary`؛ پیش‌فرض opacity-40 مثل Iconsax */ - secondaryClassName?: string -} - -export default function LoginTwoToneIcon({ className = 'size-6', primaryClassName, secondaryClassName }: LoginTwoToneIconProps) { - return ( - - - - - - - - ) -} diff --git a/components/icons/LogoIcon.tsx b/components/icons/LogoIcon.tsx deleted file mode 100644 index 830a843..0000000 --- a/components/icons/LogoIcon.tsx +++ /dev/null @@ -1,119 +0,0 @@ -export default function LogoIcon({ className = 'size-[200px]', color = 'currentColor' }: { className?: string; color?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/components/icons/MessageIcon.tsx b/components/icons/MessageIcon.tsx deleted file mode 100644 index 7fe204b..0000000 --- a/components/icons/MessageIcon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -export default function MessageIcon({ className = 'size-[21.5px]' }: { className?: string }) { - return ( - - - - - - - ) -} diff --git a/components/icons/MessagesTwoToneIcon.tsx b/components/icons/MessagesTwoToneIcon.tsx deleted file mode 100644 index 79adc61..0000000 --- a/components/icons/MessagesTwoToneIcon.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { cn } from '@/lib/cn' - -interface MessagesTwoToneIconProps { - className?: string - /** رنگ لایه اصلی — مثلاً `text-gray-800` */ - primaryClassName?: string - /** رنگ لایه ثانویه — مثلاً `text-primary`؛ پیش‌فرض opacity-40 مثل Iconsax */ - secondaryClassName?: string -} - -export default function MessagesTwoToneIcon({ className = 'size-6', primaryClassName, secondaryClassName }: MessagesTwoToneIconProps) { - return ( - - - - - ) -} diff --git a/components/icons/MuteIcon.tsx b/components/icons/MuteIcon.tsx deleted file mode 100644 index 093432f..0000000 --- a/components/icons/MuteIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export default function MuteIcon({ className = 'size-6' }) { - return ( - - - - ) -} diff --git a/components/icons/NeshanLogoIcon.tsx b/components/icons/NeshanLogoIcon.tsx deleted file mode 100644 index 2c79de7..0000000 --- a/components/icons/NeshanLogoIcon.tsx +++ /dev/null @@ -1,35 +0,0 @@ -export default function NeshanLogoIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - - - - - - ) -} diff --git a/components/icons/OverviewIcon.tsx b/components/icons/OverviewIcon.tsx deleted file mode 100644 index ec78fd0..0000000 --- a/components/icons/OverviewIcon.tsx +++ /dev/null @@ -1,37 +0,0 @@ -export default function OverviewIcon({ className = 'size-6' }) { - return ( - - - - - - - ) -} diff --git a/components/icons/PaymentCardIcon.tsx b/components/icons/PaymentCardIcon.tsx deleted file mode 100644 index 9bdbc07..0000000 --- a/components/icons/PaymentCardIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export default function PaymentCardIcon({ className = 'size-4' }: { className?: string }) { - return ( - - - - - ) -} diff --git a/components/icons/QuestionMarkIcon.tsx b/components/icons/QuestionMarkIcon.tsx deleted file mode 100644 index cb1de9f..0000000 --- a/components/icons/QuestionMarkIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default function QuestionMarkIcon({ className = 'size-6' }) { - return ( - - - - ) -} diff --git a/components/icons/SemiColonIcon.tsx b/components/icons/SemiColonIcon.tsx deleted file mode 100644 index a84dcb3..0000000 --- a/components/icons/SemiColonIcon.tsx +++ /dev/null @@ -1,234 +0,0 @@ -export default function SemiColonIcon() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/components/icons/SendIcon.tsx b/components/icons/SendIcon.tsx deleted file mode 100644 index 91a8c90..0000000 --- a/components/icons/SendIcon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export default function SendIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/SettingsIcon.tsx b/components/icons/SettingsIcon.tsx deleted file mode 100644 index 5813513..0000000 --- a/components/icons/SettingsIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export default function SettingsIcon({ className = 'size-5' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/ShareIcon.tsx b/components/icons/ShareIcon.tsx deleted file mode 100644 index 345c886..0000000 --- a/components/icons/ShareIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -export default function ShareIcon({ className = 'size-6' }: { className?: string }) { - return ( - - - - - - - - - - - ) -} diff --git a/components/icons/TagIcon.tsx b/components/icons/TagIcon.tsx deleted file mode 100644 index b4a92c5..0000000 --- a/components/icons/TagIcon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -export default function TagIcon({ className = 'size-4' }: { className?: string }) { - return ( - - - - - ) -} diff --git a/components/icons/TicketDiscountIcon.tsx b/components/icons/TicketDiscountIcon.tsx deleted file mode 100644 index d773176..0000000 --- a/components/icons/TicketDiscountIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default function TicketStarIcon({ className = 'h-[17.5px] w-[21.5px]' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/TicketDiscountTwoToneIcon.tsx b/components/icons/TicketDiscountTwoToneIcon.tsx deleted file mode 100644 index 9be9792..0000000 --- a/components/icons/TicketDiscountTwoToneIcon.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { cn } from '@/lib/cn' - -interface TicketDiscountTwoToneIconProps { - className?: string - /** رنگ لایه اصلی — مثلاً `text-gray-800` */ - primaryClassName?: string - /** رنگ لایه ثانویه — مثلاً `text-primary`؛ پیش‌فرض opacity-40 مثل Iconsax */ - secondaryClassName?: string -} - -export default function TicketDiscountTwoToneIcon({ - className = 'size-6', - primaryClassName, - secondaryClassName, -}: TicketDiscountTwoToneIconProps) { - return ( - - - - - - - - - ) -} diff --git a/components/icons/TicketStarIcon.tsx b/components/icons/TicketStarIcon.tsx deleted file mode 100644 index a45b0e5..0000000 --- a/components/icons/TicketStarIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export default function TicketStarIcon({ className = 'h-[17.5px] w-[21.5px]' }: { className?: string }) { - return ( - - - - - ) -} diff --git a/components/icons/TriangleLeftIcon.tsx b/components/icons/TriangleLeftIcon.tsx deleted file mode 100644 index d2d753f..0000000 --- a/components/icons/TriangleLeftIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function TriangleLeftIcon({ className = 'size-3' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/UserPlusIcon.tsx b/components/icons/UserPlusIcon.tsx deleted file mode 100644 index d9f7c27..0000000 --- a/components/icons/UserPlusIcon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -export default function UserPlusIcon({ className = 'size-4' }: { className?: string }) { - return ( - - - - - - ) -} diff --git a/components/icons/UserTwoToneIcon.tsx b/components/icons/UserTwoToneIcon.tsx deleted file mode 100644 index 7ec1e2f..0000000 --- a/components/icons/UserTwoToneIcon.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { cn } from '@/lib/cn' - -interface UserTwoToneIconProps { - className?: string - /** رنگ لایه اصلی — مثلاً `text-gray-800` */ - primaryClassName?: string - /** رنگ لایه ثانویه — مثلاً `text-primary`؛ پیش‌فرض opacity-40 مثل Iconsax */ - secondaryClassName?: string -} - -export default function UserTwoToneIcon({ className = 'size-6', primaryClassName, secondaryClassName }: UserTwoToneIconProps) { - return ( - - - - - ) -} diff --git a/components/icons/UsersIcon.tsx b/components/icons/UsersIcon.tsx deleted file mode 100644 index 6d50dbd..0000000 --- a/components/icons/UsersIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function UsersIcon({ className = 'size-4' }: { className?: string }) { - return ( - - - - ) -} diff --git a/components/icons/VerifiedBadgeIcon.tsx b/components/icons/VerifiedBadgeIcon.tsx deleted file mode 100644 index ddf0902..0000000 --- a/components/icons/VerifiedBadgeIcon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export default function VerifiedBadgeIcon({ className = 'size-5' }: { className?: string }) { - return ( - - - - - - - - - - - ) -} diff --git a/components/icons/VerticalDotsIcon.tsx b/components/icons/VerticalDotsIcon.tsx deleted file mode 100644 index ba91e07..0000000 --- a/components/icons/VerticalDotsIcon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -export default function VerticalDotsIcon({ className = 'size-6' }) { - return ( - - - - - - - - - - - - - ) -} diff --git a/components/icons/WalletIcon.tsx b/components/icons/WalletIcon.tsx deleted file mode 100644 index 84376d6..0000000 --- a/components/icons/WalletIcon.tsx +++ /dev/null @@ -1,85 +0,0 @@ -export default function AngleDownIcon({ className = 'size-10' }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/components/reviews/StarRating.tsx b/components/reviews/StarRating.tsx deleted file mode 100644 index f2b40b1..0000000 --- a/components/reviews/StarRating.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' - -import Button from '@/components/formElements/Button' -import HeatIcon from '@/components/icons/HeatIcon' -import HeatIconFill from '@/components/icons/HeatIconFill' -import { cn } from '@/lib/cn' -import { texts, format } from '@/texts' - -interface StarRatingProps { - value: number - onChange?: (value: number) => void - size?: 'sm' | 'md' - className?: string - /** Read-only display when onChange is omitted. */ - label?: string -} - -const ICON_SIZE = { - sm: 'size-3.5', - md: 'size-5', -} as const - -const StarRating = ({ value, onChange, size = 'md', className, label }: StarRatingProps) => { - const interactive = typeof onChange === 'function' - const iconClass = ICON_SIZE[size] - - return ( -
- {[1, 2, 3, 4, 5].map((star) => { - const filled = star <= value - const Icon = filled ? HeatIconFill : HeatIcon - const colorClass = filled ? 'text-primary' : 'text-tertiary-900/35' - - if (!interactive) { - return ( - - ) - } - - return ( - - ) - })} -
- ) -} - -export default StarRating diff --git a/config/enamad.ts b/config/enamad.ts deleted file mode 100644 index 8047dd3..0000000 --- a/config/enamad.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** Enamad trust seal id (panel) — must match footer badge. */ -export const ENAMAD_ID = '7400030' - -/** Enamad trust seal code — used in footer links/images only (not the meta tag). */ -export const ENAMAD_CODE = 'TRxYXZLDTi8LQuOIZe8Z56sZ3pS3baMp' - -export const ENAMAD_URL = `https://trustseal.enamad.ir/?id=${ENAMAD_ID}&Code=${ENAMAD_CODE}` - -/** Self-hosted badge (avoids ORB/blocks on trustseal logo.aspx). Link still goes to ENAMAD_URL. */ -export const ENAMAD_IMAGE_URL = '/images/enamad.png' diff --git a/constants/contact.ts b/constants/contact.ts deleted file mode 100644 index 4f5f5d9..0000000 --- a/constants/contact.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Single source of truth for the platform's public contact number. - * Was previously hardcoded independently in 5 places (frontend/app/(public)/{layout.tsx,contact/page.tsx}, - * frontend/components/public/{ContactForm.tsx,PublicFooter.tsx}, frontend/e2e/shared/public-pages.spec.ts) - * -- a number change would have needed a coordinated grep-and-replace across all of them. - */ -export const SITE_PHONE = '+989153641196' - -/** Digits-only form, for `tel:`/`wa.me` links that don't want the leading `+`. */ -export const SITE_PHONE_DIGITS = SITE_PHONE.slice(1) - -export const SITE_WHATSAPP_URL = (text: string) => `https://wa.me/${SITE_PHONE_DIGITS}?text=${text}` - -/** Official support profiles — used in footer + Organization `sameAs`. */ -export const SITE_INSTAGRAM_URL = 'https://www.instagram.com/Ghabilee_support/' -export const SITE_TELEGRAM_URL = 'https://t.me/Ghabilee_support' - -export const SITE_SOCIAL_SAME_AS = [SITE_INSTAGRAM_URL, SITE_TELEGRAM_URL] as const diff --git a/hooks/useCopyToClipboard.ts b/hooks/useCopyToClipboard.ts deleted file mode 100644 index 9a5d8cc..0000000 --- a/hooks/useCopyToClipboard.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { addToast } from '@/lib/toast' - -/** - * Copies `text` to the clipboard and shows a success/failure toast. - * Was hand-copied (identical try/writeText/toast/catch shape) in - * BookingActionsModal.tsx#handleCopyCode and HostedEventActionsModal.tsx#handleShare — - * this is the shared home for any future "copy X" feature. - */ -const useCopyToClipboard = () => { - const copyToClipboard = async (text: string, messages: { successMessage: string; failureMessage: string }) => { - try { - await navigator.clipboard.writeText(text) - addToast({ title: messages.successMessage, color: 'success' }) - - return true - } catch { - addToast({ title: messages.failureMessage, color: 'danger' }) - - return false - } - } - - return { copyToClipboard } -} - -export default useCopyToClipboard diff --git a/hooks/useDeferredClientMount.ts b/hooks/useDeferredClientMount.ts deleted file mode 100644 index 805bf7d..0000000 --- a/hooks/useDeferredClientMount.ts +++ /dev/null @@ -1,26 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' - -/** Default delay — keeps deferred chrome out of the LCP window on cold loads. */ -export const DEFERRED_CHROME_DELAY_MS = 4000 - -/** - * Returns true after `delayMs` so non-critical client UI (PWA banner, etc.) mounts - * after the first paint / LCP candidates. - */ -export function useDeferredClientMount(delayMs = DEFERRED_CHROME_DELAY_MS) { - const [ready, setReady] = useState(false) - - useEffect(() => { - const timeoutId = window.setTimeout(() => { - setReady(true) - }, delayMs) - - return () => { - window.clearTimeout(timeoutId) - } - }, [delayMs]) - - return ready -} diff --git a/lib/bookingPayment.ts b/lib/bookingPayment.ts deleted file mode 100644 index 9a274db..0000000 --- a/lib/bookingPayment.ts +++ /dev/null @@ -1,19 +0,0 @@ -type BookingExpiresAt = string | Date | Record | null | undefined - -const bookingExpiresAtMs = (expiresAt: BookingExpiresAt): number | null => { - if (expiresAt == null) return null - if (typeof expiresAt === 'string' || expiresAt instanceof Date) { - const ms = new Date(expiresAt).getTime() - - return Number.isNaN(ms) ? null : ms - } - - return null -} - -/** Whether a pending_payment booking's hold window has passed (client clock). */ -export const isBookingPaymentExpired = (booking: { status: string; expiresAt?: BookingExpiresAt }): boolean => { - const expiresAtMs = bookingExpiresAtMs(booking.expiresAt) - - return booking.status === 'pending_payment' && expiresAtMs != null && expiresAtMs <= Date.now() -} diff --git a/lib/chatSocket.ts b/lib/chatSocket.ts deleted file mode 100644 index b3e99d3..0000000 --- a/lib/chatSocket.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { Socket } from 'socket.io-client' - -import { CHAT_SOCKET_NAMESPACE, CHAT_SOCKET_URL } from '@/config/chatSocket' - -let socket: Socket | null = null -let connectAbortNonce = 0 - -export interface ChatMessageEvent { - id: string - conversationId: string - senderId: string - senderFirstName: string | null - senderLastName: string | null - senderAvatarUrl?: string | null - senderGender?: string | null - body: string | null - imageUrl: string | null - createdAt: string -} - -export interface ConversationUpdatedEvent { - conversationId: string - lastMessageAt: string - preview: string | null - unreadCount?: number -} - -export const getChatSocket = () => socket - -/** - * Connect chat socket on-demand. - * - * Important: `socket.io-client` must be loaded lazily to keep anonymous - * guest first-load bundles small. - */ -export const connectChatSocket = async (accessToken: string): Promise => { - if (socket?.connected) { - // Keep the stored auth token current even while already connected: if - // this connection later drops (network blip, server restart) and - // socket.io-client auto-reconnects, it replays whatever is in - // `socket.auth` at that time — a stale token here would make every - // reconnect after the token's ~15min TTL fail silently. - socket.auth = { token: accessToken } - - return socket - } - - if (socket) { - socket.auth = { token: accessToken } - socket.connect() - - return socket - } - - const nonce = ++connectAbortNonce - const mod = await import('socket.io-client') - const { io } = mod - - // Another tab/navigation/token change can call disconnectChatSocket while - // this module is still loading. - if (nonce !== connectAbortNonce) { - throw new Error('CHAT_SOCKET_CONNECT_ABORTED') - } - - socket = io(`${CHAT_SOCKET_URL}${CHAT_SOCKET_NAMESPACE}`, { - transports: ['websocket'], - autoConnect: true, - auth: { token: accessToken }, - }) - - return socket -} - -export const disconnectChatSocket = () => { - if (!socket) return - - connectAbortNonce += 1 - socket.removeAllListeners() - socket.disconnect() - socket = null -} - -export interface JoinConversationAck { - ok: boolean - error?: string - conversationId?: string -} - -export const joinConversationRoom = (conversationId: string): Promise => { - const activeSocket = socket - - if (!activeSocket) return Promise.resolve({ ok: false, error: 'NOT_CONNECTED' }) - - return new Promise((resolve) => { - activeSocket.timeout(5000).emit('conversation:join', { conversationId }, (error: Error | null, ack?: JoinConversationAck) => { - if (error) { - resolve({ ok: false, error: 'ACK_TIMEOUT' }) - - return - } - - resolve(ack ?? { ok: false, error: 'NO_ACK' }) - }) - }) -} - -export const leaveConversationRoom = (conversationId: string) => { - socket?.emit('conversation:leave', { conversationId }) -} - -export const subscribeChatEvents = (handlers: { - onMessage?: (message: ChatMessageEvent) => void - onConversationUpdated?: (payload: ConversationUpdatedEvent) => void -}) => { - if (!socket) return () => undefined - - const messageHandler = (payload: { message: ChatMessageEvent }) => { - handlers.onMessage?.(payload.message) - } - - const conversationHandler = (payload: ConversationUpdatedEvent) => { - handlers.onConversationUpdated?.(payload) - } - - socket.on('message:new', messageHandler) - socket.on('conversation:updated', conversationHandler) - - return () => { - socket?.off('message:new', messageHandler) - socket?.off('conversation:updated', conversationHandler) - } -} diff --git a/lib/clientSessionHint.ts b/lib/clientSessionHint.ts deleted file mode 100644 index 2fff7cd..0000000 --- a/lib/clientSessionHint.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Cookies from 'js-cookie' - -/** - * Synchronous client-only hint that a consumer session likely exists. - * Used to suppress guest marketing / nationwide feed flash before AuthContext - * finishes hydrating from localStorage (CLS on `/`). - * - * Returns false during SSR — guest ISR HTML stays unchanged. - */ -export function hasClientSessionHint(): boolean { - if (typeof window === 'undefined') return false - - return Boolean(Cookies.get('accessToken') || localStorage.getItem('user')) -} diff --git a/lib/paymentQuery.ts b/lib/paymentQuery.ts deleted file mode 100644 index c594d35..0000000 --- a/lib/paymentQuery.ts +++ /dev/null @@ -1,25 +0,0 @@ -'use client' - -/** - * Shared helpers for consumer payment query keys on the current URL. - * Used by EventPaymentModalController and EventDetailOverlay dismiss cleanup. - */ - -export const PAYMENT_QUERY_KEYS = ['payment', 'bookingId', 'depositId', 'status'] as const - -export const stripPaymentQueryParams = (search: string): string => { - const params = new URLSearchParams(search) - - for (const key of PAYMENT_QUERY_KEYS) { - params.delete(key) - } - - return params.toString() -} - -export const hasActivePaymentQuery = (search: string): boolean => { - const params = new URLSearchParams(search) - const mode = params.get('payment') - - return mode === 'checkout' || mode === 'result' -} diff --git a/lib/server/eventManagementBootstrap.ts b/lib/server/eventManagementBootstrap.ts deleted file mode 100644 index 0f6ebdc..0000000 --- a/lib/server/eventManagementBootstrap.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { EventManagementBootstrap } from '@/services/eventManagement' - -type ApiEnvelope = { success: true; data: T } | { success: false; message: string } - -const resolveApiBaseUrl = () => { - const raw = process.env.NEXT_PUBLIC_API_URL - - if (!raw) throw new Error('NEXT_PUBLIC_API_URL is not configured') - const trimmed = raw.replace(/\/+$/, '') - - return trimmed.endsWith('/api/v1') ? trimmed : `${trimmed}/api/v1` -} - -/** Private owner data: never enters the public Next.js or persistent browser cache. */ -export async function fetchEventManagementBootstrapForOwner( - eventId: string, - accessToken: string -): Promise { - const response = await fetch(`${resolveApiBaseUrl()}/events/${encodeURIComponent(eventId)}/management-bootstrap`, { - cache: 'no-store', - headers: { Authorization: `Bearer ${accessToken}` }, - }) - - if (response.status === 401 || response.status === 403 || response.status === 404) return null - if (!response.ok) throw new Error(`Management bootstrap failed: ${response.status}`) - - const body = (await response.json()) as ApiEnvelope - - return body.success ? body.data : null -} diff --git a/next.config.js b/next.config.js index 1c0f61e..81fb8b4 100644 --- a/next.config.js +++ b/next.config.js @@ -87,7 +87,7 @@ const nextConfig = { ...(basePath ? { basePath, assetPrefix: basePath } : {}), allowedDevOrigins: ['192.168.100.51', '192.168.100.52'], - transpilePackages: ['mapir-react-component', 'react-mapbox-gl', 'arcaptcha-react'], + transpilePackages: ['mapir-react-component', 'react-mapbox-gl'], env: { NEXT_PUBLIC_MAP_API_KEY: process.env.MAP_API_KEY || process.env.NEXT_PUBLIC_MAP_API_KEY || '', diff --git a/package.json b/package.json index 1230f1a..a73da19 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@tiptap/react": "^3.11.0", "@tiptap/starter-kit": "^3.11.0", "@types/jalaali-js": "^1.2.0", - "arcaptcha-react": "^0.4.0", "axios": "1.18.1", "clsx": "2.1.1", "dayjs": "^1.11.19", diff --git a/services/chat.ts b/services/chat.ts deleted file mode 100644 index e1063d4..0000000 --- a/services/chat.ts +++ /dev/null @@ -1,354 +0,0 @@ -import type { - ConversationParticipantResponseDto, - ConversationResponseDto, - MessageReplyToDto, - MessageResponseDto, - SendMessageDto, -} from '@/api/generated/models' -import { - createServiceError, - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiData } from '@/services/apiResponse' -import { getChat } from '@/api/generated/chat/chat' -import { texts } from '@/texts' -import { parseRemittanceList } from '@/helpers/listResponse' - -export type Conversation = ConversationResponseDto - -export type ChatMessage = MessageResponseDto - -export type ChatMessageReplyTo = MessageReplyToDto - -export type ConversationTypeFilter = 'direct' | 'event_group' - -export const CONVERSATIONS_PAGE_SIZE = 20 - -/** Direction-based history page (same field meaning in before / after / around modes). */ -export interface ChatMessagesPage { - items: ChatMessage[] - olderCursor: string | null - newerCursor: string | null - hasMoreBefore: boolean - hasMoreAfter: boolean - aroundMessageId: string | null -} - -export interface ListMessagesParams { - before?: string - after?: string - around?: string - pageSize?: number -} - -export type SendChatMessagePayload = Pick - -export interface ConversationsPage { - items: Conversation[] - totalItemsCount: number -} - -const chatApi = getChat() - -export const LIST_CONVERSATIONS = async ( - params?: { type?: ConversationTypeFilter; page?: number; pageSize?: number }, - options?: ServiceCallOptions -): Promise> => { - const page = params?.page ?? 1 - const pageSize = params?.pageSize ?? CONVERSATIONS_PAGE_SIZE - - try { - const res = await chatApi.chatControllerListMine({ - page, - pageSize, - sort: '-lastMessageAt', - ...(params?.type ? { 'filters[type]': params.type } : {}), - }) - - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.listConversationsFailed) - } - - const parsed = parseRemittanceList(unwrapApiData(payload) ?? {}, 'items', page, pageSize) - - return successResult({ items: parsed.items, totalItemsCount: parsed.pagination.totalItemsCount }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const GET_CONVERSATION = async (conversationId: string, options?: ServiceCallOptions): Promise> => { - try { - const res = await chatApi.chatControllerGetOne(conversationId) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.getConversationFailed) - } - - const conversation = unwrapApiData(payload) - - if (!conversation?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(conversation) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const GET_UNREAD_CHAT_COUNT = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await chatApi.chatControllerUnreadCount() - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.unreadCountFailed) - } - - const data = unwrapApiData(payload) - - return successResult({ totalUnread: data?.totalUnread ?? 0 }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const MARK_CONVERSATION_READ = async ( - conversationId: string, - lastSeenMessageId: string, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await chatApi.chatControllerMarkRead(conversationId, { lastSeenMessageId }) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.markReadFailed) - } - - const data = unwrapApiData(payload) - - return successResult({ totalUnread: data?.totalUnread ?? 0 }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -const MESSAGES_PAGE_SIZE = 50 - -/** - * Fetches one page of conversation history. - * - * Cursor modes are mutually exclusive (`before` | `after` | `around`). Response - * cursors are direction-based: `olderCursor` → next `before`, `newerCursor` → - * next `after`, regardless of which mode produced the page. - */ -export const LIST_MESSAGES = async ( - conversationId: string, - params?: ListMessagesParams, - options?: ServiceCallOptions -): Promise> => { - try { - const pageSize = params?.pageSize ?? MESSAGES_PAGE_SIZE - const res = await chatApi.chatControllerListMessages(conversationId, { - pageSize, - ...(params?.before ? { before: params.before } : {}), - ...(params?.after ? { after: params.after } : {}), - ...(params?.around ? { around: params.around } : {}), - }) - - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.listMessagesFailed) - } - - const data = unwrapApiData(payload) - const parsed = parseRemittanceList(data ?? {}, 'items', 1, pageSize) - const meta = data?.response - - return successResult({ - items: parsed.items, - olderCursor: meta?.olderCursor ?? null, - newerCursor: meta?.newerCursor ?? null, - hasMoreBefore: meta?.hasMoreBefore ?? false, - hasMoreAfter: meta?.hasMoreAfter ?? false, - aroundMessageId: meta?.aroundMessageId ?? null, - }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const SEND_MESSAGE = async ( - conversationId: string, - payload: SendChatMessagePayload, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await chatApi.chatControllerSendGroup(conversationId, payload) - - const responsePayload = res.data - - if (!responsePayload.success) { - throw createServiceError(responsePayload.message || texts.chats.sendFailed, responsePayload.code) - } - - const message = unwrapApiData(responsePayload) - - if (!message?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(message) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -/** - * Sends the first (or a subsequent) message of a direct conversation by the - * other participant's user id, without needing a conversationId up front. - * Backend resolves/creates the canonical direct conversation for the pair - * (`get_or_create_direct_conversation`) and inserts the message in one call — - * see docs/workflows/fa/chat.md, "جریان A — پیام مستقیم". - */ -export const SEND_DIRECT_MESSAGE = async ( - otherUserId: string, - payload: SendChatMessagePayload, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await chatApi.chatControllerSendDirect(otherUserId, payload) - - const responsePayload = res.data - - if (!responsePayload.success) { - throw createServiceError(responsePayload.message || texts.chats.sendFailed, responsePayload.code) - } - - const message = unwrapApiData(responsePayload) - - if (!message?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(message) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const ENSURE_DIRECT_CONVERSATION = async ( - otherUserId: string, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await chatApi.chatControllerEnsureDirect(otherUserId) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.openConversationFailed, payload.code) - } - - const conversation = unwrapApiData(payload) - - if (!conversation?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(conversation) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export type ChatParticipant = ConversationParticipantResponseDto - -export const LIST_PARTICIPANTS = async ( - conversationId: string, - params?: { page?: number; pageSize?: number }, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await chatApi.chatControllerListParticipants(conversationId, { - page: params?.page ?? 1, - pageSize: params?.pageSize ?? 100, - sort: 'joinedAt', - }) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.listParticipantsFailed) - } - - const parsed = parseRemittanceList(unwrapApiData(payload) ?? {}, 'items') - - return successResult({ items: parsed.items }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} diff --git a/services/contactMessages.ts b/services/contactMessages.ts deleted file mode 100644 index 17f22b0..0000000 --- a/services/contactMessages.ts +++ /dev/null @@ -1,62 +0,0 @@ -import axiosInstance from '@/config/axios' -import { texts } from '@/texts' -import { API_ROUTES } from '@/services/config' -import { - createServiceError, - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiData, type ApiResponse } from '@/services/apiResponse' - -export interface ContactMessage { - id: string - fullName: string - mobile: string - subject: string - message: string - readAt: string | null - readBy: string | null - createdAt: string -} - -export interface CreateContactMessagePayload { - fullName: string - mobile: string - subject: string - message: string - arcaptchaToken: string -} - -export const CREATE_CONTACT_MESSAGE = async ( - payload: CreateContactMessagePayload, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await axiosInstance.post>(API_ROUTES.CONTACT_MESSAGES.CREATE, payload) - const body = res.data - - if (!body.success) { - throw createServiceError(body.message || texts.chats.sendFailed) - } - - const data = unwrapApiData(body) - - if (!data?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(data) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} diff --git a/services/identity.ts b/services/identity.ts deleted file mode 100644 index e6ac533..0000000 --- a/services/identity.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { z } from 'zod' - -import { - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiDataUnknown, unwrapApiPayload } from '@/helpers/listResponse' -import { ANALYTICS_EVENTS, trackAnalyticsEventOnce } from '@/lib/analytics' -import { API_ROUTES } from '@/services/config' -import axiosInstance from '@/config/axios' -import { texts } from '@/texts' - -export interface MyIdentityVerification { - id: string - status: 'pending' | 'verified' | 'rejected' - nationalCode: string | null - contractVersion?: string | null - contractAcceptedAt?: string | null - contractAcceptanceId?: string | null - reason?: { code: string; label: string } | null - rejectionReason?: string | null - createdAt: string - reviewedAt?: string | null -} - -export interface IdentityRejectionReason { - id: number - code: string - label: string - sortOrder: number -} - -export interface RequestHostContractOtpPayload { - nationalCode: string - contractAccepted: true - contractVersion: string -} - -const identityVerificationSchema = z.object({ - id: z.string().min(1), - status: z.enum(['pending', 'verified', 'rejected']), - nationalCode: z.string().nullable().optional(), - contractVersion: z.string().nullable().optional(), - contractAcceptedAt: z.string().nullable().optional(), - contractAcceptanceId: z.string().nullable().optional(), - reason: z.object({ code: z.string(), label: z.string() }).nullable().optional(), - rejectionReason: z.string().nullable().optional(), - createdAt: z.string(), - reviewedAt: z.string().nullable().optional(), -}) - -const rejectionReasonSchema = z.object({ - id: z.number(), - code: z.string(), - label: z.string(), - sortOrder: z.number(), -}) - -const otpRequestedSchema = z.object({ - purpose: z.literal('host_contract'), - expiresIn: z.number(), - contractVersion: z.string(), -}) - -export const GET_MY_IDENTITY_VERIFICATION = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await axiosInstance.get('identity/verifications/me') - const payload = unwrapApiDataUnknown(res.data) - const latest: unknown = Array.isArray(payload) ? payload[0] : payload - - if (!latest || typeof latest !== 'object' || !('id' in latest)) return successResult(null) - const parsed = identityVerificationSchema.safeParse(latest) - - if (!parsed.success) throw new Error(texts.common.invalidServerResponse) - - return successResult({ - ...parsed.data, - nationalCode: parsed.data.nationalCode ?? null, - }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const REQUEST_HOST_CONTRACT_OTP = async ( - data: RequestHostContractOtpPayload, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await axiosInstance.post(API_ROUTES.IDENTITY.REQUEST_OTP, data) - const parsed = otpRequestedSchema.safeParse(unwrapApiPayload(res.data)) - - if (!parsed.success) throw new Error(texts.common.invalidServerResponse) - - return successResult(parsed.data) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const CONFIRM_HOST_CONTRACT_OTP = async ( - code: string, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await axiosInstance.post(API_ROUTES.IDENTITY.CONFIRM_OTP, { code }) - const parsed = identityVerificationSchema.safeParse(unwrapApiPayload(res.data)) - - if (!parsed.success) throw new Error(texts.common.invalidServerResponse) - - trackAnalyticsEventOnce(ANALYTICS_EVENTS.IDENTITY_VERIFICATION_SUBMITTED, parsed.data.id, { - verification_id: parsed.data.id, - verification_status: parsed.data.status, - }) - - return successResult({ - ...parsed.data, - nationalCode: parsed.data.nationalCode ?? null, - }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const GET_IDENTITY_REJECTION_REASONS = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await axiosInstance.get(API_ROUTES.IDENTITY.REJECTION_REASONS) - const payload = unwrapApiDataUnknown(res.data) - const parsed = z.array(rejectionReasonSchema).safeParse(Array.isArray(payload) ? payload : []) - - if (!parsed.success) throw new Error(texts.common.invalidServerResponse) - - return successResult(parsed.data) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} diff --git a/services/myEvents.ts b/services/myEvents.ts deleted file mode 100644 index b4e0d34..0000000 --- a/services/myEvents.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { MyEventsGuestItemDto, MyEventsHostItemDto, MyEventsListControllerList200, PaginationMetaDto } from '@/api/generated/models' -import { getMyEvents } from '@/api/generated/my-events/my-events' -import { unwrapApiPayload } from '@/helpers/listResponse' - -export const MY_EVENTS_PAGE_SIZE = 20 - -export type MyEventsRole = 'guest' | 'host' -export type MyEventsGuestStatus = 'upcoming' | 'held' | 'cancelled' -export type MyEventsHostStatus = 'pending_review' | 'published' | 'rejected' -export type MyEventsStatus = MyEventsGuestStatus | MyEventsHostStatus -export type MyEventsGuestItem = MyEventsGuestItemDto -export type MyEventsHostItem = MyEventsHostItemDto - -export interface MyEventsPage { - role: MyEventsRole - status: MyEventsStatus - canHost: boolean - availableRoles: MyEventsRole[] - items: TItem[] - response: PaginationMetaDto -} - -const myEventsApi = getMyEvents() - -/** Fetches one server-filtered lifecycle page for the My Events screen. */ -export const fetchMyEventsPage = async ({ - role, - status, - page, - pageSize = MY_EVENTS_PAGE_SIZE, - signal, -}: { - role: MyEventsRole - status: MyEventsStatus - page: number - pageSize?: number - signal?: AbortSignal -}): Promise> => { - const response = await myEventsApi.myEventsListControllerList({ role, status, page, pageSize }, { signal }) - const payload = unwrapApiPayload(response.data) - - return payload as MyEventsPage -} diff --git a/services/notifications.ts b/services/notifications.ts deleted file mode 100644 index 289b1e1..0000000 --- a/services/notifications.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { NotificationResponseDto } from '@/api/generated/models' -import { - createServiceError, - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiData } from '@/services/apiResponse' -import { getNotifications } from '@/api/generated/notifications/notifications' -import { parseRemittanceList } from '@/helpers/listResponse' -import { texts } from '@/texts' - -export type InAppNotification = NotificationResponseDto - -const notificationsApi = getNotifications() - -export const LIST_NOTIFICATIONS = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await notificationsApi.notificationsControllerListMine({ page: 1, pageSize: 30, sort: '-createdAt' }) - - const parsed = parseRemittanceList(res.data, 'items') - - return successResult({ items: parsed.items }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const GET_UNREAD_NOTIFICATION_COUNT = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await notificationsApi.notificationsControllerUnreadCount() - - if (!res.data.success) throw createServiceError(res.data.message) - const payload = unwrapApiData(res.data) - const totalUnread = Number(payload?.totalUnread ?? 0) - - return successResult({ totalUnread: Number.isFinite(totalUnread) ? totalUnread : 0 }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const MARK_NOTIFICATION_READ = async ( - notificationId: string, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await notificationsApi.notificationsControllerMarkAsRead(notificationId) - - if (!res.data.success) throw createServiceError(res.data.message) - const payload = unwrapApiData(res.data) - - if (!payload?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(payload) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const countUnreadNotifications = (items: InAppNotification[]): number => items.filter((item) => !item.readAt).length diff --git a/services/payments.ts b/services/payments.ts deleted file mode 100644 index 27701d6..0000000 --- a/services/payments.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { z } from 'zod' - -import type { - CreatePaymentResponseDto, - PaymentCheckoutResponseDto, - PaymentReceiptResponseDto, - PaymentResponseDto, -} from '@/api/generated/models' -import type { BookingStatus } from '@/api/generated/models' -import { - createServiceError, - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiData } from '@/services/apiResponse' -import { getBookings } from '@/api/generated/bookings/bookings' -import { getEvents } from '@/api/generated/events/events' -import { getFinancial } from '@/api/generated/financial/financial' -import { unwrapApiPayload } from '@/helpers/listResponse' -import { texts } from '@/texts' - -// docs/workflows/fa/payment.md — consumer checkout for a `pending_payment` -// booking. Payment creation is a *second, separate* call from booking -// creation (owned by saza2's services/bookings.ts) — this file only covers -// the payment leg: reading the booking/event to render the checkout -// screen, then POST /bookings/:bookingId/payments. - -export interface BookingForPayment { - id: string - eventId: string - bookingCode: string - status: BookingStatus - expiresAt: string | null -} - -export interface EventSummaryForPayment { - id: string - slug?: string | null - title: string - price: number -} - -export type PaymentCheckout = PaymentCheckoutResponseDto - -export type Payment = PaymentResponseDto - -// Orval snapshot may lag backend fields returned at runtime for checkout. -export type CreatePaymentResult = CreatePaymentResponseDto & { - depositId?: string | null - depositAmount?: number - walletBalanceUsed?: number -} - -export type PaymentReceipt = PaymentReceiptResponseDto - -const bookingForPaymentSchema = z.object({ - id: z.string().min(1), - eventId: z.string().min(1), - bookingCode: z.string(), - status: z.enum(['pending_payment', 'confirmed', 'cancelled', 'expired', 'refunded', 'no_show']), - expiresAt: z.string().nullable(), -}) - -const paymentReceiptSchema = z.object({ - id: z.string().min(1), - paymentId: z.string().min(1), - userId: z.string().min(1), - bookingId: z.string().min(1), - eventId: z.string().min(1), - receiptCode: z.string(), - snapshot: z.record(z.string(), z.unknown()), - issuedAt: z.string(), -}) - -const bookingsApi = getBookings() -const eventsApi = getEvents() -const financialApi = getFinancial() - -export const GET_BOOKING_FOR_PAYMENT = async ( - bookingId: string, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await bookingsApi.bookingsControllerFindOne(bookingId) - const parsed = bookingForPaymentSchema.safeParse(unwrapApiPayload(res.data)) - - if (!parsed.success) throw createServiceError(texts.common.invalidServerResponse) - - return successResult(parsed.data) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -// Booking detail (GET /bookings/:id) intentionally doesn't carry the -// event's price/title — only `eventId` — so the checkout screen needs a -// second read against the public event-detail endpoint. -export const GET_EVENT_SUMMARY = async (eventId: string, options?: ServiceCallOptions): Promise> => { - try { - const res = await eventsApi.eventsControllerFindOne(eventId) - const payload = unwrapApiPayload(res.data) - - if (!payload?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult({ - id: payload.id as string, - slug: typeof payload.slug === 'string' ? payload.slug : null, - title: payload.title as string, - price: Number(payload.price ?? 0), - }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -// payment.md "تصمیم ۲": `walletAmount` is a single clamp-friendly number — -// the backend clamps to min(requested, wallet.balance, totalAmount) -// regardless, so the frontend just sends what the "use wallet" toggle -// implies. -export const CREATE_PAYMENT = async ( - bookingId: string, - walletAmount: number, - options?: ServiceCallOptions & { discountCode?: string } -): Promise> => { - try { - const res = await financialApi.paymentsControllerCreate(bookingId, { - walletAmount, - discountCode: options?.discountCode?.trim() || undefined, - }) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.wallet.paymentFailed) - } - - const result = unwrapApiData(payload) - - if (!result?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(result) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const GET_PAYMENT_RECEIPT = async (paymentId: string, options?: ServiceCallOptions): Promise> => { - try { - const res = await financialApi.paymentsControllerReceipt(paymentId) - const parsed = paymentReceiptSchema.safeParse(unwrapApiPayload(res.data)) - - if (!parsed.success) throw createServiceError(texts.common.invalidServerResponse) - - return successResult(parsed.data) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} diff --git a/services/settlements.ts b/services/settlements.ts deleted file mode 100644 index 3650dac..0000000 --- a/services/settlements.ts +++ /dev/null @@ -1,27 +0,0 @@ -import axiosInstance from '@/config/axios' -import { unwrapApiPayload } from '@/helpers/listResponse' - -export interface SettlementRequest { - id: string - settlementCode: string - organizerId: string - bankAccountId: string - totalAmount: number - status: 'pending' | 'processing' | 'completed' | 'failed' - processedAt?: string | null - failureReason?: string | null - manualTrackingCode?: string | null - manualReceiptUrl?: string | null - manualNote?: string | null - createdAt: string - updatedAt: string -} - -export const LIST_MY_SETTLEMENTS = async (): Promise<{ items: SettlementRequest[] }> => { - const response = await axiosInstance.get('settlements/me', { - params: { page: 1, pageSize: 30, sort: '-createdAt' }, - }) - const payload = unwrapApiPayload<{ items?: SettlementRequest[] }>(response.data) - - return { items: Array.isArray(payload.items) ? payload.items : [] } -} diff --git a/services/userBlocks.ts b/services/userBlocks.ts deleted file mode 100644 index 0380509..0000000 --- a/services/userBlocks.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { UserBlockResponseDto } from '@/api/generated/models' -import { - createServiceError, - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiData } from '@/services/apiResponse' -import { getUserBlocks } from '@/api/generated/user-blocks/user-blocks' -import { texts } from '@/texts' -import { parseRemittanceList } from '@/helpers/listResponse' - -// docs/workflows/fa/user-blocking-reporting.md, Flow A — a directed block -// record (`blocker_id` = current user, `blocked_id` = the other user). -// Effect is mutual: once either side has a row against the other, new -// *direct* messages between them are rejected in both directions. Event -// group chats are never affected. - -export type UserBlock = UserBlockResponseDto - -const userBlocksApi = getUserBlocks() - -export const BLOCK_USER = async (blockedId: string, options?: ServiceCallOptions): Promise> => { - try { - const res = await userBlocksApi.userBlocksControllerCreate({ blockedId }) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.blockUserFailed, payload.code) - } - - const block = unwrapApiData(payload) - - if (!block?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(block) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const UNBLOCK_USER = async (blockedId: string, options?: ServiceCallOptions): Promise> => { - try { - const res = await userBlocksApi.userBlocksControllerUnblock(blockedId) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.unblockUserFailed, payload.code) - } - - const block = unwrapApiData(payload) - - if (!block?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(block) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -/** - * Checks whether the current user has an active block row against - * `otherUserId`. Used to toggle the header menu between «مسدود کردن» and - * «رفع مسدودیت» — there's no dedicated "is blocked" endpoint, so this - * reuses `GET /user-blocks/me?filters[blockedId]=...` (paginated, so a - * single-row page is enough to answer a yes/no question). - * - * Note: this only reflects blocks *I* created (`blocker_id = me`), not - * ones the other person may have created against me — matching the known - * gap documented in user-blocking-reporting.md ("هیچ endpoint «چه کسی مرا - * مسدود کرده» وجود ندارد"). - */ -export const GET_MY_BLOCK_STATUS = async ( - otherUserId: string, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await userBlocksApi.userBlocksControllerListMine({ page: 1, pageSize: 1, 'filters[blockedId]': otherUserId }) - - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.blockStatusFailed) - } - - const parsed = parseRemittanceList(unwrapApiData(payload) ?? {}, 'items') - - return successResult({ isBlocked: parsed.items.length > 0 }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} diff --git a/services/userReports.ts b/services/userReports.ts deleted file mode 100644 index a85cfdb..0000000 --- a/services/userReports.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { CreateUserReportDto, ReportReasonResponseDto, UserReportResponseDto } from '@/api/generated/models' -import { - createServiceError, - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiData } from '@/services/apiResponse' -import { getUserReports } from '@/api/generated/user-reports/user-reports' -import { texts } from '@/texts' - -// docs/workflows/fa/user-blocking-reporting.md, Flow B — purely -// informational: creating a report never blocks, hides, or notifies -// anyone by itself. It only creates a `pending` row an admin reviews later. - -export type ReportReason = ReportReasonResponseDto - -export type UserReport = UserReportResponseDto - -const userReportsApi = getUserReports() - -/** - * Public, unauthenticated — safe to fetch ahead of time (e.g. when the - * report menu item is rendered) so the reason list is ready by the time - * the user opens the report form. - */ -export const LIST_REPORT_REASONS = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await userReportsApi.userReportsControllerListReportReasons() - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.chats.listReportReasonsFailed) - } - - return successResult(unwrapApiData(payload) ?? []) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -/** - * At least one of `reasonId` / `description` is required — mirrors the - * backend's `CreateUserReportDto` cross-field rule - * (`user_reports_content_chk`). - */ -export const REPORT_USER = async ( - reportedId: string, - payload: { reasonId?: number; description?: string }, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await userReportsApi.userReportsControllerCreate({ - reportedId, - ...payload, - }) - - const responsePayload = res.data - - if (!responsePayload.success) { - throw createServiceError(responsePayload.message || texts.chats.submitReportFailed, responsePayload.code) - } - - const report = unwrapApiData(responsePayload) - - if (!report?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(report) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -/** - * Reports one specific chat message. The server derives and persists the - * conversation, sender, and immutable message snapshot; the client cannot - * forge those moderation fields. - */ -export const REPORT_MESSAGE = async ( - reportedId: string, - messageId: string, - payload: { reasonId?: number; description?: string }, - options?: ServiceCallOptions -): Promise> => { - try { - const request: CreateUserReportDto = { - reportedId, - messageId, - ...payload, - } - const res = await userReportsApi.userReportsControllerCreate(request) - const responsePayload = res.data - - if (!responsePayload.success) { - throw createServiceError(responsePayload.message || texts.chats.submitReportFailed, responsePayload.code) - } - - const report = unwrapApiData(responsePayload) - - if (!report?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(report) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} diff --git a/services/withdrawals.ts b/services/withdrawals.ts deleted file mode 100644 index 5a7176b..0000000 --- a/services/withdrawals.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { BankAccountOwnerType, BankAccountResponseDto, CreateBankAccountDto, WithdrawalResponseDto } from '@/api/generated/models' -import { - createServiceError, - errorResult, - handleServiceError, - type ServiceCallOptions, - type ServiceResult, - shouldBubbleErrorToParent, - successResult, -} from '@/services/errorHandler' -import { unwrapApiData } from '@/services/apiResponse' -import { getFinancial } from '@/api/generated/financial/financial' -import { parseRemittanceList, unwrapApiPayload } from '@/helpers/listResponse' -import { ANALYTICS_EVENTS, trackAnalyticsEventOnce } from '@/lib/analytics' -import { texts } from '@/texts' - -// docs/workflows/fa/settlement-withdrawal.md — Path A (self-service wallet -// withdrawal). Distinct from admin settlement (Path B), which never -// touches `wallets.balance` — out of scope here. - -export type { BankAccountOwnerType } -export type BankAccount = BankAccountResponseDto & { - verificationStatus?: 'pending_review' | 'approved' | 'rejected' - rejectionReason?: string | null - nationalCode?: string | null -} -export type CreateBankAccountPayload = CreateBankAccountDto & { - nationalCode?: string -} -export type WithdrawalRequest = WithdrawalResponseDto - -const financialApi = getFinancial() - -export const LIST_BANK_ACCOUNTS = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await financialApi.bankAccountsControllerListMine() - const payload = unwrapApiPayload(res.data) - const items = Array.isArray(payload) - ? payload - : Array.isArray((payload as { items?: unknown[] }).items) - ? (payload as { items: unknown[] }).items - : [] - - return successResult(items as BankAccount[]) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const CREATE_BANK_ACCOUNT = async ( - payload: CreateBankAccountPayload, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await financialApi.bankAccountsControllerCreate(payload) - const body = res.data - - if (!body.success) { - throw createServiceError(body.message || texts.wallet.bankAccountCreateFailed) - } - - const result = unwrapApiData(body) - - if (!result?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - return successResult(result) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -export const LIST_WITHDRAWAL_REQUESTS = async (options?: ServiceCallOptions): Promise> => { - try { - const res = await financialApi.withdrawalRequestsControllerListMine({ page: 1, pageSize: 30, sort: '-createdAt' }) - - const parsed = parseRemittanceList(res.data, 'items') - - return successResult({ items: parsed.items }) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} - -// amount is in Toman (platform-wide convention — see payment.md's note on -// the matching CreatePaymentDto Swagger typo; CreateWithdrawalRequestDto -// has the same "Rials" typo in its Swagger annotation). -export const CREATE_WITHDRAWAL_REQUEST = async ( - bankAccountId: string, - amount: number, - options?: ServiceCallOptions -): Promise> => { - try { - const res = await financialApi.withdrawalRequestsControllerCreate({ - bankAccountId, - amount, - }) - const payload = res.data - - if (!payload.success) { - throw createServiceError(payload.message || texts.wallet.withdrawalCreateFailed) - } - - const result = unwrapApiData(payload) - - if (!result?.id) { - throw createServiceError(texts.common.invalidServerResponse) - } - - trackAnalyticsEventOnce(ANALYTICS_EVENTS.WITHDRAWAL_REQUESTED, result.id, { - withdrawal_id: result.id, - value: amount * 10, - currency: 'IRR', - withdrawal_status: result.status, - }) - - return successResult(result) - } catch (error) { - const normalizedError = handleServiceError(error, options) - - if (shouldBubbleErrorToParent(options)) { - throw normalizedError - } - - return errorResult(normalizedError) - } -} diff --git a/validation/identity.ts b/validation/identity.ts deleted file mode 100644 index ac2c5f8..0000000 --- a/validation/identity.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { z } from 'zod' - -import { convertPersianToEnglish } from '@/helpers' -import { texts } from '@/texts' - -const toEnglishDigits = (value: string) => convertPersianToEnglish(value).trim() -const v = texts.validation.identity -const nationalId = texts.validation.nationalId - -export const IdentityVerificationFormValidation = z - .object({ - nationalCode: z - .string() - .min(1, nationalId.nationalCodeRequired) - .refine((value) => /^\d{10}$/.test(toEnglishDigits(value)), { - message: nationalId.nationalCodeDigits, - }), - contractAccepted: z.boolean().optional(), - }) - .superRefine((values, ctx) => { - if (values.contractAccepted !== true) { - ctx.addIssue({ - code: 'custom', - path: ['contractAccepted'], - message: v.contractRequired, - }) - } - }) - -export const IdentityContractOtpValidation = z.object({ - code: z - .string() - .min(1, v.otpRequired) - .refine((value) => /^\d{4}$/.test(toEnglishDigits(value)), { - message: v.otpDigits, - }), -}) - -export type IdentityVerificationFormValues = z.infer -export type IdentityContractOtpValues = z.infer - -export const normalizeNationalCode = (value: string) => toEnglishDigits(value) -export const normalizeOtpCode = (value: string) => toEnglishDigits(value) diff --git a/validation/profile.ts b/validation/profile.ts deleted file mode 100644 index f9a2b30..0000000 --- a/validation/profile.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod' - -import { texts } from '@/texts' - -const v = texts.validation.profile -const person = texts.validation.person - -export const UpdateProfileFormValidation = z.object({ - firstName: z.string().trim().min(1, person.firstNameRequired).max(100, person.firstNameMax), - lastName: z.string().trim().min(1, person.lastNameRequired).max(100, person.lastNameMax), - // z.enum keeps input/output identical so zodResolver matches useForm - gender: z.enum(['male', 'female'], { message: person.genderRequired }), - bio: z.string().trim().max(2000, v.bioMax).optional().or(z.literal('')), - // انتخاب استان فعلاً در فرم پروفایل غیرفعال است. - // provinceId: z.string().optional().or(z.literal('')), - cityId: z.string().optional().or(z.literal('')), - defaultAddress: z.string().trim().max(500, v.addressMax).optional().or(z.literal('')), -}) - -export type UpdateProfileFormValues = z.infer