@mohasinac/appkit 2.7.46 → 2.7.48

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 (71) hide show
  1. package/dist/constants/field-names.d.ts +1 -0
  2. package/dist/constants/field-names.js +1 -0
  3. package/dist/core/unit-of-work.js +3 -4
  4. package/dist/features/admin/components/DashboardStats.js +1 -1
  5. package/dist/features/auth/actions/profile-actions.js +4 -1
  6. package/dist/features/auth/repository/user.repository.d.ts +1 -0
  7. package/dist/features/auth/repository/user.repository.js +3 -0
  8. package/dist/features/blog/api/route.js +3 -0
  9. package/dist/features/blog/components/BlogIndexListing.js +2 -0
  10. package/dist/features/blog/components/BlogPostView.js +2 -1
  11. package/dist/features/categories/api/route.js +1 -1
  12. package/dist/features/categories/components/BundleDetailView.js +5 -3
  13. package/dist/features/categories/components/CategoryBundlesListing.js +77 -41
  14. package/dist/features/categories/components/CategoryDetailPageView.js +12 -9
  15. package/dist/features/categories/components/CategoryProductsListing.js +3 -5
  16. package/dist/features/classified/components/ClassifiedIndexListing.js +3 -4
  17. package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +3 -4
  18. package/dist/features/events/actions/event-actions.d.ts +2 -3
  19. package/dist/features/events/actions/event-actions.js +3 -5
  20. package/dist/features/events/api/route.js +7 -7
  21. package/dist/features/events/components/AdminEventEditorView.js +80 -6
  22. package/dist/features/events/components/AdminEventEntriesView.js +33 -4
  23. package/dist/features/events/components/EventFilters.d.ts +3 -0
  24. package/dist/features/events/components/EventFilters.js +1 -0
  25. package/dist/features/events/components/EventLeaderboard.js +1 -1
  26. package/dist/features/events/components/EventsIndexListing.js +10 -7
  27. package/dist/features/events/repository/event-entry.repository.d.ts +2 -2
  28. package/dist/features/events/repository/event-entry.repository.js +32 -7
  29. package/dist/features/events/types/index.d.ts +3 -1
  30. package/dist/features/filters/SwitchFilter.js +2 -4
  31. package/dist/features/homepage/components/BrandsSection.js +2 -2
  32. package/dist/features/homepage/components/CustomCardsSection.js +1 -1
  33. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  34. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  35. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  36. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  37. package/dist/features/live/components/LiveItemsIndexListing.js +3 -4
  38. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +3 -4
  39. package/dist/features/products/components/AuctionsIndexListing.js +3 -4
  40. package/dist/features/products/components/PrizeDrawsIndexListing.js +3 -8
  41. package/dist/features/products/components/ProductDetailPageView.js +17 -2
  42. package/dist/features/products/components/ProductsIndexListing.js +10 -6
  43. package/dist/features/products/schemas/firestore.d.ts +2 -0
  44. package/dist/features/reviews/actions/review-actions.js +2 -0
  45. package/dist/features/reviews/components/ReviewDetailShell.js +3 -2
  46. package/dist/features/reviews/components/ReviewFilters.js +1 -1
  47. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -2
  48. package/dist/features/reviews/hooks/useReviews.js +2 -0
  49. package/dist/features/reviews/schemas/firestore.d.ts +4 -0
  50. package/dist/features/reviews/types/index.d.ts +3 -0
  51. package/dist/features/scams/components/ScamRegistryView.js +41 -8
  52. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  53. package/dist/features/stores/components/StoreAuctionsListing.js +3 -4
  54. package/dist/features/stores/components/StoreDetailLayoutView.js +13 -5
  55. package/dist/features/stores/components/StorePreOrdersListing.js +3 -4
  56. package/dist/features/stores/components/StoreProductsListing.js +3 -4
  57. package/dist/index.d.ts +1 -0
  58. package/dist/providers/db-firebase/helpers.d.ts +4 -6
  59. package/dist/providers/db-firebase/helpers.js +18 -5
  60. package/dist/providers/db-firebase/sieve.js +26 -1
  61. package/dist/react/hooks/usePendingFilters.d.ts +2 -0
  62. package/dist/react/hooks/usePendingFilters.js +9 -0
  63. package/dist/react/hooks/usePendingTable.d.ts +5 -0
  64. package/dist/react/hooks/usePendingTable.js +1 -0
  65. package/dist/seed/homepage-sections-seed-data.js +2 -2
  66. package/dist/seed/products-standard-seed-data.js +1 -0
  67. package/dist/tailwind-utilities.css +1 -1
  68. package/dist/ui/components/Drawer.style.css +13 -0
  69. package/dist/ui/components/FilterDrawer.d.ts +4 -2
  70. package/dist/ui/components/FilterDrawer.js +2 -2
  71. package/package.json +1 -1
@@ -1,11 +1,12 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, 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
5
  import { Alert, Button, Form, FormActions, Input, RichTextEditor, Select, StackedViewShell, Text, Toggle, } from "../../../ui";
6
6
  import { apiClient } from "../../../http";
7
7
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
8
8
  const CLS_PANEL_SM = "rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 p-3 space-y-2";
9
+ const CLS_PANEL_LG = "rounded-xl border border-zinc-200 dark:border-slate-700 p-4 space-y-4";
9
10
  function toLocalDatetime(iso) {
10
11
  if (!iso)
11
12
  return "";
@@ -50,6 +51,21 @@ const EVENT_STATUS_OPTIONS = [
50
51
  { label: "Paused", value: "paused" },
51
52
  { label: "Ended", value: "ended" },
52
53
  ];
54
+ const FORM_FIELD_TYPE_OPTIONS = [
55
+ { label: "Short text", value: "text" },
56
+ { label: "Long text", value: "textarea" },
57
+ { label: "Email", value: "email" },
58
+ { label: "Phone", value: "phone" },
59
+ { label: "Number", value: "number" },
60
+ { label: "Dropdown (select)", value: "select" },
61
+ { label: "Multi-select", value: "multiselect" },
62
+ { label: "Checkbox", value: "checkbox" },
63
+ { label: "Radio", value: "radio" },
64
+ { label: "Date", value: "date" },
65
+ { label: "Rating (1–5)", value: "rating" },
66
+ { label: "File upload", value: "file" },
67
+ ];
68
+ const FIELD_TYPES_WITH_OPTIONS = ["select", "multiselect", "radio", "checkbox"];
53
69
  const POLL_VISIBILITY_OPTIONS = [
54
70
  { label: "Always visible", value: "always" },
55
71
  { label: "After voting", value: "after_vote" },
@@ -78,7 +94,12 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, ...rest }) {
78
94
  const [requireLogin, setRequireLogin] = React.useState(true);
79
95
  const [maxEntriesPerUser, setMaxEntriesPerUser] = React.useState("1");
80
96
  const [hasLeaderboard, setHasLeaderboard] = React.useState(false);
97
+ const [hasPointSystem, setHasPointSystem] = React.useState(false);
98
+ const [pointsLabel, setPointsLabel] = React.useState("Points");
99
+ const [entryReviewRequired, setEntryReviewRequired] = React.useState(false);
100
+ const [surveyFields, setSurveyFields] = React.useState([]);
81
101
  const [anonymous, setAnonymous] = React.useState(false);
102
+ const [feedbackFields, setFeedbackFields] = React.useState([]);
82
103
  const [hasRaffle, setHasRaffle] = React.useState(false);
83
104
  const [raffleType, setRaffleType] = React.useState("open_raffle");
84
105
  const [raffleTopN, setRaffleTopN] = React.useState("10");
@@ -130,9 +151,14 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, ...rest }) {
130
151
  setRequireLogin(event.surveyConfig.requireLogin !== false);
131
152
  setMaxEntriesPerUser(String(event.surveyConfig.maxEntriesPerUser ?? 1));
132
153
  setHasLeaderboard(Boolean(event.surveyConfig.hasLeaderboard));
154
+ setHasPointSystem(Boolean(event.surveyConfig.hasPointSystem));
155
+ setPointsLabel(event.surveyConfig.pointsLabel ?? "Points");
156
+ setEntryReviewRequired(Boolean(event.surveyConfig.entryReviewRequired));
157
+ setSurveyFields(Array.isArray(event.surveyConfig.formFields) ? event.surveyConfig.formFields : []);
133
158
  }
134
159
  if (event.feedbackConfig) {
135
160
  setAnonymous(Boolean(event.feedbackConfig.anonymous));
161
+ setFeedbackFields(Array.isArray(event.feedbackConfig.formFields) ? event.feedbackConfig.formFields : []);
136
162
  }
137
163
  setHasRaffle(Boolean(event.hasRaffle));
138
164
  setRaffleType(event.raffleType || "open_raffle");
@@ -176,14 +202,18 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, ...rest }) {
176
202
  requireLogin,
177
203
  maxEntriesPerUser: Number(maxEntriesPerUser) || 1,
178
204
  hasLeaderboard,
179
- hasPointSystem: false,
180
- entryReviewRequired: false,
181
- formFields: [],
205
+ hasPointSystem,
206
+ pointsLabel: hasPointSystem ? (pointsLabel.trim() || "Points") : undefined,
207
+ entryReviewRequired,
208
+ formFields: surveyFields.map((f, i) => ({ ...f, order: i })),
182
209
  },
183
210
  };
184
211
  if (type === "feedback")
185
212
  return {
186
- feedbackConfig: { anonymous, formFields: [] },
213
+ feedbackConfig: {
214
+ anonymous,
215
+ formFields: feedbackFields.map((f, i) => ({ ...f, order: i })),
216
+ },
187
217
  };
188
218
  return {};
189
219
  };
@@ -237,6 +267,50 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, ...rest }) {
237
267
  setSaveMessage(error instanceof Error ? error.message : "Save failed");
238
268
  },
239
269
  });
270
+ const makeAddField = (setter) => () => setter((prev) => [
271
+ ...prev,
272
+ { id: `field-${Date.now()}`, type: "text", label: "", required: false, order: prev.length },
273
+ ]);
274
+ const makeRemoveField = (setter) => (id) => setter((prev) => prev.filter((f) => f.id !== id));
275
+ const makeUpdateField = (setter) => (id, patch) => setter((prev) => prev.map((f) => (f.id === id ? { ...f, ...patch } : f)));
276
+ const renderFormFieldBuilder = (fields, setter) => {
277
+ const addField = makeAddField(setter);
278
+ const removeField = makeRemoveField(setter);
279
+ const updateField = makeUpdateField(setter);
280
+ return (_jsxs("div", { className: "space-y-3", children: [_jsx(Text, { className: "text-xs font-medium text-zinc-600 dark:text-zinc-400 uppercase tracking-wide", children: "Form fields" }), fields.length === 0 && (_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "No fields yet. Add fields to collect responses from participants." })), fields.map((field, idx) => {
281
+ const hasOptions = FIELD_TYPES_WITH_OPTIONS.includes(field.type);
282
+ const isNumeric = field.type === "number" || field.type === "rating";
283
+ const isText = field.type === "text" || field.type === "textarea" || field.type === "email";
284
+ return (_jsxs("div", { className: "rounded-lg border border-zinc-200 dark:border-zinc-600 p-3 space-y-2 bg-white dark:bg-zinc-900", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs(Text, { className: "text-xs font-medium text-zinc-500 dark:text-zinc-400", children: ["Field ", idx + 1] }), _jsx("button", { type: "button", onClick: () => removeField(field.id), className: "text-zinc-400 hover:text-red-500 transition-colors text-lg leading-none px-1", "aria-label": "Remove field", children: "\u00D7" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2", children: [_jsx(Select, { label: "Type", value: field.type, options: FORM_FIELD_TYPE_OPTIONS, onChange: (e) => updateField(field.id, { type: e.target.value, options: undefined }) }), _jsx(Input, { label: "Label", value: field.label, onChange: (e) => updateField(field.id, { label: e.target.value }), placeholder: "e.g. Describe your entry", required: true })] }), !hasOptions && field.type !== "date" && field.type !== "file" && field.type !== "rating" && (_jsx(Input, { label: "Placeholder (optional)", value: field.placeholder ?? "", onChange: (e) => updateField(field.id, { placeholder: e.target.value || undefined }), placeholder: "Hint shown inside the field" })), hasOptions && (_jsxs("div", { className: "space-y-1", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Options \u2014 one per line" }), _jsx("textarea", { className: "w-full rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-sm text-zinc-800 dark:text-zinc-200 px-3 py-2 resize-y min-h-[72px] focus:outline-none focus:ring-2 focus:ring-primary/50", value: (field.options ?? []).join("\n"), onChange: (e) => updateField(field.id, {
285
+ options: e.target.value.split("\n").map((s) => s.trim()).filter(Boolean),
286
+ }), placeholder: "Option A\nOption B\nOption C" })] })), (isText || isNumeric) && (_jsxs("div", { className: "rounded bg-zinc-50 dark:bg-zinc-800 p-2 space-y-2", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Validation (optional)" }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [isText && (_jsxs(_Fragment, { children: [_jsx(Input, { label: "Min length", type: "number", value: String(field.validation?.minLength ?? ""), onChange: (e) => updateField(field.id, {
287
+ validation: {
288
+ ...field.validation,
289
+ minLength: e.target.value ? Number(e.target.value) : undefined,
290
+ },
291
+ }), placeholder: "0" }), _jsx(Input, { label: "Max length", type: "number", value: String(field.validation?.maxLength ?? ""), onChange: (e) => updateField(field.id, {
292
+ validation: {
293
+ ...field.validation,
294
+ maxLength: e.target.value ? Number(e.target.value) : undefined,
295
+ },
296
+ }), placeholder: "500" })] })), isNumeric && (_jsxs(_Fragment, { children: [_jsx(Input, { label: "Min value", type: "number", value: String(field.validation?.min ?? ""), onChange: (e) => updateField(field.id, {
297
+ validation: {
298
+ ...field.validation,
299
+ min: e.target.value ? Number(e.target.value) : undefined,
300
+ },
301
+ }), placeholder: "0" }), _jsx(Input, { label: "Max value", type: "number", value: String(field.validation?.max ?? ""), onChange: (e) => updateField(field.id, {
302
+ validation: {
303
+ ...field.validation,
304
+ max: e.target.value ? Number(e.target.value) : undefined,
305
+ },
306
+ }), placeholder: "100" })] }))] }), field.type === "text" && (_jsx(Input, { label: "Pattern (regex, optional)", value: field.validation?.pattern ?? "", onChange: (e) => updateField(field.id, {
307
+ validation: {
308
+ ...field.validation,
309
+ pattern: e.target.value || undefined,
310
+ },
311
+ }), placeholder: "^[A-Z].*" }))] })), _jsx(Toggle, { checked: field.required, onChange: (v) => updateField(field.id, { required: v }), label: "Required" })] }, field.id));
312
+ }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: addField, children: "+ Add field" })] }));
313
+ };
240
314
  const addSpinPrize = () => {
241
315
  setSpinPrizes((prev) => [
242
316
  ...prev,
@@ -293,7 +367,7 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, ...rest }) {
293
367
  const formSection = (_jsxs(Form, { onSubmit: (e) => {
294
368
  e.preventDefault();
295
369
  saveMutation.mutate();
296
- }, className: "space-y-6", children: [!eventId && (_jsx(Select, { label: "Event type", value: type, options: EVENT_TYPE_OPTIONS, onChange: (e) => setType(e.target.value) })), _jsx(Input, { label: "Title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "Charizard Flash Sale 2026", required: true }), _jsxs(_Fragment, { children: [_jsx("label", { className: "block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1", children: "Description" }), _jsx(RichTextEditor, { value: description, onChange: setDescription, minHeightClassName: "min-h-[120px]", placeholder: "Describe this event\u2026" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Input, { label: "Starts at", type: "datetime-local", value: startsAt, onChange: (e) => setStartsAt(e.target.value), required: true }), _jsx(Input, { label: "Ends at", type: "datetime-local", value: endsAt, onChange: (e) => setEndsAt(e.target.value), required: true })] }), _jsx(Input, { label: "Cover image URL (optional)", value: coverImageUrl, onChange: (e) => setCoverImageUrl(e.target.value), placeholder: "https://\u2026" }), eventId && (_jsx(Select, { label: "Status", value: status, options: EVENT_STATUS_OPTIONS, onChange: (e) => setStatus(e.target.value) })), type === "sale" && (_jsxs("div", { className: CLS_PANEL_SM, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Sale configuration" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Input, { label: "Discount %", type: "number", value: discountPercent, onChange: (e) => setDiscountPercent(e.target.value), placeholder: "10" }), _jsx(Input, { label: "Banner text (optional)", value: saleBannerText, onChange: (e) => setSaleBannerText(e.target.value), placeholder: "Limited time \u2014 ends Sunday!" })] })] })), type === "offer" && (_jsxs("div", { className: CLS_PANEL_SM, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Offer configuration" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Input, { label: "Coupon ID", value: couponId, onChange: (e) => setCouponId(e.target.value), placeholder: "Firestore coupon document ID", required: true }), _jsx(Input, { label: "Display code", value: displayCode, onChange: (e) => setDisplayCode(e.target.value), placeholder: "CHARIZARD25", required: true })] }), _jsx(Input, { label: "Banner text (optional)", value: offerBannerText, onChange: (e) => setOfferBannerText(e.target.value), placeholder: "Use CHARIZARD25 at checkout" })] })), type === "poll" && (_jsxs("div", { className: "rounded-xl border border-zinc-200 dark:border-slate-700 p-4 space-y-4", children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Poll configuration" }), _jsxs("div", { className: "space-y-2", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Options (minimum 2)" }), pollOptions.map((opt, idx) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1", children: _jsx(Input, { value: opt.label, onChange: (e) => updatePollOption(opt.id, e.target.value), placeholder: `Option ${idx + 1}` }) }), pollOptions.length > 2 && (_jsx("button", { type: "button", onClick: () => removePollOption(opt.id), className: "text-zinc-400 hover:text-red-500 transition-colors px-2 py-1 text-lg leading-none", "aria-label": "Remove option", children: "\u00D7" }))] }, opt.id))), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: addPollOption, children: "+ Add option" })] }), _jsx(Select, { label: "Results visibility", value: resultsVisibility, options: POLL_VISIBILITY_OPTIONS, onChange: (e) => setResultsVisibility(e.target.value) }), _jsxs("div", { className: "space-y-3", children: [_jsx(Toggle, { checked: allowMultiSelect, onChange: setAllowMultiSelect, label: "Allow multi-select" }), _jsx(Toggle, { checked: allowComment, onChange: setAllowComment, label: "Allow comment with vote" })] })] })), type === "survey" && (_jsxs("div", { className: CLS_PANEL_SM, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Survey configuration" }), _jsx(Input, { label: "Max entries per user", type: "number", value: maxEntriesPerUser, onChange: (e) => setMaxEntriesPerUser(e.target.value), placeholder: "1" }), _jsx(Toggle, { checked: requireLogin, onChange: setRequireLogin, label: "Require login to participate" }), _jsx(Toggle, { checked: hasLeaderboard, onChange: setHasLeaderboard, label: "Show leaderboard" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Form fields can be configured from the event detail page after saving." })] })), type === "feedback" && (_jsxs("div", { className: CLS_PANEL_SM, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Feedback configuration" }), _jsx(Toggle, { checked: anonymous, onChange: setAnonymous, label: "Allow anonymous submissions" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Form fields can be configured from the event detail page after saving." })] })), _jsxs("div", { className: "rounded-xl border border-zinc-200 dark:border-slate-700 p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Raffle & spin wheel" }), type !== "raffle" && type !== "spin_wheel" && (_jsx(Toggle, { checked: hasRaffle, onChange: setHasRaffle, label: "Attach raffle" }))] }), (hasRaffle || type === "raffle" || type === "spin_wheel") && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Select, { label: "Raffle type", value: type === "spin_wheel" ? "spin_wheel" : raffleType, options: RAFFLE_TYPE_OPTIONS, onChange: (e) => setRaffleType(e.target.value), disabled: type === "spin_wheel" }), _jsx(Input, { label: "Top N (for top-N raffle types)", type: "number", value: raffleTopN, onChange: (e) => setRaffleTopN(e.target.value), placeholder: "10" })] }), _jsx(Input, { label: "Prize description", value: rafflePrize, onChange: (e) => setRafflePrize(e.target.value), placeholder: "\u20B92,000 store credit + exclusive Pikachu sticker" }), _jsx(Input, { label: "Coupon ID for winner (optional)", value: rafflePrizeCouponId, onChange: (e) => setRafflePrizeCouponId(e.target.value), placeholder: "coupon-vip-winner-2026" }), (raffleType === "spin_wheel" || type === "spin_wheel") && (_jsxs("div", { className: "rounded-lg border border-zinc-200 dark:border-slate-700 p-3 space-y-3", children: [_jsx(Text, { className: "text-xs font-medium text-zinc-600 dark:text-zinc-300", children: "Spin prizes (weighted random)" }), spinPrizes.length === 0 && (_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "No spin prizes yet. Add at least one to enable spinning." })), spinPrizes.map((p) => (_jsxs("div", { className: "grid grid-cols-12 gap-2 items-end", children: [_jsx("div", { className: "col-span-5", children: _jsx(Input, { label: "Label", value: p.label, onChange: (e) => updateSpinPrize(p.id, { label: e.target.value }), placeholder: "\u20B9100 off" }) }), _jsx("div", { className: "col-span-3", children: _jsx(Input, { label: "Coupon ID", value: p.couponId ?? "", onChange: (e) => updateSpinPrize(p.id, {
370
+ }, className: "space-y-6", children: [!eventId && (_jsx(Select, { label: "Event type", value: type, options: EVENT_TYPE_OPTIONS, onChange: (e) => setType(e.target.value) })), _jsx(Input, { label: "Title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "Charizard Flash Sale 2026", required: true }), _jsxs(_Fragment, { children: [_jsx("label", { className: "block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1", children: "Description" }), _jsx(RichTextEditor, { value: description, onChange: setDescription, minHeightClassName: "min-h-[120px]", placeholder: "Describe this event\u2026" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Input, { label: "Starts at", type: "datetime-local", value: startsAt, onChange: (e) => setStartsAt(e.target.value), required: true }), _jsx(Input, { label: "Ends at", type: "datetime-local", value: endsAt, onChange: (e) => setEndsAt(e.target.value), required: true })] }), _jsx(Input, { label: "Cover image URL (optional)", value: coverImageUrl, onChange: (e) => setCoverImageUrl(e.target.value), placeholder: "https://\u2026" }), eventId && (_jsx(Select, { label: "Status", value: status, options: EVENT_STATUS_OPTIONS, onChange: (e) => setStatus(e.target.value) })), type === "sale" && (_jsxs("div", { className: CLS_PANEL_SM, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Sale configuration" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Input, { label: "Discount %", type: "number", value: discountPercent, onChange: (e) => setDiscountPercent(e.target.value), placeholder: "10" }), _jsx(Input, { label: "Banner text (optional)", value: saleBannerText, onChange: (e) => setSaleBannerText(e.target.value), placeholder: "Limited time \u2014 ends Sunday!" })] })] })), type === "offer" && (_jsxs("div", { className: CLS_PANEL_SM, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Offer configuration" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Input, { label: "Coupon ID", value: couponId, onChange: (e) => setCouponId(e.target.value), placeholder: "Firestore coupon document ID", required: true }), _jsx(Input, { label: "Display code", value: displayCode, onChange: (e) => setDisplayCode(e.target.value), placeholder: "CHARIZARD25", required: true })] }), _jsx(Input, { label: "Banner text (optional)", value: offerBannerText, onChange: (e) => setOfferBannerText(e.target.value), placeholder: "Use CHARIZARD25 at checkout" })] })), type === "poll" && (_jsxs("div", { className: CLS_PANEL_LG, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Poll configuration" }), _jsxs("div", { className: "space-y-2", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Options (minimum 2)" }), pollOptions.map((opt, idx) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1", children: _jsx(Input, { value: opt.label, onChange: (e) => updatePollOption(opt.id, e.target.value), placeholder: `Option ${idx + 1}` }) }), pollOptions.length > 2 && (_jsx("button", { type: "button", onClick: () => removePollOption(opt.id), className: "text-zinc-400 hover:text-red-500 transition-colors px-2 py-1 text-lg leading-none", "aria-label": "Remove option", children: "\u00D7" }))] }, opt.id))), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: addPollOption, children: "+ Add option" })] }), _jsx(Select, { label: "Results visibility", value: resultsVisibility, options: POLL_VISIBILITY_OPTIONS, onChange: (e) => setResultsVisibility(e.target.value) }), _jsxs("div", { className: "space-y-3", children: [_jsx(Toggle, { checked: allowMultiSelect, onChange: setAllowMultiSelect, label: "Allow multi-select" }), _jsx(Toggle, { checked: allowComment, onChange: setAllowComment, label: "Allow comment with vote" })] })] })), type === "survey" && (_jsxs("div", { className: CLS_PANEL_LG, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Survey configuration" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Input, { label: "Max entries per user", type: "number", value: maxEntriesPerUser, onChange: (e) => setMaxEntriesPerUser(e.target.value), placeholder: "1" }), hasPointSystem && (_jsx(Input, { label: "Points label", value: pointsLabel, onChange: (e) => setPointsLabel(e.target.value), placeholder: "Points" }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Toggle, { checked: requireLogin, onChange: setRequireLogin, label: "Require login to participate" }), _jsx(Toggle, { checked: hasLeaderboard, onChange: setHasLeaderboard, label: "Show leaderboard" }), _jsx(Toggle, { checked: hasPointSystem, onChange: setHasPointSystem, label: "Enable point system (employees assign points per entry)" }), _jsx(Toggle, { checked: entryReviewRequired, onChange: setEntryReviewRequired, label: "Require employee review before entry is approved" })] }), renderFormFieldBuilder(surveyFields, setSurveyFields)] })), type === "feedback" && (_jsxs("div", { className: CLS_PANEL_LG, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Feedback configuration" }), _jsx(Toggle, { checked: anonymous, onChange: setAnonymous, label: "Allow anonymous submissions" }), renderFormFieldBuilder(feedbackFields, setFeedbackFields)] })), _jsxs("div", { className: CLS_PANEL_LG, children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Raffle & spin wheel" }), type !== "raffle" && type !== "spin_wheel" && (_jsx(Toggle, { checked: hasRaffle, onChange: setHasRaffle, label: "Attach raffle" }))] }), (hasRaffle || type === "raffle" || type === "spin_wheel") && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsx(Select, { label: "Raffle type", value: type === "spin_wheel" ? "spin_wheel" : raffleType, options: RAFFLE_TYPE_OPTIONS, onChange: (e) => setRaffleType(e.target.value), disabled: type === "spin_wheel" }), _jsx(Input, { label: "Top N (for top-N raffle types)", type: "number", value: raffleTopN, onChange: (e) => setRaffleTopN(e.target.value), placeholder: "10" })] }), _jsx(Input, { label: "Prize description", value: rafflePrize, onChange: (e) => setRafflePrize(e.target.value), placeholder: "\u20B92,000 store credit + exclusive Pikachu sticker" }), _jsx(Input, { label: "Coupon ID for winner (optional)", value: rafflePrizeCouponId, onChange: (e) => setRafflePrizeCouponId(e.target.value), placeholder: "coupon-vip-winner-2026" }), (raffleType === "spin_wheel" || type === "spin_wheel") && (_jsxs("div", { className: "rounded-lg border border-zinc-200 dark:border-slate-700 p-3 space-y-3", children: [_jsx(Text, { className: "text-xs font-medium text-zinc-600 dark:text-zinc-300", children: "Spin prizes (weighted random)" }), spinPrizes.length === 0 && (_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "No spin prizes yet. Add at least one to enable spinning." })), spinPrizes.map((p) => (_jsxs("div", { className: "grid grid-cols-12 gap-2 items-end", children: [_jsx("div", { className: "col-span-5", children: _jsx(Input, { label: "Label", value: p.label, onChange: (e) => updateSpinPrize(p.id, { label: e.target.value }), placeholder: "\u20B9100 off" }) }), _jsx("div", { className: "col-span-3", children: _jsx(Input, { label: "Coupon ID", value: p.couponId ?? "", onChange: (e) => updateSpinPrize(p.id, {
297
371
  couponId: e.target.value || undefined,
298
372
  }) }) }), _jsx("div", { className: "col-span-2", children: _jsx(Input, { label: "Weight", type: "number", value: String(p.weight), onChange: (e) => updateSpinPrize(p.id, {
299
373
  weight: Number(e.target.value) || 0,
@@ -57,13 +57,15 @@ export function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEnd
57
57
  enabled: Boolean(eventId && resolvedStatsEndpoint),
58
58
  staleTime: 15000,
59
59
  });
60
+ const [expandedEntryId, setExpandedEntryId] = React.useState(null);
61
+ const [pointsInputs, setPointsInputs] = React.useState({});
60
62
  const reviewMutation = useMutation({
61
- mutationFn: async ({ entryId, status, }) => {
63
+ mutationFn: async ({ entryId, status, points, }) => {
62
64
  if (!eventId)
63
65
  throw new Error("eventId is required");
64
66
  const endpoint = entryReviewEndpoint?.(eventId, entryId) ??
65
67
  ADMIN_ENDPOINTS.EVENT_ENTRY_BY_ID(eventId, entryId);
66
- await apiClient.patch(endpoint, { status });
68
+ await apiClient.patch(endpoint, { status, ...(points !== undefined ? { points } : {}) });
67
69
  },
68
70
  onSuccess: async () => {
69
71
  await Promise.all([
@@ -72,6 +74,19 @@ export function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEnd
72
74
  ]);
73
75
  },
74
76
  });
77
+ const pointsMutation = useMutation({
78
+ mutationFn: async ({ entryId, points, currentStatus }) => {
79
+ if (!eventId)
80
+ throw new Error("eventId is required");
81
+ const endpoint = entryReviewEndpoint?.(eventId, entryId) ??
82
+ ADMIN_ENDPOINTS.EVENT_ENTRY_BY_ID(eventId, entryId);
83
+ const status = currentStatus === "flagged" ? "flagged" : "approved";
84
+ await apiClient.patch(endpoint, { status, points });
85
+ },
86
+ onSuccess: async () => {
87
+ await queryClient.invalidateQueries({ queryKey: ["admin-event-entries", eventId] });
88
+ },
89
+ });
75
90
  const rows = entriesQuery.data?.items ?? [];
76
91
  const columns = React.useMemo(() => [
77
92
  {
@@ -89,12 +104,23 @@ export function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEnd
89
104
  header: "Status",
90
105
  render: (row) => (_jsx(Text, { className: "text-xs font-medium uppercase tracking-wide", children: row.reviewStatus })),
91
106
  },
107
+ {
108
+ key: "points",
109
+ header: "Points",
110
+ render: (row) => (_jsxs("div", { className: "flex items-center gap-1.5", "data-section": "adminevententriesview-points", children: [_jsx("input", { type: "number", min: 0, className: "w-20 rounded border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-sm px-2 py-1 text-zinc-800 dark:text-zinc-200 focus:outline-none focus:ring-2 focus:ring-primary/50", placeholder: "0", value: pointsInputs[row.id] ?? (row.points !== undefined ? String(row.points) : ""), onChange: (e) => setPointsInputs((prev) => ({ ...prev, [row.id]: e.target.value })) }), _jsx(Button, { size: "sm", variant: "outline", disabled: pointsMutation.isPending || !(row.id in pointsInputs), onClick: () => {
111
+ const val = Number(pointsInputs[row.id]);
112
+ if (!isNaN(val)) {
113
+ pointsMutation.mutate({ entryId: row.id, points: val, currentStatus: row.reviewStatus });
114
+ setPointsInputs((prev) => { const next = { ...prev }; delete next[row.id]; return next; });
115
+ }
116
+ }, children: "Save" })] })),
117
+ },
92
118
  {
93
119
  key: "actions",
94
120
  header: "Actions",
95
- render: (row) => (_jsxs("div", { className: "flex items-center gap-2", "data-section": "adminevententriesview-div-274", children: [_jsx(Button, { size: "sm", variant: "secondary", disabled: reviewMutation.isPending || row.reviewStatus === "approved", onClick: () => reviewMutation.mutate({ entryId: row.id, status: "approved" }), children: "Approve" }), _jsx(Button, { size: "sm", variant: "outline", disabled: reviewMutation.isPending || row.reviewStatus === "flagged", onClick: () => reviewMutation.mutate({ entryId: row.id, status: "flagged" }), children: "Flag" })] })),
121
+ render: (row) => (_jsxs("div", { className: "flex items-center gap-2 flex-wrap", "data-section": "adminevententriesview-div-274", children: [_jsx(Button, { size: "sm", variant: "secondary", disabled: reviewMutation.isPending || row.reviewStatus === "approved", onClick: () => reviewMutation.mutate({ entryId: row.id, status: "approved" }), children: "Approve" }), _jsx(Button, { size: "sm", variant: "outline", disabled: reviewMutation.isPending || row.reviewStatus === "flagged", onClick: () => reviewMutation.mutate({ entryId: row.id, status: "flagged" }), children: "Flag" }), row.formResponses && Object.keys(row.formResponses).length > 0 && (_jsx(Button, { size: "sm", variant: "outline", onClick: () => setExpandedEntryId((prev) => (prev === row.id ? null : row.id)), children: expandedEntryId === row.id ? "Hide" : "Responses" }))] })),
96
122
  },
97
- ], [reviewMutation]);
123
+ ], [reviewMutation, pointsMutation, pointsInputs, expandedEntryId]);
98
124
  const statsSection = (_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-3", "data-section": "adminevententriesview-div-275", children: [_jsx(Alert, { variant: "info", title: "Total entries", children: String(statsQuery.data?.stats?.totalEntries ?? 0) }), _jsx(Alert, { variant: "success", title: "Approved", children: String(statsQuery.data?.stats?.approvedEntries ?? 0) }), _jsx(Alert, { variant: "warning", title: "Flagged", children: String(statsQuery.data?.stats?.flaggedEntries ?? 0) })] }));
99
125
  const filtersSection = (_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-3", "data-section": "adminevententriesview-div-276", children: [_jsx(Input, { label: "Search entries", value: searchQuery, onChange: (event) => {
100
126
  setPage(1);
@@ -103,6 +129,8 @@ export function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEnd
103
129
  setPage(1);
104
130
  setStatusFilter(event.target.value);
105
131
  } })] }));
132
+ const expandedEntry = expandedEntryId ? rows.find((r) => r.id === expandedEntryId) : null;
133
+ const responsesPanelSection = expandedEntry?.formResponses && Object.keys(expandedEntry.formResponses).length > 0 ? (_jsxs("div", { className: "rounded-xl border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 p-4 space-y-3", children: [_jsxs(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: ["Responses \u2014 ", expandedEntry.userDisplayName || expandedEntry.userId || "Anonymous"] }), _jsx("div", { className: "space-y-2", children: Object.entries(expandedEntry.formResponses).map(([key, value]) => (_jsxs("div", { className: "space-y-0.5", children: [_jsx(Text, { className: "text-xs font-medium text-zinc-500 dark:text-zinc-400", children: key }), _jsx(Text, { className: "text-sm text-zinc-800 dark:text-zinc-200 whitespace-pre-wrap break-words", children: Array.isArray(value) ? value.join(", ") : String(value ?? "—") })] }, key))) })] })) : null;
106
134
  const tableSection = (_jsx(DataTable, { columns: columns, rows: rows, isLoading: entriesQuery.isLoading, currentPage: entriesQuery.data?.page ?? page, totalPages: entriesQuery.data?.totalPages ?? 1, onPageChange: setPage, emptyLabel: "No entries found" }));
107
135
  const eventTitle = statsQuery.data?.event?.title;
108
136
  return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: eventTitle ? `${eventTitle} Entries` : "Event Entries", renderHeader: renderHeader, sections: [
@@ -112,6 +140,7 @@ export function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEnd
112
140
  renderStats?.() ?? statsSection,
113
141
  renderFilters?.() ?? filtersSection,
114
142
  renderTable?.() ?? tableSection,
143
+ responsesPanelSection,
115
144
  renderPagination?.(),
116
145
  ], overlays: renderReviewDrawer?.() }));
117
146
  }
@@ -64,6 +64,9 @@ export declare const EVENT_PUBLIC_SORT_OPTIONS: readonly [{
64
64
  }, {
65
65
  readonly value: string;
66
66
  readonly label: "Title A–Z";
67
+ }, {
68
+ readonly value: string;
69
+ readonly label: "Title Z–A";
67
70
  }, {
68
71
  readonly value: "-stats.totalEntries";
69
72
  readonly label: "Most Entries";
@@ -28,6 +28,7 @@ export const EVENT_PUBLIC_SORT_OPTIONS = [
28
28
  { value: sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), label: "Starts Soonest" },
29
29
  { value: sortBy(EVENT_FIELDS.STARTS_AT), label: "Starts Latest" },
30
30
  { value: sortBy(EVENT_FIELDS.TITLE, "ASC"), label: "Title A–Z" },
31
+ { value: sortBy(EVENT_FIELDS.TITLE), label: "Title Z–A" },
31
32
  { value: "-stats.totalEntries", label: "Most Entries" },
32
33
  ];
33
34
  // Backward-compatible alias.
@@ -14,5 +14,5 @@ export function EventLeaderboard({ isLoading = false, isEmpty = false, renderLis
14
14
  }
15
15
  if (renderList)
16
16
  return _jsx("div", { className: className, "data-section": "eventleaderboard-div-280", children: renderList() });
17
- return (_jsx("div", { className: `space-y-2 ${className}`, "data-section": "eventleaderboard-div-281", children: entries.map((entry, i) => renderEntry ? (_jsx(React.Fragment, { children: renderEntry(entry, i) }, entry.userId)) : (_jsxs("div", { className: "flex items-center justify-between p-3 rounded-xl border", "data-section": "eventleaderboard-div-282", children: [_jsxs(Span, { className: "font-medium", children: ["#", entry.rank, " ", entry.userDisplayName] }), _jsxs(Span, { children: [entry.points, " ", labels.points ?? "pts"] })] }, entry.userId))) }));
17
+ return (_jsx("div", { className: `space-y-2 ${className}`, "data-section": "eventleaderboard-div-281", children: entries.map((entry, i) => renderEntry ? (_jsx(React.Fragment, { children: renderEntry(entry, i) }, entry.userId)) : (_jsxs("div", { className: "flex items-center justify-between p-3 rounded-xl border", "data-section": "eventleaderboard-div-282", children: [_jsxs(Span, { className: "font-medium", children: ["#", entry.rank, " ", entry.userDisplayName] }), _jsxs(Span, { children: [entry.totalPoints, " ", labels.points ?? "pts"] })] }, entry.userId))) }));
18
18
  }
@@ -81,6 +81,7 @@ export function EventsIndexListing({ initialData }) {
81
81
  const statusRaw = table.get(TABLE_KEYS.STATUS);
82
82
  const dateFrom = table.get(TABLE_KEYS.DATE_FROM);
83
83
  const dateTo = table.get(TABLE_KEYS.DATE_TO);
84
+ const showExpired = table.get(TABLE_KEYS.SHOW_EXPIRED) === "true";
84
85
  const filterParts = [];
85
86
  if (typeRaw) {
86
87
  const types = typeRaw.split("|").filter(Boolean);
@@ -90,13 +91,15 @@ export function EventsIndexListing({ initialData }) {
90
91
  else if (types.length > 1)
91
92
  filterParts.push(sieveMultiEq(EVENT_FIELDS.TYPE, types));
92
93
  }
93
- if (statusRaw) {
94
- const statuses = statusRaw.split("|").filter(Boolean);
95
- if (statuses.length === 1)
96
- filterParts.push(sieveFilter(EVENT_FIELDS.STATUS, SIEVE_OP.EQ, statuses[0]));
97
- else if (statuses.length > 1)
98
- filterParts.push(sieveMultiEq(EVENT_FIELDS.STATUS, statuses));
99
- }
94
+ const statusValues = statusRaw
95
+ ? statusRaw.split("|").filter(Boolean)
96
+ : showExpired
97
+ ? ["active", "ended", "paused"]
98
+ : [];
99
+ if (statusValues.length === 1)
100
+ filterParts.push(sieveFilter(EVENT_FIELDS.STATUS, SIEVE_OP.EQ, statusValues[0]));
101
+ else if (statusValues.length > 1)
102
+ filterParts.push(sieveMultiEq(EVENT_FIELDS.STATUS, statusValues));
100
103
  if (dateFrom)
101
104
  filterParts.push(sieveFilter(EVENT_FIELDS.STARTS_AT, SIEVE_OP.GTE, dateFrom));
102
105
  if (dateTo)
@@ -9,9 +9,9 @@ declare class EventEntryRepository extends BaseRepository<EventEntryDocument> {
9
9
  listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
10
10
  hasUserEntered(eventId: string, userId: string): Promise<boolean>;
11
11
  countUserEntries(eventId: string, userId: string): Promise<number>;
12
- getLeaderboard(eventId: string, limit?: number): Promise<EventEntryDocument[]>;
12
+ getLeaderboard(eventId: string, limit?: number): Promise<import("../types").LeaderboardEntry[]>;
13
13
  createEntry(input: EventEntryCreateInput): Promise<EventEntryDocument>;
14
- reviewEntry(id: string, reviewStatus: EventEntryDocument["reviewStatus"], reviewedBy: string, reviewNote?: string): Promise<EventEntryDocument>;
14
+ reviewEntry(id: string, reviewStatus: EventEntryDocument["reviewStatus"], reviewedBy: string, reviewNote?: string, points?: number): Promise<EventEntryDocument>;
15
15
  }
16
16
  declare const eventEntryRepository: EventEntryRepository;
17
17
  export { EventEntryRepository, eventEntryRepository };
@@ -53,13 +53,37 @@ class EventEntryRepository extends BaseRepository {
53
53
  const snapshot = await this.getCollection()
54
54
  .where(EVENT_ENTRY_FIELDS.EVENT_ID, "==", eventId)
55
55
  .where(EVENT_ENTRY_FIELDS.REVIEW_STATUS, "==", EVENT_ENTRY_FIELDS.REVIEW_STATUS_VALUES.APPROVED)
56
- .orderBy(EVENT_ENTRY_FIELDS.POINTS, "desc")
57
- .limit(limit)
58
56
  .get();
59
- return snapshot.docs.map((doc) => decryptPiiFields({
60
- id: doc.id,
61
- ...doc.data(),
62
- }, [...EVENT_ENTRY_PII_FIELDS]));
57
+ // Aggregate per user: sum all approved entry points
58
+ const byUser = new Map();
59
+ for (const doc of snapshot.docs) {
60
+ const raw = decryptPiiFields({ id: doc.id, ...doc.data() }, [...EVENT_ENTRY_PII_FIELDS]);
61
+ if (!raw.userId)
62
+ continue;
63
+ const existing = byUser.get(raw.userId);
64
+ const pts = typeof raw.points === "number" ? raw.points : 0;
65
+ if (existing) {
66
+ existing.totalPoints += pts;
67
+ existing.entryCount += 1;
68
+ }
69
+ else {
70
+ byUser.set(raw.userId, {
71
+ displayName: raw.userDisplayName ?? raw.userId,
72
+ totalPoints: pts,
73
+ entryCount: 1,
74
+ });
75
+ }
76
+ }
77
+ return Array.from(byUser.entries())
78
+ .sort((a, b) => b[1].totalPoints - a[1].totalPoints)
79
+ .slice(0, limit)
80
+ .map(([userId, data], idx) => ({
81
+ rank: idx + 1,
82
+ userId,
83
+ userDisplayName: data.displayName,
84
+ totalPoints: data.totalPoints,
85
+ entryCount: data.entryCount,
86
+ }));
63
87
  }
64
88
  catch (error) {
65
89
  throw new DatabaseError(`Failed to get leaderboard for event ${eventId}`, error);
@@ -88,7 +112,7 @@ class EventEntryRepository extends BaseRepository {
88
112
  throw new DatabaseError("Failed to create event entry", error);
89
113
  }
90
114
  }
91
- async reviewEntry(id, reviewStatus, reviewedBy, reviewNote) {
115
+ async reviewEntry(id, reviewStatus, reviewedBy, reviewNote, points) {
92
116
  try {
93
117
  const now = new Date();
94
118
  await this.getCollection()
@@ -98,6 +122,7 @@ class EventEntryRepository extends BaseRepository {
98
122
  [EVENT_ENTRY_FIELDS.REVIEWED_BY]: reviewedBy,
99
123
  [EVENT_ENTRY_FIELDS.REVIEWED_AT]: now,
100
124
  ...(reviewNote !== undefined ? { reviewNote } : {}),
125
+ ...(points !== undefined ? { points } : {}),
101
126
  }));
102
127
  serverLogger.info("Event entry reviewed", { entryId: id, reviewStatus });
103
128
  return this.findByIdOrFail(id);
@@ -127,7 +127,9 @@ export interface LeaderboardEntry {
127
127
  rank: number;
128
128
  userId: string;
129
129
  userDisplayName: string;
130
- points: number;
130
+ /** Sum of all approved entry points for this user in the event */
131
+ totalPoints: number;
132
+ entryCount: number;
131
133
  }
132
134
  export interface EventListResponse {
133
135
  items: EventItem[];
@@ -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, Row, Span, Text } from "../../ui";
4
+ import { Button, Div, Row, Span, Text, Toggle } from "../../ui";
5
5
  import { cn } from "./filterUtils";
6
6
  export function SwitchFilter({ title, label, checked, onChange, defaultCollapsed = false, className = "", isOpen: controlledOpen, onToggle, onClear, }) {
7
7
  const isControlled = controlledOpen !== undefined;
@@ -13,7 +13,5 @@ export function SwitchFilter({ title, label, checked, onChange, defaultCollapsed
13
13
  else
14
14
  setInternalCollapsed((c) => !c);
15
15
  };
16
- return (_jsxs(Div, { role: "group", "aria-labelledby": `sf-${title}`, className: cn("p-4 border-b border-zinc-200 dark:border-slate-700 last:border-b-0", className), children: [_jsxs(Row, { className: "gap-2", children: [_jsxs(Button, { type: "button", id: `sf-${title}`, onClick: handleToggle, variant: "ghost", size: "sm", className: "flex flex-1 items-center justify-between text-sm font-semibold text-zinc-900 dark:text-zinc-50 py-1 hover:opacity-80 transition-opacity", "aria-expanded": !isCollapsed, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-50", children: title }), _jsx("svg", { className: cn("w-4 h-4 text-zinc-500 dark:text-zinc-400 transition-transform duration-200", isCollapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), checked && onClear && (_jsx(Button, { type: "button", onClick: onClear, variant: "ghost", size: "sm", className: "inline-flex items-center justify-center w-5 h-5 text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 rounded-full transition-colors", "aria-label": "Clear", children: _jsx("svg", { className: "w-3 h-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), !isCollapsed && (_jsx(Div, { className: "mt-3", children: _jsxs(Button, { type: "button", role: "switch", variant: "ghost", size: "sm", "aria-checked": checked, onClick: () => onChange(!checked), className: "flex items-center justify-between w-full group", children: [_jsx(Span, { className: "text-sm text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors", children: label }), _jsx(Span, { "aria-hidden": "true", className: cn("relative inline-flex h-5 w-9 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200", checked
17
- ? "bg-primary-600 dark:bg-secondary-500"
18
- : "bg-zinc-200 dark:bg-slate-700"), children: _jsx(Span, { className: cn("pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200", checked ? "translate-x-4" : "translate-x-0") }) })] }) }))] }));
16
+ return (_jsxs(Div, { role: "group", "aria-labelledby": `sf-${title}`, className: cn("p-4 border-b border-zinc-200 dark:border-slate-700 last:border-b-0", className), children: [_jsxs(Row, { className: "gap-2", children: [_jsxs(Button, { type: "button", id: `sf-${title}`, onClick: handleToggle, variant: "ghost", size: "sm", className: "flex flex-1 items-center justify-between text-sm font-semibold text-zinc-900 dark:text-zinc-50 py-1 hover:opacity-80 transition-opacity", "aria-expanded": !isCollapsed, children: [_jsx(Text, { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-50", children: title }), _jsx("svg", { className: cn("w-4 h-4 text-zinc-500 dark:text-zinc-400 transition-transform duration-200", isCollapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), checked && onClear && (_jsx(Button, { type: "button", onClick: onClear, variant: "ghost", size: "sm", className: "inline-flex items-center justify-center w-5 h-5 text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 rounded-full transition-colors", "aria-label": "Clear", children: _jsx("svg", { className: "w-3 h-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), !isCollapsed && (_jsxs(Div, { className: "mt-3 flex items-center justify-between gap-3", children: [_jsx(Span, { className: "text-sm text-zinc-700 dark:text-zinc-300", children: label }), _jsx(Toggle, { checked: checked, onChange: onChange, size: "sm" })] }))] }));
19
17
  }
@@ -24,7 +24,7 @@ function BrandLogo({ brand }) {
24
24
  const iconSrc = brand.display?.icon;
25
25
  const coverImage = brand.display?.coverImage;
26
26
  const initial = brand.name[0]?.toUpperCase() ?? "?";
27
- return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-24 w-28 flex-col items-center justify-center gap-2 rounded-xl border border-zinc-200 bg-white p-3 shadow-sm transition-all hover:border-primary-300 hover:shadow-md dark:border-slate-700 dark:bg-slate-900", children: [iconSrc || coverImage ? (_jsx(Image, { src: iconSrc ?? coverImage, alt: brand.name, width: 40, height: 40, className: "h-10 w-10 rounded object-contain" })) : (_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-lg bg-primary-100 text-sm font-bold text-primary-700 dark:bg-primary-900 dark:text-primary-300", children: initial })), _jsx(Text, { className: "w-full truncate text-center text-xs font-medium text-zinc-700 dark:text-zinc-300", children: brand.name })] }));
27
+ return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-2 rounded-xl border border-zinc-200 bg-white p-3 shadow-sm transition-all hover:border-primary-300 hover:shadow-md dark:border-slate-700 dark:bg-slate-900 md:h-40", children: [iconSrc || coverImage ? (_jsx(Image, { src: iconSrc ?? coverImage, alt: brand.name, width: 64, height: 64, className: "h-16 w-16 rounded object-contain" })) : (_jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-lg bg-primary-100 text-sm font-bold text-primary-700 dark:bg-primary-900 dark:text-primary-300", children: initial })), _jsx(Text, { className: "w-full truncate text-center text-xs font-medium text-zinc-700 dark:text-zinc-300", children: brand.name })] }));
28
28
  }
29
29
  export function BrandsSection({ title = "Top Brands", subtitle, limit = 12, viewMoreHref, viewMoreLabel = "All brands →", className = "", initialItems, cta, filters, }) {
30
30
  const { themed } = THEME_CONSTANTS;
@@ -39,5 +39,5 @@ export function BrandsSection({ title = "Top Brands", subtitle, limit = 12, view
39
39
  });
40
40
  if (!isLoading && allBrands.length === 0)
41
41
  return null;
42
- return (_jsx(Section, { className: `py-12 px-4 ${themed.bgSecondary} ${className}`, children: _jsxs("div", { className: "mx-auto max-w-7xl", children: [_jsxs("div", { className: "mb-6 flex items-center justify-between", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: `text-2xl font-bold md:text-3xl ${themed.textPrimary}`, children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-1 text-sm", children: subtitle }))] }), !cta && viewMoreHref && (_jsx(Link, { href: viewMoreHref, className: "text-sm font-medium text-[var(--appkit-color-primary)] hover:underline", children: viewMoreLabel }))] }), showFeaturedChip && !isLoading && (_jsxs("div", { className: "mb-4 flex flex-wrap gap-2", children: [_jsx(BrandFilterChip, { label: "All", active: activeFilter === "all", onClick: () => setActiveFilter("all") }), _jsx(BrandFilterChip, { label: "Featured", active: activeFilter === "featured", onClick: () => setActiveFilter(activeFilter === "featured" ? "all" : "featured") })] })), isLoading ? (_jsx("div", { className: "flex gap-3 overflow-hidden", children: Array.from({ length: 8 }).map((_, i) => (_jsx("div", { className: "h-24 w-28 flex-none animate-pulse rounded-xl bg-zinc-200 dark:bg-slate-700" }, i))) })) : (_jsx(HorizontalScroller, { items: brands, renderItem: (brand) => _jsx(BrandLogo, { brand: brand }), keyExtractor: (brand) => brand.id, gap: 12, showArrows: true, showScrollbar: false })), cta && !isLoading && (_jsx("div", { className: "mt-6 text-center", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) }))] }) }));
42
+ return (_jsx(Section, { className: `py-12 px-4 ${themed.bgSecondary} ${className}`, children: _jsxs("div", { className: "mx-auto max-w-7xl", children: [_jsxs("div", { className: "mb-6 flex items-center justify-between", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: `text-2xl font-bold md:text-3xl ${themed.textPrimary}`, children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-1 text-sm", children: subtitle }))] }), !cta && viewMoreHref && (_jsx(Link, { href: viewMoreHref, className: "text-sm font-medium text-[var(--appkit-color-primary)] hover:underline", children: viewMoreLabel }))] }), showFeaturedChip && !isLoading && (_jsxs("div", { className: "mb-4 flex flex-wrap gap-2", children: [_jsx(BrandFilterChip, { label: "All", active: activeFilter === "all", onClick: () => setActiveFilter("all") }), _jsx(BrandFilterChip, { label: "Featured", active: activeFilter === "featured", onClick: () => setActiveFilter(activeFilter === "featured" ? "all" : "featured") })] })), isLoading ? (_jsx("div", { className: "flex gap-3 overflow-hidden", children: Array.from({ length: 8 }).map((_, i) => (_jsx("div", { className: "h-32 w-36 flex-none animate-pulse rounded-xl bg-zinc-200 dark:bg-slate-700 md:h-40" }, i))) })) : (_jsx(HorizontalScroller, { items: brands, renderItem: (brand) => _jsx(BrandLogo, { brand: brand }), keyExtractor: (brand) => brand.id, gap: 12, showArrows: true, showScrollbar: false, loop: true })), cta && !isLoading && (_jsx("div", { className: "mt-6 text-center", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) }))] }) }));
43
43
  }
@@ -61,7 +61,7 @@ export function CustomCardsSection(config) {
61
61
  }
62
62
  // Row layout: horizontal scroller
63
63
  if (layout === "row") {
64
- return (_jsx(Section, { className: `py-12 ${themed.bgPrimary}`, children: _jsxs("div", { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(HorizontalScroller, { gap: 16, showArrows: true, snapToItems: true, children: cards.map((card) => (_jsx("div", { className: "w-72 flex-shrink-0", children: _jsx(CardItem, { card: card }) }, card.id))) })] }) }));
64
+ return (_jsx(Section, { className: `py-12 ${themed.bgPrimary}`, children: _jsxs("div", { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(HorizontalScroller, { gap: 16, showArrows: true, snapToItems: true, loop: true, children: cards.map((card) => (_jsx("div", { className: "w-72 flex-shrink-0", children: _jsx(CardItem, { card: card }) }, card.id))) })] }) }));
65
65
  }
66
66
  // Masonry layout: CSS columns
67
67
  if (layout === "masonry") {
@@ -9,5 +9,5 @@ export function CustomerReviewsSection({ title, subtitle, items, renderItem, vie
9
9
  }
10
10
  if (items.length === 0)
11
11
  return null;
12
- return (_jsx(Section, { className: `py-12 ${themed.bgPrimary} ${className}`, children: _jsxs("div", { className: "w-full max-w-7xl mx-auto px-4 md:px-8", "data-section": "customerreviewssection-div-317", children: [_jsxs("div", { className: "text-center mb-10", "data-section": "customerreviewssection-div-318", children: [_jsx(Heading, { level: 2, className: `text-3xl md:text-4xl font-bold ${themed.textPrimary} mb-3`, children: title }), subtitle && (_jsx(Text, { className: `text-base ${themed.textSecondary}`, children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: THEME_CONSTANTS.carousel.perView.reviews, gap: 24, autoScroll: true, autoScrollInterval: 4500, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor }), viewMoreHref && (_jsx("div", { className: "text-center mt-8", "data-section": "customerreviewssection-div-319", children: _jsx(TextLink, { href: viewMoreHref, className: "text-sm font-medium text-primary hover:text-primary/80", children: viewMoreLabel }) }))] }) }));
12
+ return (_jsx(Section, { className: `py-12 ${themed.bgPrimary} ${className}`, children: _jsxs("div", { className: "w-full max-w-7xl mx-auto px-4 md:px-8", "data-section": "customerreviewssection-div-317", children: [_jsxs("div", { className: "text-center mb-10", "data-section": "customerreviewssection-div-318", children: [_jsx(Heading, { level: 2, className: `text-3xl md:text-4xl font-bold ${themed.textPrimary} mb-3`, children: title }), subtitle && (_jsx(Text, { className: `text-base ${themed.textSecondary}`, children: subtitle }))] }), _jsx(HorizontalScroller, { items: items, renderItem: (item) => renderItem(item), perView: THEME_CONSTANTS.carousel.perView.reviews, gap: 24, autoScroll: true, autoScrollInterval: 4500, pauseOnHover: true, snapToItems: true, showArrows: true, keyExtractor: keyExtractor, loop: true }), viewMoreHref && (_jsx("div", { className: "text-center mt-8", "data-section": "customerreviewssection-div-319", children: _jsx(TextLink, { href: viewMoreHref, className: "text-sm font-medium text-primary hover:text-primary/80", children: viewMoreLabel }) }))] }) }));
13
13
  }
@@ -14,5 +14,5 @@ export function FeaturedResultsSection({ title, subtitle, pillLabel, ornamentLab
14
14
  afterImage: item.afterImage,
15
15
  caption: item.caption ?? "",
16
16
  sortOrder: item.sortOrder ?? i,
17
- } }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: THEME_CONSTANTS.carousel.perView.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true, showFadeEdges: true }) })] }));
17
+ } }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: THEME_CONSTANTS.carousel.perView.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true, showFadeEdges: true, loop: true }) })] }));
18
18
  }
@@ -36,7 +36,7 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
36
36
  headingClass,
37
37
  ]
38
38
  .filter(Boolean)
39
- .join(" "), children: title }), headingVariant === "editorial" && (_jsxs("div", { className: `${flex.center} gap-2 mt-1 text-zinc-400 dark:text-zinc-500 text-xs select-none`, "aria-hidden": "true", "data-section": "sectioncarousel-div-355", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { className: "text-xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `text-base ${descVariant} mt-2`, children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, loop: autoScroll && rows === 1, keyExtractor: keyExtractor, rows: rows, minItemWidth: minItemWidth, showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 flex justify-center", "data-section": "sectioncarousel-div-356", children: _jsx(TextLink, { href: viewMoreHref, className: `inline-flex items-center gap-1.5 rounded-lg border px-6 py-2.5 text-sm font-medium transition-colors ${useLightText
39
+ .join(" "), children: title }), headingVariant === "editorial" && (_jsxs("div", { className: `${flex.center} gap-2 mt-1 text-zinc-400 dark:text-zinc-500 text-xs select-none`, "aria-hidden": "true", "data-section": "sectioncarousel-div-355", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { className: "text-xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `text-base ${descVariant} mt-2`, children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, loop: rows === 1, keyExtractor: keyExtractor, rows: rows, minItemWidth: minItemWidth, showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 flex justify-center", "data-section": "sectioncarousel-div-356", children: _jsx(TextLink, { href: viewMoreHref, className: `inline-flex items-center gap-1.5 rounded-lg border px-6 py-2.5 text-sm font-medium transition-colors ${useLightText
40
40
  ? "border-white/40 text-white hover:bg-white/10"
41
41
  : "border-zinc-200 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800"}`, children: viewMoreLabel }) }))] })] }));
42
42
  }
@@ -61,5 +61,5 @@ export function ShopByCategorySection({ title = "Shop by Category", subtitle, li
61
61
  if (!cat)
62
62
  return null;
63
63
  return (_jsx(FilterChip, { label: cat.name, active: activeFilter === id, onClick: () => setActiveFilter(activeFilter === id ? "all" : id) }, id));
64
- })] })), isLoading ? (_jsx("div", { className: "flex gap-3 overflow-hidden px-1", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "flex-none h-[104px] w-[108px] animate-pulse rounded-xl bg-zinc-200 dark:bg-slate-700" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: true, autoScrollInterval: 3500, showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), cta && !isLoading && (_jsx("div", { className: "mt-6 text-center", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 text-center", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-1 text-sm font-medium text-[var(--appkit-color-primary)] hover:opacity-80", children: viewMoreLabel }) }))] }) }));
64
+ })] })), isLoading ? (_jsx("div", { className: "flex gap-3 overflow-hidden px-1", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "flex-none h-[104px] w-[108px] animate-pulse rounded-xl bg-zinc-200 dark:bg-slate-700" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: true, autoScrollInterval: 3500, showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, loop: true, pauseOnHover: true })), cta && !isLoading && (_jsx("div", { className: "mt-6 text-center", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 text-center", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-1 text-sm font-medium text-[var(--appkit-color-primary)] hover:opacity-80", children: viewMoreLabel }) }))] }) }));
65
65
  }
@@ -42,7 +42,7 @@ export function LiveItemsIndexListing({ initialData }) {
42
42
  const { categories } = useCategoryTree();
43
43
  const categoryOptions = categoriesToFacetOptions(categories);
44
44
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "live").map((i) => i.itemId));
45
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
45
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
46
46
  const openFilters = useCallback(() => {
47
47
  onFilterReset();
48
48
  setFilterOpen(true);
@@ -52,10 +52,9 @@ export function LiveItemsIndexListing({ initialData }) {
52
52
  setFilterOpen(false);
53
53
  }, [onFilterApply]);
54
54
  const resetAll = useCallback(() => {
55
- table.setMany({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
56
- onFilterClear();
55
+ onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
57
56
  setSearchInput("");
58
- }, [table, onFilterClear]);
57
+ }, [onResetAll]);
59
58
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
60
59
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
61
60
  filterActiveCount > 0;
@@ -46,7 +46,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
46
46
  const categoryOptions = categoriesToFacetOptions(categories);
47
47
  const { brandOptions } = useBrands();
48
48
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "preorder").map((i) => i.itemId));
49
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
49
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
50
50
  const openFilters = useCallback(() => {
51
51
  onFilterReset();
52
52
  setFilterOpen(true);
@@ -56,10 +56,9 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
56
56
  setFilterOpen(false);
57
57
  }, [onFilterApply]);
58
58
  const resetAll = useCallback(() => {
59
- table.setMany({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_CLOSED]: "" });
60
- onFilterClear();
59
+ onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_CLOSED]: "" });
61
60
  setSearchInput("");
62
- }, [table, onFilterClear]);
61
+ }, [onResetAll]);
63
62
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
64
63
  table.get(TABLE_KEYS.SHOW_CLOSED) === "true" ||
65
64
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
@@ -40,7 +40,7 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
40
40
  const { categories } = useCategoryTree();
41
41
  const categoryOptions = categoriesToFacetOptions(categories);
42
42
  const { brandOptions } = useBrands();
43
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
43
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
44
44
  const openFilters = useCallback(() => {
45
45
  onFilterReset();
46
46
  setFilterOpen(true);
@@ -50,10 +50,9 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
50
50
  setFilterOpen(false);
51
51
  }, [onFilterApply]);
52
52
  const resetAll = useCallback(() => {
53
- table.setMany({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_ENDED]: "" });
54
- onFilterClear();
53
+ onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_ENDED]: "" });
55
54
  setSearchInput("");
56
- }, [table, onFilterClear]);
55
+ }, [onResetAll]);
57
56
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
58
57
  table.get(TABLE_KEYS.SHOW_ENDED) === "true" ||
59
58
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||