@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,11 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
- import { X } from "lucide-react";
5
4
  import { useMutation, useQueryClient } from "@tanstack/react-query";
6
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
7
6
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
8
- import { BulkActionBar, ConfirmDeleteModal, ListingToolbar, ListingViewShell, Pagination, RowActionMenu, Text, useToast } from "../../../ui";
7
+ import { BulkActionBar, ConfirmDeleteModal, ListingToolbar, ListingViewShell, Pagination, RowActionMenu, Text, useToast, ListingFilterDrawer } from "../../../ui";
9
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
9
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
11
10
  import { ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
@@ -29,11 +28,6 @@ function maskIp(ip) {
29
28
  return `${parts[0]}.${parts[1]}.${parts[2]}.*`;
30
29
  return ip.replace(/:\w+$/, ":*");
31
30
  }
32
- function SessionsFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
33
- if (!filterOpen)
34
- return null;
35
- return (_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: "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", 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 space-y-5", children: _jsxs("div", { className: "space-y-2", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400", children: "Status" }), _jsx("div", { className: "flex flex-wrap gap-2", children: [{ label: "All", value: "" }, { label: "Active only", value: "true" }].map((opt) => (_jsx("button", { type: "button", onClick: () => setPendingFilters((p) => ({ ...p, isActive: opt.value })), className: `rounded-full px-3 py-1 text-xs font-medium border transition-colors ${(pendingFilters.isActive || "") === opt.value ? "bg-primary text-white border-primary" : "border-zinc-300 dark:border-slate-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: opt.label }, opt.label))) })] }) }), _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})` : ""] }) })] })] }));
36
- }
37
31
  export function AdminSessionsView({ children, ...props }) {
38
32
  const hasChildren = React.Children.count(children) > 0;
39
33
  const [view, setView] = useState("table");
@@ -130,6 +124,6 @@ export function AdminSessionsView({ children, ...props }) {
130
124
  destructive: ROW_ACTION_META[ROW_ACTION_ID.REVOKE].destructive,
131
125
  onClick: () => setRevokeTarget(row),
132
126
  },
133
- ] })) })] }), _jsx(SessionsFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(ConfirmDeleteModal, { isOpen: Boolean(revokeTarget), onClose: () => setRevokeTarget(null), onConfirm: () => { if (revokeTarget)
127
+ ] })) })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs("div", { className: "space-y-2", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400", children: "Status" }), _jsx("div", { className: "flex flex-wrap gap-2", children: [{ label: "All", value: "" }, { label: "Active only", value: "true" }].map((opt) => (_jsx("button", { type: "button", onClick: () => setPendingFilters((p) => ({ ...p, isActive: opt.value })), className: `rounded-full px-3 py-1 text-xs font-medium border transition-colors ${(pendingFilters.isActive || "") === opt.value ? "bg-primary text-white border-primary" : "border-zinc-300 dark:border-slate-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: opt.label }, opt.label))) })] }) })] }), _jsx(ConfirmDeleteModal, { isOpen: Boolean(revokeTarget), onClose: () => setRevokeTarget(null), onConfirm: () => { if (revokeTarget)
134
128
  revokeMutation.mutate(revokeTarget.id); }, isDeleting: revokeMutation.isPending, confirmText: "Revoke", title: "Revoke session?", message: "This will sign the user out of this device immediately.", variant: "danger" })] }));
135
129
  }
@@ -1,12 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
- import { X } from "lucide-react";
5
4
  import { useMutation, useQueryClient } from "@tanstack/react-query";
6
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
7
6
  import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
8
7
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
9
- import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu, useToast } from "../../../ui";
8
+ import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu, useToast, ListingFilterDrawer } from "../../../ui";
10
9
  import { AdminViewCards } from "./AdminViewCards";
11
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
12
11
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
@@ -25,11 +24,6 @@ const SORT_OPTIONS = [
25
24
  { value: "storeName", label: "Name A–Z" },
26
25
  ];
27
26
  const STATUS_OPTIONS = ADMIN_STORE_STATUS_TABS;
28
- function StoresFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
29
- if (!filterOpen)
30
- return null;
31
- return (_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: "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", 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 space-y-5", children: _jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }) }), _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})` : ""] }) })] })] }));
32
- }
33
27
  export function AdminStoresView({ children, ...props }) {
34
28
  const hasChildren = React.Children.count(children) > 0;
35
29
  const [view, setView] = useState("table");
@@ -139,7 +133,7 @@ export function AdminStoresView({ children, ...props }) {
139
133
  disabled: isSuspended || suspendStore.isPending,
140
134
  },
141
135
  ] }));
142
- } })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No stores found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(StoresFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminStoreEditorView, { open: isEditOpen, onClose: closePanel, storeId: editId ?? undefined, storeName: panelRow?.primary, currentStatus: panelRow?.status?.toLowerCase(), currentIsVerified: Boolean(panelRow?._raw?.isVerified), currentCapabilities: Array.isArray(panelRow?._raw?.capabilities)
136
+ } })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No stores found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }) })] }), _jsx(AdminStoreEditorView, { open: isEditOpen, onClose: closePanel, storeId: editId ?? undefined, storeName: panelRow?.primary, currentStatus: panelRow?.status?.toLowerCase(), currentIsVerified: Boolean(panelRow?._raw?.isVerified), currentCapabilities: Array.isArray(panelRow?._raw?.capabilities)
143
137
  ? panelRow._raw.capabilities
144
138
  : undefined })] }));
145
139
  }
@@ -7,6 +7,7 @@ import { BulkActionBar, ListingToolbar, Pagination } from "../../../ui";
7
7
  import { useBottomActions } from "../../layout";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
9
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
10
+ import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
10
11
  import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
11
12
  import { DataTable } from "./DataTable";
12
13
  const PAGE_SIZE = 25;
@@ -54,12 +55,7 @@ export function AdminSublistingCategoriesView({ onBulkDelete }) {
54
55
  const totalPages = Math.ceil(total / PAGE_SIZE);
55
56
  const selection = useBulkSelection({ items: rows ?? [], keyExtractor: (r) => r.id });
56
57
  const bulkActions = [
57
- ...(onBulkDelete ? [{
58
- id: "bulk-delete",
59
- label: ACTIONS.ADMIN["delete-sublisting-category"].label,
60
- variant: "danger",
61
- onClick: async () => { await onBulkDelete(selection.selectedIds); selection.clearSelection(); },
62
- }] : []),
58
+ ...(onBulkDelete ? [buildBulkAction(ACTIONS.ADMIN["delete-sublisting-category"], async () => { await onBulkDelete(selection.selectedIds); selection.clearSelection(); })] : []),
63
59
  ];
64
60
  useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
65
61
  return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { searchValue: searchInput, searchPlaceholder: "Search sub-listing categories\u2026", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => {
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { useMutation, useQuery } from "@tanstack/react-query";
5
- import { Button, Form, Input, StackedViewShell, useToast, } from "../../../ui";
5
+ import { Button, ConfirmDeleteModal, Form, Input, StackedViewShell, useToast, } from "../../../ui";
6
6
  import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
7
7
  import { ImageUpload } from "../../media/upload/ImageUpload";
8
8
  import { useMediaUpload } from "../../media";
@@ -15,6 +15,7 @@ export function AdminSublistingCategoryEditorView({ categoryId, onSaved, onDelet
15
15
  const [itemCode, setItemCode] = React.useState("");
16
16
  const [description, setDescription] = React.useState("");
17
17
  const [coverImage, setCoverImage] = React.useState("");
18
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
18
19
  const categoryQuery = useQuery({
19
20
  queryKey: ["admin", "sublisting-category", categoryId],
20
21
  queryFn: async () => {
@@ -69,22 +70,18 @@ export function AdminSublistingCategoryEditorView({ categoryId, onSaved, onDelet
69
70
  const { upload } = useMediaUpload();
70
71
  const isSubmitting = saveMutation.isPending || categoryQuery.isLoading;
71
72
  const { shellCtx, setFieldError, clearErrors } = useFormShellState();
72
- return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Sub-listing Category" : "New Sub-listing Category", sections: [
73
- _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
74
- e.preventDefault();
75
- clearErrors();
76
- if (!name.trim()) {
77
- setFieldError("name", "Category name is required");
78
- return;
79
- }
80
- saveMutation.mutate();
81
- }, className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(FieldInput, { name: "name", label: "Category name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Base Set Charizard 108/120" }), _jsx(Input, { label: "Item code", value: itemCode, onChange: (e) => setItemCode(e.target.value), placeholder: "e.g. PSA 10, 108/120, STH", helperText: "Grade, card number, or series code. Optional." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description shown on the public category page" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "sublisting-categories", true, {
82
- type: "category-image",
83
- name: name || "sublisting",
84
- }), onChange: setCoverImage }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
85
- if (confirm("Delete this category? All linked listings will be unlinked. This cannot be undone.")) {
86
- deleteMutation.mutate();
87
- }
88
- }, children: "Delete" }))] })] }, "sc-editor-form") }, "sc-ctx"),
89
- ] }));
73
+ return (_jsxs(_Fragment, { children: [_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Sub-listing Category" : "New Sub-listing Category", sections: [
74
+ _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
75
+ e.preventDefault();
76
+ clearErrors();
77
+ if (!name.trim()) {
78
+ setFieldError("name", "Category name is required");
79
+ return;
80
+ }
81
+ saveMutation.mutate();
82
+ }, className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(FieldInput, { name: "name", label: "Category name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Base Set Charizard 108/120" }), _jsx(Input, { label: "Item code", value: itemCode, onChange: (e) => setItemCode(e.target.value), placeholder: "e.g. PSA 10, 108/120, STH", helperText: "Grade, card number, or series code. Optional." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description shown on the public category page" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "sublisting-categories", true, {
83
+ type: "category-image",
84
+ name: name || "sublisting",
85
+ }), onChange: setCoverImage }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => setDeleteConfirmOpen(true), children: "Delete" }))] })] }, "sc-editor-form") }, "sc-ctx"),
86
+ ] }), deleteConfirmOpen && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Sublisting Category", message: "Delete this category? All linked listings will be unlinked. This cannot be undone.", onConfirm: () => { deleteMutation.mutate(); setDeleteConfirmOpen(false); }, onClose: () => setDeleteConfirmOpen(false), isDeleting: deleteMutation.isPending }))] }));
90
87
  }
@@ -1,10 +1,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
- import { X } from "lucide-react";
5
4
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
5
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
7
- import { FilterChipGroup, ListingToolbar, ListingViewShell, Pagination, RowActionMenu, Text } from "../../../ui";
6
+ import { FilterChipGroup, ListingToolbar, ListingViewShell, Pagination, RowActionMenu, Text, ListingFilterDrawer } from "../../../ui";
8
7
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
8
  import { ROW_ACTION_META, ROW_ACTION_ID } from "../../../features/products/constants/action-defs";
10
9
  import { ADMIN_SUPPORT_TICKET_STATUS_TABS, ADMIN_SUPPORT_TICKET_PRIORITY_TABS, } from "../constants/filter-tabs";
@@ -32,11 +31,6 @@ const STATUS_BADGE = {
32
31
  resolved: "bg-success-surface text-success",
33
32
  closed: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
34
33
  };
35
- function SupportTicketsFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
36
- if (!filterOpen)
37
- return null;
38
- return (_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: "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", 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-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_SUPPORT_TICKET_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Priority", tabs: ADMIN_SUPPORT_TICKET_PRIORITY_TABS, value: pendingFilters.priority ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, priority: id })) })] }), _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})` : ""] }) })] })] }));
39
- }
40
34
  function buildTicketColumns(setSelectedRow, setDrawerOpen) {
41
35
  return [
42
36
  {
@@ -146,7 +140,7 @@ export function AdminSupportTicketsView({ children, ...props }) {
146
140
  table.set("sort", v);
147
141
  }, showTableView: true, view: view, onViewChange: (v) => setView(v), 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) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-error-surface px-4 py-3 text-sm text-error dark:border-red-900/60", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No support tickets found", renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
148
142
  { label: ROW_ACTION_META[ROW_ACTION_ID.VIEW].label, onClick: () => { setSelectedRow(row); setDrawerOpen(true); } },
149
- ] })), columns: buildTicketColumns(setSelectedRow, setDrawerOpen) })] }), _jsx(SupportTicketsFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminSupportTicketDetailView, { open: drawerOpen, onClose: () => setDrawerOpen(false), ticketId: selectedRow?.id, subject: selectedRow?.primary, userDisplayName: toStringValue(selectedRow?._raw?.userDisplayName, ""), category: toStringValue(selectedRow?._raw?.category, "general"), currentStatus: selectedRow?.status, currentPriority: toStringValue(selectedRow?._raw?.priority, "normal"), description: toStringValue(selectedRow?._raw?.description, ""), messages: Array.isArray(selectedRow?._raw?.messages)
143
+ ] })), columns: buildTicketColumns(setSelectedRow, setDrawerOpen) })] }), _jsxs(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_SUPPORT_TICKET_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Priority", tabs: ADMIN_SUPPORT_TICKET_PRIORITY_TABS, value: pendingFilters.priority ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, priority: id })) })] })] }), _jsx(AdminSupportTicketDetailView, { open: drawerOpen, onClose: () => setDrawerOpen(false), ticketId: selectedRow?.id, subject: selectedRow?.primary, userDisplayName: toStringValue(selectedRow?._raw?.userDisplayName, ""), category: toStringValue(selectedRow?._raw?.category, "general"), currentStatus: selectedRow?.status, currentPriority: toStringValue(selectedRow?._raw?.priority, "normal"), description: toStringValue(selectedRow?._raw?.description, ""), messages: Array.isArray(selectedRow?._raw?.messages)
150
144
  ? selectedRow._raw.messages
151
145
  : [], internalNotes: toStringValue(selectedRow?._raw?.internalNotes, "") || undefined, orderId: toStringValue(selectedRow?._raw?.orderId, "") || undefined })] }));
152
146
  }
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
- import { X, UserPlus } from "lucide-react";
4
+ import { UserPlus } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
7
- import { BulkActionBar, Button, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu, } from "../../../ui";
7
+ import { BulkActionBar, Button, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu, ListingFilterDrawer } from "../../../ui";
8
8
  import { useBottomActions } from "../../layout";
9
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
10
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
11
+ import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
11
12
  import { ALL_TAB } from "../constants/filter-tabs";
12
13
  import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
13
14
  import { DataTable } from "./DataTable";
@@ -45,11 +46,6 @@ const GROUP_TABS = [
45
46
  function formatGroup(group) {
46
47
  return group.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
47
48
  }
48
- function TeamFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingGroup, setPendingGroup, applyFilters, }) {
49
- if (!filterOpen)
50
- return null;
51
- return (_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: "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", 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(FilterChipGroup, { label: "Permission Group", tabs: GROUP_TABS, value: pendingGroup, onChange: (id) => setPendingGroup(id) }) }), _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})` : ""] }) })] })] }));
52
- }
53
49
  export function AdminTeamView({ children, onBulkRemove, ...props }) {
54
50
  const hasChildren = React.Children.count(children) > 0;
55
51
  const [view, setView] = useState("table");
@@ -133,12 +129,7 @@ export function AdminTeamView({ children, onBulkRemove, ...props }) {
133
129
  const totalPages = Math.ceil(total / PAGE_SIZE);
134
130
  const selection = useBulkSelection({ items: rows, keyExtractor: (r) => r.id });
135
131
  const bulkActions = [
136
- ...(onBulkRemove ? [{
137
- id: "bulk-remove",
138
- label: ACTIONS.ADMIN["remove-team-member"].label,
139
- variant: "danger",
140
- onClick: async () => { await onBulkRemove(selection.selectedIds); selection.clearSelection(); },
141
- }] : []),
132
+ ...(onBulkRemove ? [buildBulkAction(ACTIONS.ADMIN["remove-team-member"], async () => { await onBulkRemove(selection.selectedIds); selection.clearSelection(); })] : []),
142
133
  ];
143
134
  useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
144
135
  if (hasChildren) {
@@ -151,7 +142,7 @@ export function AdminTeamView({ children, onBulkRemove, ...props }) {
151
142
  label: ACTIONS.ADMIN["edit-team-member"].label,
152
143
  onClick: () => openEdit(row),
153
144
  },
154
- ] })) })] }), _jsx(TeamFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingGroup: pendingGroup, setPendingGroup: setPendingGroup, applyFilters: applyFilters })] }), _jsx(AdminEmployeeEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), mode: inviteMode ? "invite" : "edit", userId: selectedRow?.id, displayName: selectedRow?.primary, currentPermissionGroup: toStringValue(selectedRow?._raw?.permissionGroup, "custom"), currentPermissions: Array.isArray(selectedRow?._raw?.permissions)
145
+ ] })) })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(FilterChipGroup, { label: "Permission Group", tabs: GROUP_TABS, value: pendingGroup, onChange: (id) => setPendingGroup(id) }) })] }), _jsx(AdminEmployeeEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), mode: inviteMode ? "invite" : "edit", userId: selectedRow?.id, displayName: selectedRow?.primary, currentPermissionGroup: toStringValue(selectedRow?._raw?.permissionGroup, "custom"), currentPermissions: Array.isArray(selectedRow?._raw?.permissions)
155
146
  ? selectedRow._raw.permissions
156
147
  : [] })] }));
157
148
  }
@@ -1,11 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useCallback } from "react";
4
- import { X } from "lucide-react";
5
4
  import { useMutation, useQueryClient } from "@tanstack/react-query";
6
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
7
6
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
8
- import { BulkActionBar, Button, FilterChipGroup, Form, FormActions, Input, ListingToolbar, Modal, Pagination, ListingViewShell, RowActionMenu, Text as AppText, useToast } from "../../../ui";
7
+ import { BulkActionBar, Button, FilterChipGroup, Form, FormActions, Input, ListingToolbar, Modal, Pagination, ListingViewShell, RowActionMenu, Text as AppText, useToast, ListingFilterDrawer } from "../../../ui";
9
8
  import { AdminViewCards } from "./AdminViewCards";
10
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
10
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
@@ -25,11 +24,6 @@ const SORT_OPTIONS = [
25
24
  ];
26
25
  const STATUS_OPTIONS = ADMIN_USER_STATUS_TABS;
27
26
  const ROLE_OPTIONS = ADMIN_USER_ROLE_TABS;
28
- function UsersFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
29
- if (!filterOpen)
30
- return null;
31
- return (_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: "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", 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-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Role", tabs: ROLE_OPTIONS, value: pendingFilters.role ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, role: id })) })] }), _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})` : ""] }) })] })] }));
32
- }
33
27
  export function AdminUsersView({ children, ...props }) {
34
28
  const hasChildren = React.Children.count(children) > 0;
35
29
  const [view, setView] = useState("table");
@@ -171,7 +165,7 @@ export function AdminUsersView({ children, ...props }) {
171
165
  disabled: !isBanned || unbanUser.isPending,
172
166
  },
173
167
  ] }));
174
- } })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No users found", onRowClick: (row) => { setSelectedRow(row); setDrawerOpen(true); }, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(UsersFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminUserEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), userId: selectedRow?.id, displayName: selectedRow?.primary, currentRole: toStringValue(selectedRow?._raw?.role, "user"), currentEmailVerified: Boolean(selectedRow?._raw?.emailVerified), ownedStoreId: toStringValue(selectedRow?._raw?.storeId, "") || undefined, ownedStoreName: toStringValue(selectedRow?._raw?.storeName, "") || undefined, currentIsHardBanned: Boolean((selectedRow?._raw?.isDisabled ?? selectedRow?._raw?.disabled) &&
168
+ } })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No users found", onRowClick: (row) => { setSelectedRow(row); setDrawerOpen(true); }, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsxs(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Role", tabs: ROLE_OPTIONS, value: pendingFilters.role ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, role: id })) })] })] }), _jsx(AdminUserEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), userId: selectedRow?.id, displayName: selectedRow?.primary, currentRole: toStringValue(selectedRow?._raw?.role, "user"), currentEmailVerified: Boolean(selectedRow?._raw?.emailVerified), ownedStoreId: toStringValue(selectedRow?._raw?.storeId, "") || undefined, ownedStoreName: toStringValue(selectedRow?._raw?.storeName, "") || undefined, currentIsHardBanned: Boolean((selectedRow?._raw?.isDisabled ?? selectedRow?._raw?.disabled) &&
175
169
  selectedRow?._raw?.hardBanReason), currentHardBanReason: toStringValue(selectedRow?._raw?.hardBanReason, "") || undefined, currentSoftBans: Array.isArray(selectedRow?._raw?.softBans)
176
170
  ? selectedRow._raw.softBans.map((b) => ({
177
171
  action: toStringValue(b.action, ""),
@@ -0,0 +1,58 @@
1
+ export interface AdminListingConfig<TResponse, TRow extends {
2
+ id: string;
3
+ }> {
4
+ filterKeys: string[];
5
+ defaultSort: string;
6
+ pageSize?: number;
7
+ queryKey: readonly unknown[];
8
+ endpoint: string;
9
+ mapRows: (response: TResponse) => TRow[];
10
+ getTotal?: (response: TResponse, rows: TRow[]) => number;
11
+ buildFilters: (filterState: Record<string, string>) => string | undefined;
12
+ }
13
+ export declare function useAdminListing<TResponse, TRow extends {
14
+ id: string;
15
+ }>(config: AdminListingConfig<TResponse, TRow>): {
16
+ view: "table" | "grid" | "list";
17
+ setView: import("react").Dispatch<import("react").SetStateAction<"table" | "grid" | "list">>;
18
+ table: {
19
+ params: URLSearchParams;
20
+ get: (key: string) => string;
21
+ getNumber: (key: string, fallback?: number) => number;
22
+ set: (key: string, value: string) => void;
23
+ setMany: (updates: Record<string, string>) => void;
24
+ clear: (keys?: string[]) => void;
25
+ setPage: (page: number) => void;
26
+ setPageSize: (size: number) => void;
27
+ setSort: (sort: string) => void;
28
+ buildSieveParams: (sieveFilters: string) => string;
29
+ buildSearchParams: () => string;
30
+ };
31
+ panel: import("../../..").PanelUrlSync;
32
+ searchInput: string;
33
+ setSearchInput: import("react").Dispatch<import("react").SetStateAction<string>>;
34
+ commitSearch: () => void;
35
+ filterOpen: boolean;
36
+ setFilterOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
37
+ openFilters: () => void;
38
+ applyFilters: () => void;
39
+ clearFilters: () => void;
40
+ pendingFilters: Record<string, string>;
41
+ setPendingFilters: import("react").Dispatch<import("react").SetStateAction<Record<string, string>>>;
42
+ activeFilterCount: number;
43
+ hasActiveState: boolean;
44
+ resetAll: () => void;
45
+ rows: TRow[];
46
+ total: number;
47
+ isLoading: boolean;
48
+ errorMessage: string | undefined;
49
+ refetch: () => void;
50
+ currentPage: number;
51
+ totalPages: number;
52
+ selection: import("../../../react").UseBulkSelectionReturn;
53
+ defaultSort: string;
54
+ pageSize: number;
55
+ };
56
+ export type AdminListingReturn<TRow extends {
57
+ id: string;
58
+ }> = ReturnType<typeof useAdminListing<unknown, TRow>>;
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { useState, useCallback } from "react";
3
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
4
+ import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
5
+ import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
6
+ import { useAdminListingData } from "./useAdminListingData";
7
+ export function useAdminListing(config) {
8
+ const { filterKeys, defaultSort, pageSize = 25, queryKey, endpoint, mapRows, getTotal, buildFilters, } = config;
9
+ const [view, setView] = useState("table");
10
+ const table = useUrlTable({ defaults: { pageSize: String(pageSize), sort: defaultSort } });
11
+ const panel = usePanelUrlSync();
12
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
13
+ const [filterOpen, setFilterOpen] = useState(false);
14
+ const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(filterKeys.map((k) => [k, table.get(k)])));
15
+ const openFilters = useCallback(() => {
16
+ setPendingFilters(Object.fromEntries(filterKeys.map((k) => [k, table.get(k)])));
17
+ setFilterOpen(true);
18
+ }, [filterKeys, table]);
19
+ const applyFilters = useCallback(() => {
20
+ const updates = { page: "1" };
21
+ for (const k of filterKeys)
22
+ updates[k] = pendingFilters[k] ?? "";
23
+ table.setMany(updates);
24
+ setFilterOpen(false);
25
+ }, [filterKeys, pendingFilters, table]);
26
+ const clearFilters = useCallback(() => {
27
+ setPendingFilters(Object.fromEntries(filterKeys.map((k) => [k, ""])));
28
+ }, [filterKeys]);
29
+ const resetAll = useCallback(() => {
30
+ const updates = { q: "", sort: "" };
31
+ for (const k of filterKeys)
32
+ updates[k] = "";
33
+ table.setMany(updates);
34
+ setSearchInput("");
35
+ }, [filterKeys, table]);
36
+ const commitSearch = useCallback(() => {
37
+ table.set("q", searchInput.trim());
38
+ }, [searchInput, table]);
39
+ const activeFilterCount = filterKeys.filter((k) => !!table.get(k)).length;
40
+ const hasActiveState = !!table.get("q") || table.get("sort") !== defaultSort || activeFilterCount > 0;
41
+ const currentFilterState = Object.fromEntries(filterKeys.map((k) => [k, table.get(k)]));
42
+ const filters = buildFilters(currentFilterState);
43
+ const { rows, total, isLoading, errorMessage, refetch } = useAdminListingData({
44
+ queryKey,
45
+ endpoint,
46
+ page: table.getNumber("page", 1),
47
+ pageSize,
48
+ sorts: table.get("sort") || defaultSort,
49
+ filters,
50
+ q: table.get("q") || undefined,
51
+ mapRows,
52
+ getTotal,
53
+ });
54
+ const currentPage = table.getNumber("page", 1);
55
+ const totalPages = Math.ceil(total / pageSize);
56
+ const selection = useBulkSelection({ items: rows, keyExtractor: (r) => r.id });
57
+ return {
58
+ view, setView,
59
+ table,
60
+ panel,
61
+ searchInput, setSearchInput, commitSearch,
62
+ filterOpen, setFilterOpen, openFilters, applyFilters, clearFilters,
63
+ pendingFilters, setPendingFilters,
64
+ activeFilterCount, hasActiveState, resetAll,
65
+ rows, total, isLoading, errorMessage, refetch,
66
+ currentPage, totalPages,
67
+ selection,
68
+ defaultSort,
69
+ pageSize,
70
+ };
71
+ }
@@ -100,6 +100,7 @@ export interface UserDocument {
100
100
  scamAwarenessAcknowledgedAt?: Date;
101
101
  /** User-controlled notification channel and type preferences. */
102
102
  notificationPreferences?: import("../../account/types").NotificationPreferences;
103
+ dismissedBannerHash?: string;
103
104
  }
104
105
  export interface UserSoftBan {
105
106
  action: BannedAction;
@@ -1,10 +1,9 @@
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
5
  import { useBlogPosts } from "../hooks/useBlog";
7
- import { ListingToolbar, Pagination, Text } from "../../../ui";
6
+ import { ListingFilterDrawer, ListingToolbar, Pagination, Text } from "../../../ui";
8
7
  import { ROUTES } from "../../../next";
9
8
  import { BlogCard } from "./BlogListView";
10
9
  import { BlogFilters, BLOG_PUBLIC_SORT_OPTIONS } from "./BlogFilters";
@@ -26,9 +25,7 @@ function renderBlogGrid(props) {
26
25
  }
27
26
  function renderBlogFilterDrawer(props) {
28
27
  const { filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable } = props;
29
- if (!filterOpen)
30
- return null;
31
- return (_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(BlogFilters, { table: pendingTable, variant: "public" }) }), _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})` : ""] }) })] })] }));
28
+ return (_jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(BlogFilters, { table: pendingTable, variant: "public" }) }));
32
29
  }
33
30
  const PAGE_SIZE = 24;
34
31
  const DEFAULT_SORT = sortBy(BLOG_FIELDS.PUBLISHED_AT);
@@ -1,10 +1,9 @@
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 { useCategoriesFiltered } from "../hooks/useCategories";
6
5
  import { ROUTES } from "../../../next";
7
- import { ListingToolbar, Pagination, Text } from "../../../ui";
6
+ import { ListingFilterDrawer, ListingToolbar, Pagination, Text } from "../../../ui";
8
7
  import { CategoryCard } from "./CategoryGrid";
9
8
  import { CategoryFilters } from "./CategoryFilters";
10
9
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
@@ -28,9 +27,7 @@ function renderCategoryGrid(props) {
28
27
  }
29
28
  function renderCategoryFilterDrawer(props) {
30
29
  const { filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable } = props;
31
- if (!filterOpen)
32
- return null;
33
- return (_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(CategoryFilters, { table: pendingTable, variant: "public" }) }), _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})` : ""] }) })] })] }));
30
+ return (_jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(CategoryFilters, { table: pendingTable, variant: "public" }) }));
34
31
  }
35
32
  function getNumericParam(table, key) {
36
33
  return table.get(key) ? Number(table.get(key)) : undefined;
@@ -1,12 +1,12 @@
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 React, { useState, useCallback } from "react";
4
- import { Plus, X } from "lucide-react";
4
+ import { Plus } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
7
7
  import { AdminViewCards } from "../../admin/components/AdminViewCards";
8
8
  import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
9
- import { BulkActionBar, Button, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, SideDrawer } from "../../../ui";
9
+ import { BulkActionBar, Button, FilterChipGroup, ListingFilterDrawer, ListingToolbar, Pagination, ListingViewShell, SideDrawer } from "../../../ui";
10
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
11
  import { ADMIN_EVENT_STATUS_TABS } from "../../admin/constants/filter-tabs";
12
12
  import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../../admin/hooks/useAdminListingData";
@@ -108,5 +108,5 @@ export function AdminEventsView({ children, getRowHref, ...props }) {
108
108
  ] } } : {});
109
109
  return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events by title or type", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs(Button, { size: "sm", onClick: openCreatePanel, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Event"] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
110
110
  { id: "delete", label: "Delete Selected", variant: "secondary", onClick: () => { selection.clearSelection(); } },
111
- ] }), 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) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-error-surface px-4 py-3 text-sm text-error dark:border-red-900/60", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No events found", onRowClick: (row) => openEditPanel(row.id) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No events found", onRowClick: undefined, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), 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: "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", 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-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Type", tabs: TYPE_OPTIONS, value: pendingFilters.type ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, type: id })) })] }), _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})` : ""] }) })] })] })), _jsx(SideDrawer, { isOpen: isCreateOpen || isEditOpen, onClose: closePanel, title: isCreateOpen ? "Add Event" : "Edit Event", mode: isCreateOpen ? "create" : "edit", children: (isCreateOpen || isEditOpen) && (_jsx(AdminEventEditorView, { eventId: editId ?? undefined, onSaved: closePanel, embedded: true })) })] }));
111
+ ] }), 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) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-error-surface px-4 py-3 text-sm text-error dark:border-red-900/60", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No events found", onRowClick: (row) => openEditPanel(row.id) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No events found", onRowClick: undefined, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsxs(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Type", tabs: TYPE_OPTIONS, value: pendingFilters.type ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, type: id })) })] }), _jsx(SideDrawer, { isOpen: isCreateOpen || isEditOpen, onClose: closePanel, title: isCreateOpen ? "Add Event" : "Edit Event", mode: isCreateOpen ? "create" : "edit", children: (isCreateOpen || isEditOpen) && (_jsx(AdminEventEditorView, { eventId: editId ?? undefined, onSaved: closePanel, embedded: true })) })] }));
112
112
  }
@@ -1,10 +1,9 @@
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
5
  import { useEvents } from "../hooks/useEvents";
7
- import { ListingToolbar, Pagination, Text } from "../../../ui";
6
+ import { ListingFilterDrawer, ListingToolbar, Pagination, Text } from "../../../ui";
8
7
  import { EventCard } from "./EventCard";
9
8
  import { EventFilters, EVENT_PUBLIC_SORT_OPTIONS } from "./EventFilters";
10
9
  import { EVENT_FIELDS } from "../../../constants/field-names";
@@ -116,5 +115,5 @@ export function EventsIndexListing({ initialData }) {
116
115
  const commitSearch = useCallback(() => {
117
116
  table.set(TABLE_KEYS.QUERY, searchInput.trim());
118
117
  }, [searchInput, table]);
119
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), sortOptions: EVENT_PUBLIC_SORT_OPTIONS, 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) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-video bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-4 space-y-2", 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" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded mt-2" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No events 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: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: events.map((event) => (_jsx(EventCard, { event: event }, event.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(EventFilters, { table: pendingTable, variant: "public" }) }), _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})` : ""] }) })] })] }))] }));
118
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), sortOptions: EVENT_PUBLIC_SORT_OPTIONS, 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) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-video bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-4 space-y-2", 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" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded mt-2" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No events 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: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(EventFilters, { table: pendingTable, variant: "public" }) })] }));
120
119
  }
@@ -1,5 +1,8 @@
1
+ export declare function hashBannerMessage(msg: string): string;
1
2
  export interface AnnouncementBarProps {
2
3
  message: string;
3
4
  className?: string;
5
+ /** Called when the user dismisses — consumer wires this to a server action to persist. */
6
+ onDismiss?: (hash: string) => void;
4
7
  }
5
- export declare function AnnouncementBar({ message, className }: AnnouncementBarProps): import("react/jsx-runtime").JSX.Element | null;
8
+ export declare function AnnouncementBar({ message, className, onDismiss }: AnnouncementBarProps): import("react/jsx-runtime").JSX.Element | null;