@mohasinac/appkit 2.7.58 → 2.7.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/_internal/server/features/scams/og.js +1 -1
  2. package/dist/_internal/shared/actions/bulk-helpers.d.ts +3 -0
  3. package/dist/_internal/shared/actions/bulk-helpers.js +19 -0
  4. package/dist/_internal/shared/config/schema.d.ts +4 -4
  5. package/dist/client.d.ts +3 -0
  6. package/dist/client.js +2 -0
  7. package/dist/configs/next.js +11 -0
  8. package/dist/features/account/components/AddressesIndexListing.js +3 -4
  9. package/dist/features/account/components/UserSidebar.js +14 -10
  10. package/dist/features/admin/components/AdminAllEventEntriesView.js +3 -9
  11. package/dist/features/admin/components/AdminBidsView.js +2 -8
  12. package/dist/features/admin/components/AdminBlogView.js +4 -9
  13. package/dist/features/admin/components/AdminBrandEditorView.js +14 -17
  14. package/dist/features/admin/components/AdminBrandsView.js +4 -9
  15. package/dist/features/admin/components/AdminBundlesView.js +39 -58
  16. package/dist/features/admin/components/AdminCarouselView.js +7 -20
  17. package/dist/features/admin/components/AdminCartsView.js +3 -4
  18. package/dist/features/admin/components/AdminCategoriesView.js +4 -9
  19. package/dist/features/admin/components/AdminCategoryEditorView.js +1 -5
  20. package/dist/features/admin/components/AdminContactView.js +6 -24
  21. package/dist/features/admin/components/AdminCouponEditorView.js +14 -17
  22. package/dist/features/admin/components/AdminCouponsView.js +8 -16
  23. package/dist/features/admin/components/AdminFaqEditorView.js +14 -17
  24. package/dist/features/admin/components/AdminFaqsView.js +18 -69
  25. package/dist/features/admin/components/AdminFeatureEditorView.js +7 -9
  26. package/dist/features/admin/components/AdminNewsletterView.js +4 -14
  27. package/dist/features/admin/components/AdminNotificationsView.js +2 -8
  28. package/dist/features/admin/components/AdminOrdersView.js +2 -8
  29. package/dist/features/admin/components/AdminPayoutsView.js +2 -8
  30. package/dist/features/admin/components/AdminPrizeDrawsView.js +4 -9
  31. package/dist/features/admin/components/AdminProductEditorView.js +1 -5
  32. package/dist/features/admin/components/AdminProductsView.js +4 -9
  33. package/dist/features/admin/components/AdminReviewsView.js +2 -8
  34. package/dist/features/admin/components/AdminScammersView.js +2 -8
  35. package/dist/features/admin/components/AdminSessionsView.js +2 -8
  36. package/dist/features/admin/components/AdminStoresView.js +2 -8
  37. package/dist/features/admin/components/AdminSublistingCategoriesView.js +2 -6
  38. package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +17 -20
  39. package/dist/features/admin/components/AdminSupportTicketsView.js +2 -8
  40. package/dist/features/admin/components/AdminTeamView.js +5 -14
  41. package/dist/features/admin/components/AdminUsersView.js +2 -8
  42. package/dist/features/admin/hooks/useAdminListing.d.ts +58 -0
  43. package/dist/features/admin/hooks/useAdminListing.js +71 -0
  44. package/dist/features/auth/schemas/firestore.d.ts +1 -0
  45. package/dist/features/blog/components/BlogIndexListing.js +3 -6
  46. package/dist/features/categories/components/CategoriesIndexListing.js +3 -6
  47. package/dist/features/events/components/AdminEventsView.js +4 -4
  48. package/dist/features/events/components/EventsIndexListing.js +3 -4
  49. package/dist/features/homepage/components/AnnouncementBar.d.ts +4 -1
  50. package/dist/features/homepage/components/AnnouncementBar.js +51 -4
  51. package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +3 -1
  52. package/dist/features/homepage/components/MarketplaceHomepageView.js +2 -2
  53. package/dist/features/homepage/components/index.d.ts +1 -1
  54. package/dist/features/homepage/components/index.js +1 -1
  55. package/dist/features/homepage/lib/section-renderer.d.ts +2 -2
  56. package/dist/features/homepage/lib/section-renderer.js +2 -2
  57. package/dist/features/orders/repository/orders.repository.d.ts +0 -1
  58. package/dist/features/orders/repository/orders.repository.js +0 -1
  59. package/dist/features/orders/server.d.ts +1 -1
  60. package/dist/features/orders/server.js +1 -1
  61. package/dist/features/products/components/GroupSettingsPanel.js +64 -48
  62. package/dist/features/products/repository/products.repository.d.ts +0 -2
  63. package/dist/features/products/repository/products.repository.js +0 -2
  64. package/dist/features/products/server.d.ts +1 -1
  65. package/dist/features/products/server.js +1 -1
  66. package/dist/features/promotions/components/CouponCard.js +5 -5
  67. package/dist/features/promotions/components/CouponsIndexListing.js +3 -2
  68. package/dist/features/reviews/components/ReviewsIndexListing.js +3 -4
  69. package/dist/features/reviews/repository/reviews.repository.d.ts +0 -1
  70. package/dist/features/reviews/repository/reviews.repository.js +0 -1
  71. package/dist/features/reviews/server.d.ts +1 -1
  72. package/dist/features/reviews/server.js +1 -1
  73. package/dist/features/seller/components/SellerAddressesView.js +10 -20
  74. package/dist/features/seller/components/SellerAuctionsView.js +13 -22
  75. package/dist/features/seller/components/SellerBidsView.js +5 -5
  76. package/dist/features/seller/components/SellerBundlesView.js +11 -19
  77. package/dist/features/seller/components/SellerClassifiedView.js +11 -19
  78. package/dist/features/seller/components/SellerCouponsView.js +14 -20
  79. package/dist/features/seller/components/SellerDigitalCodesView.js +11 -19
  80. package/dist/features/seller/components/SellerGroupedListingsView.js +6 -10
  81. package/dist/features/seller/components/SellerLiveView.js +11 -19
  82. package/dist/features/seller/components/SellerOffersView.js +3 -4
  83. package/dist/features/seller/components/SellerOrdersView.js +8 -22
  84. package/dist/features/seller/components/SellerPayoutMethodsView.js +11 -23
  85. package/dist/features/seller/components/SellerPayoutsView.js +3 -4
  86. package/dist/features/seller/components/SellerPreOrdersView.js +13 -22
  87. package/dist/features/seller/components/SellerPrizeDrawsView.js +13 -22
  88. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +1 -1
  89. package/dist/features/seller/components/SellerProductsFilterDrawer.js +3 -6
  90. package/dist/features/seller/components/SellerProductsView.js +9 -22
  91. package/dist/features/seller/components/SellerShippingConfigsView.js +11 -23
  92. package/dist/features/seller/components/SellerStoreCategoriesView.js +14 -33
  93. package/dist/features/seller/components/SellerTemplatesView.js +25 -41
  94. package/dist/features/stores/components/StoreReviewsListing.js +3 -4
  95. package/dist/features/stores/components/StoresIndexListing.js +3 -4
  96. package/dist/index.d.ts +2 -6
  97. package/dist/index.js +3 -12
  98. package/dist/react/contexts/SessionContext.d.ts +2 -0
  99. package/dist/react/contexts/SessionContext.js +2 -0
  100. package/dist/react/hooks/useEntityDelete.d.ts +15 -0
  101. package/dist/react/hooks/useEntityDelete.js +39 -0
  102. package/dist/react/index.d.ts +2 -0
  103. package/dist/react/index.js +2 -0
  104. package/dist/repositories/index.d.ts +3 -3
  105. package/dist/repositories/index.js +3 -3
  106. package/dist/styles.css +1 -1
  107. package/dist/tailwind-utilities.css +1 -1
  108. package/dist/ui/components/ListingFilterDrawer.d.ts +10 -0
  109. package/dist/ui/components/ListingFilterDrawer.js +8 -0
  110. package/dist/ui/index.d.ts +2 -0
  111. package/dist/ui/index.js +1 -0
  112. package/package.json +1 -1
@@ -1,10 +1,57 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useState, useEffect } from "react";
4
4
  import { X } from "lucide-react";
5
- export function AnnouncementBar({ message, className = "" }) {
6
- const [dismissed, setDismissed] = useState(false);
5
+ import { useAuth } from "../../../react/contexts/SessionContext";
6
+ const STORAGE_KEY = "letitrip:announcement-dismissed";
7
+ const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
8
+ export function hashBannerMessage(msg) {
9
+ let h = 0;
10
+ for (let i = 0; i < msg.length; i++) {
11
+ h = (Math.imul(31, h) + msg.charCodeAt(i)) | 0;
12
+ }
13
+ return h.toString(36);
14
+ }
15
+ function isLocallyDismissed(message) {
16
+ try {
17
+ const raw = localStorage.getItem(STORAGE_KEY);
18
+ if (!raw)
19
+ return false;
20
+ const { hash, at } = JSON.parse(raw);
21
+ if (hash !== hashBannerMessage(message))
22
+ return false;
23
+ return Date.now() - at < ONE_WEEK_MS;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ function saveLocalDismissal(message) {
30
+ try {
31
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ hash: hashBannerMessage(message), at: Date.now() }));
32
+ }
33
+ catch { /* quota exceeded — ignore */ }
34
+ }
35
+ export function AnnouncementBar({ message, className = "", onDismiss }) {
36
+ const { user } = useAuth();
37
+ const bannerHash = hashBannerMessage(message);
38
+ const userAlreadyDismissed = !!user?.dismissedBannerHash && user.dismissedBannerHash === bannerHash;
39
+ const [dismissed, setDismissed] = useState(userAlreadyDismissed);
40
+ const [mounted, setMounted] = useState(userAlreadyDismissed);
41
+ useEffect(() => {
42
+ if (userAlreadyDismissed) {
43
+ setDismissed(true);
44
+ }
45
+ else if (isLocallyDismissed(message)) {
46
+ setDismissed(true);
47
+ }
48
+ setMounted(true);
49
+ }, [message, userAlreadyDismissed]);
7
50
  if (dismissed)
8
51
  return null;
9
- return (_jsx("div", { className: `relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-700 ${className}`, role: "banner", "data-section": "announcementbar-div-310", children: _jsx("div", { className: "container mx-auto max-w-[1920px] px-4 sm:px-6 lg:px-8", "data-section": "announcementbar-div-311", children: _jsxs("div", { className: "relative flex items-center justify-center py-2 text-center text-sm font-medium text-white", "data-section": "announcementbar-div-312", children: [_jsx("span", { children: message }), _jsx("button", { type: "button", onClick: () => setDismissed(true), className: "absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-white", "aria-label": "Dismiss announcement", children: _jsx(X, { className: "h-4 w-4" }) })] }) }) }));
52
+ return (_jsx("div", { className: `relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-700 transition-opacity duration-200 ${mounted ? "opacity-100" : "opacity-0"} ${className}`, role: "banner", "data-section": "announcementbar-div-310", children: _jsx("div", { className: "container mx-auto max-w-[1920px] px-4 sm:px-6 lg:px-8", "data-section": "announcementbar-div-311", children: _jsxs("div", { className: "relative flex items-center justify-center py-2 text-center text-sm font-medium text-white", "data-section": "announcementbar-div-312", children: [_jsx("span", { children: message }), _jsx("button", { type: "button", onClick: () => {
53
+ saveLocalDismissal(message);
54
+ onDismiss?.(bannerHash);
55
+ setDismissed(true);
56
+ }, className: "absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-white", "aria-label": "Dismiss announcement", children: _jsx(X, { className: "h-4 w-4" }) })] }) }) }));
10
57
  }
@@ -3,5 +3,7 @@ export type { MarketplaceHomepageViewAdSlots } from "../lib/section-renderer";
3
3
  export interface MarketplaceHomepageViewProps {
4
4
  adSlots?: import("../lib/section-renderer").MarketplaceHomepageViewAdSlots;
5
5
  newsletterFormSlot?: React.ReactNode;
6
+ /** Callback when user dismisses announcement banner — wire to a server action to persist. */
7
+ onBannerDismiss?: (hash: string) => void;
6
8
  }
7
- export declare function MarketplaceHomepageView({ adSlots, newsletterFormSlot, }?: MarketplaceHomepageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
9
+ export declare function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, }?: MarketplaceHomepageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -23,7 +23,7 @@ function toCarouselSlides(docs) {
23
23
  updatedAt: updatedAt instanceof Date ? updatedAt.toISOString() : undefined,
24
24
  }));
25
25
  }
26
- export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, } = {}) {
26
+ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, } = {}) {
27
27
  const slides = await carouselRepository.getActiveSlides().catch(() => []);
28
28
  const siteSettings = await siteSettingsRepository.getSingleton().catch(() => null);
29
29
  const announcementMessage = siteSettings?.announcementBar?.message?.trim() ||
@@ -113,5 +113,5 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, } =
113
113
  : undefined,
114
114
  };
115
115
  const carouselSlides = toCarouselSlides(slides);
116
- return (_jsxs(Main, { children: [showAnnouncement ? _jsx(AnnouncementBar, { message: announcementMessage }) : null, orderedSections.map((section) => renderSection(section, adSlots, newsletterFormSlot ?? null, faqItems, carouselSlides, liveStats, sectionData))] }));
116
+ return (_jsxs(Main, { children: [showAnnouncement ? _jsx(AnnouncementBar, { message: announcementMessage, onDismiss: onBannerDismiss }) : null, orderedSections.map((section) => renderSection(section, adSlots, newsletterFormSlot ?? null, faqItems, carouselSlides, liveStats, sectionData))] }));
117
117
  }
@@ -53,7 +53,7 @@ export { FeaturedResultsSection } from "./FeaturedResultsSection";
53
53
  export type { HowItWorksInfoStep, HowItWorksInfoDetail, HowItWorksInfoViewProps, } from "./HowItWorksInfoView";
54
54
  export { HowItWorksInfoView } from "./HowItWorksInfoView";
55
55
  export type { AnnouncementBarProps } from "./AnnouncementBar";
56
- export { AnnouncementBar } from "./AnnouncementBar";
56
+ export { AnnouncementBar, hashBannerMessage } from "./AnnouncementBar";
57
57
  export type { ShopByCategorySectionProps } from "./ShopByCategorySection";
58
58
  export { ShopByCategorySection } from "./ShopByCategorySection";
59
59
  export type { FeaturedProductsSectionProps } from "./FeaturedProductsSection";
@@ -26,7 +26,7 @@ export { WhatsAppCommunitySection } from "./WhatsAppCommunitySection";
26
26
  export { HomepageSkeleton } from "./HomepageSkeleton";
27
27
  export { FeaturedResultsSection } from "./FeaturedResultsSection";
28
28
  export { HowItWorksInfoView } from "./HowItWorksInfoView";
29
- export { AnnouncementBar } from "./AnnouncementBar";
29
+ export { AnnouncementBar, hashBannerMessage } from "./AnnouncementBar";
30
30
  export { ShopByCategorySection } from "./ShopByCategorySection";
31
31
  export { FeaturedProductsSection } from "./FeaturedProductsSection";
32
32
  export { FeaturedAuctionsSection } from "./FeaturedAuctionsSection";
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import type { LiveStatsMap } from "./live-stats";
3
- import { AnnouncementBar } from "../components/AnnouncementBar";
3
+ import { AnnouncementBar, hashBannerMessage } from "../components/AnnouncementBar";
4
4
  import type { CarouselSlide } from "../types/index";
5
5
  import type { HomepageSectionDocument } from "../schemas";
6
6
  import type { ProductItem } from "../../products/types";
@@ -33,4 +33,4 @@ export type FaqItem = {
33
33
  category: string;
34
34
  };
35
35
  export declare function renderSection(section: HomepageSectionDocument, adSlots: MarketplaceHomepageViewAdSlots | undefined, newsletterFormSlot: React.ReactNode, faqItems: FaqItem[], slides: CarouselSlide[], liveStats: LiveStatsMap, sectionData?: SectionData): React.ReactNode;
36
- export { AnnouncementBar };
36
+ export { AnnouncementBar, hashBannerMessage };
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { ROUTES } from "../../../next";
4
4
  import { cleanTitle, parseWelcomeDescription } from "./section-helpers";
5
5
  import { DEFAULT_TRUST_FEATURES, DEFAULT_SECURITY_ITEMS } from "./section-defaults";
6
- import { AnnouncementBar } from "../components/AnnouncementBar";
6
+ import { AnnouncementBar, hashBannerMessage } from "../components/AnnouncementBar";
7
7
  import { HeroCarousel } from "../components/HeroCarousel";
8
8
  import { StatsCounterSection } from "../components/StatsCounterSection";
9
9
  import { TrustFeaturesSection } from "../components/TrustFeaturesSection";
@@ -202,4 +202,4 @@ export function renderSection(section, adSlots, newsletterFormSlot, faqItems, sl
202
202
  const adSlotKey = AD_SLOT_MAP[section.type];
203
203
  return (_jsxs(React.Fragment, { children: [sectionElement, adSlots && adSlotKey !== undefined && adSlotKey in adSlots && adSlots[adSlotKey]] }, section.id));
204
204
  }
205
- export { AnnouncementBar };
205
+ export { AnnouncementBar, hashBannerMessage };
@@ -219,4 +219,3 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
219
219
  }
220
220
  declare const orderRepository: OrderRepository;
221
221
  export { OrderRepository, orderRepository };
222
- export { OrderRepository as OrdersRepository };
@@ -360,4 +360,3 @@ OrderRepository.ADMIN_SIEVE_FIELDS = {
360
360
  };
361
361
  const orderRepository = new OrderRepository();
362
362
  export { OrderRepository, orderRepository };
363
- export { OrderRepository as OrdersRepository };
@@ -4,5 +4,5 @@
4
4
  * Server-only entry point for order repositories.
5
5
  */
6
6
  export * from "./actions";
7
- export { OrderRepository, OrdersRepository, orderRepository, } from "./repository/orders.repository";
7
+ export { OrderRepository, orderRepository, } from "./repository/orders.repository";
8
8
  export { GET as getOrderTrackingHandler } from "./api/track/[trackingId]/route";
@@ -4,6 +4,6 @@
4
4
  * Server-only entry point for order repositories.
5
5
  */
6
6
  export * from "./actions";
7
- export { OrderRepository, OrdersRepository, orderRepository, } from "./repository/orders.repository";
7
+ export { OrderRepository, orderRepository, } from "./repository/orders.repository";
8
8
  // API route handler — re-export for consumer app router stubs
9
9
  export { GET as getOrderTrackingHandler } from "./api/track/[trackingId]/route";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button, Div, FormField, Heading, Modal, Row, Select, SideDrawer, Stack, Text, Tabs, TabsContent, TabsList, TabsTrigger, DynamicSelect, useToast, } from "../../../ui";
4
+ import { Button, ConfirmDeleteModal, Div, FormField, Heading, Modal, Row, Select, SideDrawer, Stack, Text, Tabs, TabsContent, TabsList, TabsTrigger, DynamicSelect, useToast, } from "../../../ui";
5
5
  import { apiClient } from "../../../http";
6
6
  const CONDITION_OPTIONS = [
7
7
  { value: "new", label: "New" },
@@ -24,6 +24,7 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
24
24
  });
25
25
  const [linkTarget, setLinkTarget] = useState(null);
26
26
  const [children, setChildren] = useState(null);
27
+ const [confirmAction, setConfirmAction] = useState(null);
27
28
  if (isAuction)
28
29
  return null;
29
30
  const groupEndpoint = `${storeProductsEndpoint}/${productId}/group`;
@@ -71,38 +72,48 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
71
72
  setLoading(false);
72
73
  }
73
74
  }
74
- async function dissolveGroup() {
75
- if (!confirm("Dissolve this group? All members will be unlinked. This cannot be undone."))
76
- return;
77
- setLoading(true);
78
- try {
79
- await apiClient.delete(groupEndpoint);
80
- showToast("Group dissolved.", "success");
81
- onGroupChanged();
82
- }
83
- catch (e) {
84
- showToast(e?.message ?? "Failed to dissolve group.", "error");
85
- }
86
- finally {
87
- setLoading(false);
88
- }
75
+ function requestDissolveGroup() {
76
+ setConfirmAction({
77
+ title: "Dissolve Group",
78
+ message: "Dissolve this group? All members will be unlinked. This cannot be undone.",
79
+ onConfirm: async () => {
80
+ setConfirmAction(null);
81
+ setLoading(true);
82
+ try {
83
+ await apiClient.delete(groupEndpoint);
84
+ showToast("Group dissolved.", "success");
85
+ onGroupChanged();
86
+ }
87
+ catch (e) {
88
+ showToast(e?.message ?? "Failed to dissolve group.", "error");
89
+ }
90
+ finally {
91
+ setLoading(false);
92
+ }
93
+ },
94
+ });
89
95
  }
90
- async function unlinkChild(childId) {
91
- if (!confirm("Remove this listing from the group?"))
92
- return;
93
- setLoading(true);
94
- try {
95
- await apiClient.delete(`${childrenEndpoint}/${childId}`);
96
- showToast("Listing removed from group.", "success");
97
- setChildren((prev) => prev?.filter((c) => c.id !== childId) ?? null);
98
- onGroupChanged();
99
- }
100
- catch (e) {
101
- showToast(e?.message ?? "Failed to unlink.", "error");
102
- }
103
- finally {
104
- setLoading(false);
105
- }
96
+ function requestUnlinkChild(childId) {
97
+ setConfirmAction({
98
+ title: "Remove from Group",
99
+ message: "Remove this listing from the group?",
100
+ onConfirm: async () => {
101
+ setConfirmAction(null);
102
+ setLoading(true);
103
+ try {
104
+ await apiClient.delete(`${childrenEndpoint}/${childId}`);
105
+ showToast("Listing removed from group.", "success");
106
+ setChildren((prev) => prev?.filter((c) => c.id !== childId) ?? null);
107
+ onGroupChanged();
108
+ }
109
+ catch (e) {
110
+ showToast(e?.message ?? "Failed to unlink.", "error");
111
+ }
112
+ finally {
113
+ setLoading(false);
114
+ }
115
+ },
116
+ });
106
117
  }
107
118
  async function addCreateChild() {
108
119
  if (!createForm.title || !createForm.price) {
@@ -152,21 +163,26 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
152
163
  setLoading(false);
153
164
  }
154
165
  }
155
- async function leaveGroup() {
156
- if (!confirm("Leave this group? This listing will become standalone."))
157
- return;
158
- setLoading(true);
159
- try {
160
- await apiClient.delete(`${storeProductsEndpoint}/${productId}/group/leave`);
161
- showToast("Left the group.", "success");
162
- onGroupChanged();
163
- }
164
- catch (e) {
165
- showToast(e?.message ?? "Failed to leave group.", "error");
166
- }
167
- finally {
168
- setLoading(false);
169
- }
166
+ function requestLeaveGroup() {
167
+ setConfirmAction({
168
+ title: "Leave Group",
169
+ message: "Leave this group? This listing will become standalone.",
170
+ onConfirm: async () => {
171
+ setConfirmAction(null);
172
+ setLoading(true);
173
+ try {
174
+ await apiClient.delete(`${storeProductsEndpoint}/${productId}/group/leave`);
175
+ showToast("Left the group.", "success");
176
+ onGroupChanged();
177
+ }
178
+ catch (e) {
179
+ showToast(e?.message ?? "Failed to leave group.", "error");
180
+ }
181
+ finally {
182
+ setLoading(false);
183
+ }
184
+ },
185
+ });
170
186
  }
171
187
  async function loadLinkOptions(query, page) {
172
188
  const params = new URLSearchParams({ page: String(page), pageSize: "25" });
@@ -185,7 +201,7 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
185
201
  loadChildren();
186
202
  }, className: "w-full flex items-center justify-between py-2 text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Group Settings" }), _jsx("span", { className: "text-xs text-zinc-400 dark:text-zinc-400 group-hover:text-zinc-600 dark:group-hover:text-zinc-300 transition-colors", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: "mt-3 p-4 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50/50 dark:bg-zinc-800/30", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { align: "start", gap: "sm", className: "flex-wrap", children: [_jsx("div", { className: "flex-1 min-w-[200px]", children: _jsx(FormField, { name: "groupTitle", label: "Group title", type: "text", value: editTitle, onChange: setEditTitle, placeholder: "e.g. Human Toy Complete Set" }) }), _jsx("div", { className: "pt-6", children: _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: saveTitle, isLoading: loading, children: "Save title" }) })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "text-xs font-semibold text-zinc-600 dark:text-zinc-400 mb-2", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), loading && !children ? (_jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400", children: "Loading\u2026" })) : (_jsxs("div", { className: "divide-y divide-zinc-100 dark:divide-zinc-800", children: [_jsxs(Row, { align: "center", gap: "sm", className: "py-2", children: [_jsx("span", { className: "rounded bg-[var(--appkit-color-primary,#6366f1)]/10 text-[var(--appkit-color-primary,#6366f1)] text-[10px] font-semibold px-1.5 py-0.5", children: "Parent" }), _jsx(Text, { className: "text-sm text-zinc-800 dark:text-zinc-200 flex-1", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", className: "py-2", children: [child.images?.[0] ? (
187
203
  // eslint-disable-next-line @next/next/no-img-element
188
- _jsx("img", { src: child.images[0], alt: child.title, className: "w-8 h-8 rounded-full object-cover border border-zinc-200 dark:border-zinc-700" })) : (_jsx("div", { className: "w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-800" })), _jsx(Text, { className: "text-sm text-zinc-800 dark:text-zinc-200 flex-1 truncate", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => unlinkChild(child.id), isLoading: loading, className: "text-red-500 hover:text-red-600 text-xs", children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", className: "flex-wrap", children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: dissolveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600", children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { className: "text-sm text-zinc-600 dark:text-zinc-400", children: ["Part of:", " ", _jsx("span", { className: "font-medium text-zinc-800 dark:text-zinc-200", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: ["Parent listing: ", _jsx("code", { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: leaveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600 w-fit", children: "Leave group" })] }))] })), (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })) : (_jsx(Modal, { open: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", size: "lg", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) }))] }));
204
+ _jsx("img", { src: child.images[0], alt: child.title, className: "w-8 h-8 rounded-full object-cover border border-zinc-200 dark:border-zinc-700" })) : (_jsx("div", { className: "w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-800" })), _jsx(Text, { className: "text-sm text-zinc-800 dark:text-zinc-200 flex-1 truncate", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: "text-red-500 hover:text-red-600 text-xs", children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", className: "flex-wrap", children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600", children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { className: "text-sm text-zinc-600 dark:text-zinc-400", children: ["Part of:", " ", _jsx("span", { className: "font-medium text-zinc-800 dark:text-zinc-200", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: ["Parent listing: ", _jsx("code", { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600 w-fit", children: "Leave group" })] }))] })), (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })) : (_jsx(Modal, { open: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", size: "lg", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })), confirmAction && (_jsx(ConfirmDeleteModal, { isOpen: true, title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onClose: () => setConfirmAction(null), isDeleting: loading }))] }));
189
205
  }
190
206
  function AddChildContent({ addTab, setAddTab, createForm, setCreateForm, productSlug, linkTarget, setLinkTarget, loadLinkOptions, onAddCreate, onAddLink, loading, }) {
191
207
  return (_jsx(Stack, { gap: "md", children: _jsxs(Tabs, { value: addTab, onChange: (v) => setAddTab(v), children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "create", children: "Create new child" }), _jsx(TabsTrigger, { value: "link", children: "Link existing" })] }), _jsx(TabsContent, { value: "create", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(FormField, { name: "childTitle", label: "Title *", type: "text", value: createForm.title, onChange: (v) => setCreateForm({ ...createForm, title: v }), placeholder: `${productSlug}-part` }), _jsx(FormField, { name: "childPrice", label: "Price (\u20B9) *", type: "number", value: createForm.price, onChange: (v) => setCreateForm({ ...createForm, price: v }), placeholder: "0" }), _jsx(Select, { label: "Condition", value: createForm.condition, onValueChange: (v) => setCreateForm({ ...createForm, condition: v }), options: CONDITION_OPTIONS }), _jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400/80", children: "Other fields (category, brand, shipping, return policy) are inherited from this parent listing. Need more control? Edit the full listing after saving." }), _jsx(Button, { type: "button", onClick: onAddCreate, isLoading: loading, disabled: !createForm.title || !createForm.price, children: "Create and link child" })] }) }), _jsx(TabsContent, { value: "link", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Search your existing products or pre-orders. Auctions cannot be linked." }), _jsx(DynamicSelect, { value: linkTarget, onChange: (v) => setLinkTarget(v), loadOptions: loadLinkOptions, placeholder: "Search listings\u2026", searchPlaceholder: "Type title or slug\u2026", noResultsText: "No matching listings found", ariaLabel: "Listing to link" }), _jsx(Button, { type: "button", onClick: onAddLink, isLoading: loading, disabled: !linkTarget, children: "Link to group" })] }) })] }) }));
@@ -256,6 +256,4 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
256
256
  /** Cloud Functions: returns refs for draft products older than `cutoff` (for 30-day prune). */
257
257
  getStaleDraftRefs(cutoff: Date): Promise<DocumentReference[]>;
258
258
  }
259
- export declare class ProductsRepository extends ProductRepository {
260
- }
261
259
  export declare const productRepository: ProductRepository;
@@ -573,6 +573,4 @@ ProductRepository.FILTER_ALIASES = {
573
573
  ].join(",");
574
574
  },
575
575
  };
576
- export class ProductsRepository extends ProductRepository {
577
- }
578
576
  export const productRepository = new ProductRepository();
@@ -4,6 +4,6 @@
4
4
  * Server-only entry point — repositories and API route handlers.
5
5
  */
6
6
  export * from "./actions";
7
- export { ProductRepository, ProductsRepository, productRepository, } from "./repository/products.repository";
7
+ export { ProductRepository, productRepository, } from "./repository/products.repository";
8
8
  export { GET, POST } from "./api/route";
9
9
  export { GET as productItemGET, PATCH as productItemPATCH, DELETE as productItemDELETE, } from "./api/[id]/route";
@@ -4,6 +4,6 @@
4
4
  * Server-only entry point — repositories and API route handlers.
5
5
  */
6
6
  export * from "./actions";
7
- export { ProductRepository, ProductsRepository, productRepository, } from "./repository/products.repository";
7
+ export { ProductRepository, productRepository, } from "./repository/products.repository";
8
8
  export { GET, POST } from "./api/route";
9
9
  export { GET as productItemGET, PATCH as productItemPATCH, DELETE as productItemDELETE, } from "./api/[id]/route";
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { Pencil, Trash2, ToggleLeft, ToggleRight } from "lucide-react";
5
- import { BaseListingCard, Div, Row, Span, Text } from "../../../ui";
5
+ import { BaseListingCard, ConfirmDeleteModal, Div, Row, Span, Text } from "../../../ui";
6
6
  import { useLongPress } from "../../../react/hooks/useLongPress";
7
7
  const TYPE_COLORS = {
8
8
  percentage: { card: "bg-purple-50 border-purple-200 dark:bg-purple-950/30 dark:border-purple-800", code: "bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-200 border-purple-300 dark:border-purple-700" },
@@ -91,6 +91,7 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
91
91
  const [copied, setCopied] = useState(false);
92
92
  const [claiming, setClaiming] = useState(false);
93
93
  const [busy, setBusy] = useState(null);
94
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
94
95
  const longPress = useLongPress(() => onSelect?.(n.id, !isSelected));
95
96
  const expiry = formatDateSafe(n.expiresAt);
96
97
  const discountLabel = formatDiscount(n, labels);
@@ -144,11 +145,10 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
144
145
  setBusy(null);
145
146
  }
146
147
  };
147
- const handleDelete = async () => {
148
+ const handleDeleteConfirmed = async () => {
148
149
  if (!onDelete)
149
150
  return;
150
- if (typeof window !== "undefined" && !window.confirm(labels.deleteConfirm))
151
- return;
151
+ setDeleteConfirmOpen(false);
152
152
  setBusy("delete");
153
153
  try {
154
154
  await onDelete(n.id);
@@ -161,5 +161,5 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
161
161
  ? "bg-success-surface text-success"
162
162
  : "bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300"}`, children: n.isActive ? labels.active : labels.inactive }), n.scope && (_jsx(Span, { className: "inline-flex items-center rounded-full bg-zinc-200/70 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-700 dark:bg-zinc-700/60 dark:text-zinc-200", children: n.scope }))] })), _jsxs(Div, { className: "mb-3", children: [_jsx(Text, { className: "text-2xl font-extrabold tracking-tight leading-none", children: discountLabel }), n.name && (_jsx(Text, { className: "text-sm mt-0.5 font-medium opacity-80", children: n.name }))] }), n.description && (_jsx(Text, { className: "text-xs opacity-60 mb-3", children: n.description })), _jsxs("div", { className: `flex items-center gap-2 rounded-lg border border-dashed px-3 py-2 ${colors.code}`, children: [_jsx("span", { className: "flex-1 font-mono text-sm font-bold tracking-widest uppercase select-all", children: n.code || "—" }), n.code && (_jsx("button", { type: "button", onClick: handleCopy, className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-semibold transition-all hover:opacity-80 active:scale-95 bg-white/60 dark:bg-black/20 border border-current/20", "aria-label": "Copy coupon code", children: copied ? labels.copied : labels.copy }))] }), showClaim && n.code && n.isActive && (_jsx("div", { className: "mt-2", children: _jsx("button", { type: "button", onClick: handleClaim, disabled: claiming, className: "w-full rounded-md px-3 py-1.5 text-xs font-semibold transition-all active:scale-95 bg-[var(--appkit-color-primary-700)] text-[var(--appkit-color-text-on-primary,#fff)] hover:bg-[var(--appkit-color-primary-800)] disabled:opacity-50", "aria-label": `Claim coupon ${n.code} and apply at checkout`, children: claiming ? labels.claiming : labels.claim }) })), _jsxs(Row, { wrap: true, gap: "sm", className: "text-xs opacity-60 mt-2", children: [n.minOrderAmount ? (_jsxs(Span, { children: [labels.minOrder, ": \u20B9", (n.minOrderAmount / 100).toFixed(0)] })) : null, expiry && (_jsxs(Span, { children: [labels.expires, ": ", expiry] })), n.usageLimit ? (_jsxs(Span, { children: [n.usageCount, "/", n.usageLimit, " ", labels.used] })) : n.usageCount > 0 ? (_jsxs(Span, { children: [n.usageCount, " ", labels.used] })) : null] }), hasAdminActions && (_jsxs(Row, { gap: "xs", justify: "end", className: "mt-auto pt-3", children: [onEdit && (_jsx("button", { type: "button", onClick: () => onEdit(n.id), title: labels.edit, "aria-label": labels.edit, className: "rounded-lg p-1.5 text-zinc-500 dark:text-zinc-400 hover:bg-white/60 dark:hover:bg-black/30 hover:text-zinc-800 dark:hover:text-zinc-200 transition-colors", children: _jsx(Pencil, { className: "h-4 w-4" }) })), onToggleActive && (_jsx("button", { type: "button", onClick: handleToggle, disabled: busy === "toggle", title: n.isActive ? labels.deactivate : labels.activate, "aria-label": n.isActive ? labels.deactivate : labels.activate, className: "rounded-lg p-1.5 text-zinc-500 dark:text-zinc-400 hover:bg-white/60 dark:hover:bg-black/30 transition-colors disabled:opacity-50", children: n.isActive
163
163
  ? _jsx(ToggleRight, { className: "h-4 w-4 text-emerald-600 dark:text-emerald-400" })
164
- : _jsx(ToggleLeft, { className: "h-4 w-4" }) })), onDelete && (_jsx("button", { type: "button", onClick: handleDelete, disabled: busy === "delete", title: labels.delete, "aria-label": labels.delete, className: "rounded-lg p-1.5 text-zinc-500 dark:text-zinc-400 hover:bg-red-50 dark:hover:bg-red-950/40 hover:text-red-600 dark:hover:text-red-400 transition-colors disabled:opacity-50", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }))] }));
164
+ : _jsx(ToggleLeft, { className: "h-4 w-4" }) })), onDelete && (_jsx("button", { type: "button", onClick: () => setDeleteConfirmOpen(true), disabled: busy === "delete", title: labels.delete, "aria-label": labels.delete, className: "rounded-lg p-1.5 text-zinc-500 dark:text-zinc-400 hover:bg-red-50 dark:hover:bg-red-950/40 hover:text-red-600 dark:hover:text-red-400 transition-colors disabled:opacity-50", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })), deleteConfirmOpen && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Coupon", message: labels.deleteConfirm, onConfirm: handleDeleteConfirmed, onClose: () => setDeleteConfirmOpen(false), isDeleting: busy === "delete" }))] }));
165
165
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { Search, SlidersHorizontal, X } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
- import { Pagination, SortDropdown, Div, Text, Heading } from "../../../ui";
6
+ import { ListingFilterDrawer, Pagination, SortDropdown, Div, Text, Heading } from "../../../ui";
7
7
  import { usePromotions } from "../hooks/usePromotions";
8
8
  import { CouponCard } from "./CouponCard";
9
9
  import { TABLE_KEYS } from "../../../constants/table-keys";
@@ -34,6 +34,7 @@ export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
34
34
  get: (key) => pendingFilters[key] ?? "",
35
35
  set: (key, value) => setPendingFilters((p) => ({ ...p, [key]: value })),
36
36
  }), [pendingFilters]);
37
+ const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
37
38
  const openFilters = useCallback(() => {
38
39
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
39
40
  setFilterOpen(true);
@@ -97,5 +98,5 @@ export function CouponsIndexListing({ initialCoupons, storeSlug, storeId, }) {
97
98
  minOrder: "Min. order",
98
99
  off: "OFF",
99
100
  freeShipping: "Free Shipping",
100
- } }, coupon.id))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: table.getNumber("page", 1), totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), !isLoading && total > 0 && (_jsx(Div, { className: "mt-4 text-center", children: _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400", children: [total, " coupon", total !== 1 ? "s" : "", " available"] }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-6", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Discount Type" }), _jsxs("div", { className: "space-y-2", children: [COUPON_TYPES.map((t) => (_jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "coupon-type", value: t.value, checked: pendingTable.get(TABLE_KEYS.TYPE) === t.value, onChange: () => { pendingTable.set(TABLE_KEYS.TYPE, t.value); }, className: "accent-primary" }), t.label] }, t.value))), pendingTable.get(TABLE_KEYS.TYPE) && (_jsx("button", { type: "button", onClick: () => { pendingTable.set(TABLE_KEYS.TYPE, ""); }, className: "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 underline", children: "Clear type" }))] })] }), _jsxs(_Fragment, { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Valid Date Range" }), _jsxs("div", { className: "space-y-3", children: [_jsxs(_Fragment, { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "From date" }), _jsx("input", { type: "date", value: pendingTable.get(TABLE_KEYS.DATE_FROM) || "", onChange: (e) => { pendingTable.set(TABLE_KEYS.DATE_FROM, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] }), _jsxs(_Fragment, { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "To date" }), _jsx("input", { type: "date", value: pendingTable.get(TABLE_KEYS.DATE_TO) || "", onChange: (e) => { pendingTable.set(TABLE_KEYS.DATE_TO, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] })] })] })] }), _jsxs("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5 flex gap-2", children: [_jsx("button", { type: "button", onClick: clearPending, className: "flex-1 rounded-lg border border-zinc-300 dark:border-slate-600 py-2.5 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: "Clear all" }), _jsx("button", { type: "button", onClick: applyFilters, className: "flex-1 rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply Filters" })] })] })] }))] }));
101
+ } }, coupon.id))) })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: table.getNumber("page", 1), totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), !isLoading && total > 0 && (_jsx(Div, { className: "mt-4 text-center", children: _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400", children: [total, " coupon", total !== 1 ? "s" : "", " available"] }) }))] }), _jsxs(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearPending, activeCount: activeFilterCount, children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Discount Type" }), _jsxs("div", { className: "space-y-2", children: [COUPON_TYPES.map((t) => (_jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx("input", { type: "radio", name: "coupon-type", value: t.value, checked: pendingTable.get(TABLE_KEYS.TYPE) === t.value, onChange: () => { pendingTable.set(TABLE_KEYS.TYPE, t.value); }, className: "accent-primary" }), t.label] }, t.value))), pendingTable.get(TABLE_KEYS.TYPE) && (_jsx("button", { type: "button", onClick: () => { pendingTable.set(TABLE_KEYS.TYPE, ""); }, className: "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 underline", children: "Clear type" }))] })] }), _jsxs(_Fragment, { children: [_jsx(Heading, { level: 6, className: "text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-3", children: "Valid Date Range" }), _jsxs("div", { className: "space-y-3", children: [_jsxs(_Fragment, { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "From date" }), _jsx("input", { type: "date", value: pendingTable.get(TABLE_KEYS.DATE_FROM) || "", onChange: (e) => { pendingTable.set(TABLE_KEYS.DATE_FROM, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] }), _jsxs(_Fragment, { children: [_jsx("label", { className: "block text-xs text-zinc-500 dark:text-zinc-400 mb-1", children: "To date" }), _jsx("input", { type: "date", value: pendingTable.get(TABLE_KEYS.DATE_TO) || "", onChange: (e) => { pendingTable.set(TABLE_KEYS.DATE_TO, e.target.value); }, className: "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:ring-2 focus:ring-primary" })] })] })] })] })] }));
101
102
  }
@@ -1,9 +1,8 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
- import { X } from "lucide-react";
5
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
- import { ListingToolbar, Pagination, Text } from "../../../ui";
5
+ import { ListingFilterDrawer, ListingToolbar, Pagination, Text } from "../../../ui";
7
6
  import { ReviewCard } from "./ReviewsList";
8
7
  import { ReviewFilters, REVIEW_PUBLIC_SORT_OPTIONS } from "./ReviewFilters";
9
8
  import { useReviews } from "../hooks/useReviews";
@@ -103,5 +102,5 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
103
102
  value: opt.value,
104
103
  label: SORT_OPTION_LABELS[opt.key] ?? opt.key,
105
104
  }));
106
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: _jsxs("div", { className: "p-4 space-y-3", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] }) }, i))) })) : reviews.length === 0 ? (_jsx(Text, { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No reviews found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
105
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: _jsxs("div", { className: "p-4 space-y-3", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] }) }, i))) })) : reviews.length === 0 ? (_jsx(Text, { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No reviews found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) })] }));
107
106
  }
@@ -106,4 +106,3 @@ declare class ReviewRepository extends BaseRepository<ReviewDocument> {
106
106
  }
107
107
  declare const reviewRepository: ReviewRepository;
108
108
  export { ReviewRepository, reviewRepository };
109
- export { ReviewRepository as ReviewsRepository };
@@ -233,4 +233,3 @@ ReviewRepository.SIEVE_FIELDS = {
233
233
  };
234
234
  const reviewRepository = new ReviewRepository();
235
235
  export { ReviewRepository, reviewRepository };
236
- export { ReviewRepository as ReviewsRepository };
@@ -4,6 +4,6 @@
4
4
  * Server-only entry point — repositories and API route handlers.
5
5
  */
6
6
  export * from "./actions";
7
- export { ReviewRepository, ReviewsRepository, reviewRepository, } from "./repository/reviews.repository";
7
+ export { ReviewRepository, reviewRepository, } from "./repository/reviews.repository";
8
8
  export { GET as reviewsGET, GET, POST } from "./api/route";
9
9
  export { reviewItemGET, reviewItemPATCH, reviewItemDELETE, } from "./api/[id]/route";
@@ -4,6 +4,6 @@
4
4
  * Server-only entry point — repositories and API route handlers.
5
5
  */
6
6
  export * from "./actions";
7
- export { ReviewRepository, ReviewsRepository, reviewRepository, } from "./repository/reviews.repository";
7
+ export { ReviewRepository, reviewRepository, } from "./repository/reviews.repository";
8
8
  export { GET as reviewsGET, GET, POST } from "./api/route";
9
9
  export { reviewItemGET, reviewItemPATCH, reviewItemDELETE, } from "./api/[id]/route";