@mohasinac/appkit 3.7.0 → 3.8.0

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 (40) hide show
  1. package/dist/_internal/server/features/categories/data.js +2 -2
  2. package/dist/_internal/shared/actions/action-registry.js +8 -0
  3. package/dist/constants/api-endpoints.d.ts +3 -0
  4. package/dist/constants/api-endpoints.js +1 -0
  5. package/dist/features/admin/components/AdminBidsView.js +6 -6
  6. package/dist/features/admin/components/AdminBrandsView.js +2 -2
  7. package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
  8. package/dist/features/admin/components/AdminReviewsView.js +5 -5
  9. package/dist/features/admin/components/AdminSublistingCategoriesView.js +1 -1
  10. package/dist/features/auth/repository/session.repository.d.ts +2 -0
  11. package/dist/features/auth/repository/session.repository.js +4 -1
  12. package/dist/features/categories/api/route.js +2 -2
  13. package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
  14. package/dist/features/events/repository/event-entry.repository.js +1 -0
  15. package/dist/features/events/schemas/firestore.d.ts +3 -1
  16. package/dist/features/events/schemas/firestore.js +1 -0
  17. package/dist/features/layout/BottomNavbar.js +1 -1
  18. package/dist/features/layout/NavItem.js +1 -1
  19. package/dist/features/layout/NavbarLayout.js +2 -2
  20. package/dist/features/products/constants/sieve.d.ts +0 -6
  21. package/dist/features/products/constants/sieve.js +4 -2
  22. package/dist/features/products/repository/products.repository.d.ts +20 -0
  23. package/dist/features/products/repository/products.repository.js +5 -0
  24. package/dist/features/promotions/components/CouponsIndexListing.js +16 -3
  25. package/dist/features/search/hooks/useNavSuggestions.js +20 -1
  26. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  27. package/dist/features/stores/components/StoreDetailLayoutView.js +1 -1
  28. package/dist/features/stores/repository/store.repository.js +3 -0
  29. package/dist/features/tester/components/AdminTesterFeedbackView.js +22 -2
  30. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +8 -0
  31. package/dist/features/tester/repository/tester-checklist-response.repository.js +98 -0
  32. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +1 -1
  33. package/dist/seed/bids-seed-data.js +63 -184
  34. package/dist/styles.css +19 -19
  35. package/dist/tailwind-utilities.css +1 -1
  36. package/dist/tokens/themes/default-dark.js +9 -9
  37. package/dist/tokens/themes/default-light.js +9 -9
  38. package/dist/tokens/tokens.css +18 -18
  39. package/dist/ui/components/ListingToolbar.js +1 -1
  40. package/package.json +1 -1
@@ -16,14 +16,14 @@ export const listRootCategories = cache(async () => {
16
16
  /** Featured categories for homepage display (showOnHomepage: true). */
17
17
  export const listFeaturedCategories = cache(async () => {
18
18
  return categoriesRepository
19
- .list({ filters: "isActive==true,isFeatured==true", sorts: "displayOrder", page: 1, pageSize: CATEGORIES_FEATURED_LIMIT })
19
+ .list({ filters: "isActive==true,showOnHomepage==true", sorts: "order", page: 1, pageSize: CATEGORIES_FEATURED_LIMIT })
20
20
  .then((r) => r.data ?? [])
21
21
  .catch(() => []);
22
22
  });
23
23
  /** Nav-menu categories (showInMenu: true, tier 1 + 2). */
24
24
  export const listMenuCategories = cache(async () => {
25
25
  return categoriesRepository
26
- .list({ filters: "isActive==true,display.showInMenu==true", sorts: "displayOrder", page: 1, pageSize: CATEGORIES_MENU_LIMIT })
26
+ .list({ filters: "isActive==true,display.showInMenu==true", sorts: "order", page: 1, pageSize: CATEGORIES_MENU_LIMIT })
27
27
  .then((r) => r.data ?? [])
28
28
  .catch(() => []);
29
29
  });
@@ -1273,6 +1273,14 @@ export const ACTIONS = {
1273
1273
  kind: "secondary",
1274
1274
  permissions: ["admin"],
1275
1275
  },
1276
+ "export-tester-feedback": {
1277
+ id: "admin.export-tester-feedback",
1278
+ label: "Download Report",
1279
+ ariaLabel: "Download tester feedback as a Markdown report",
1280
+ description: "Download every tester checklist response as a Markdown file, grouped by feature area, for offline triage.",
1281
+ kind: "ghost",
1282
+ permissions: ["admin"],
1283
+ },
1276
1284
  "create-checklist-item": {
1277
1285
  id: "admin.create-checklist-item",
1278
1286
  label: "Add Test Case",
@@ -119,6 +119,7 @@ export declare const ADMIN_ENDPOINTS: {
119
119
  readonly TESTER_FEEDBACK: "/api/admin/tester-feedback";
120
120
  readonly TESTER_FEEDBACK_BY_ID: (id: string) => string;
121
121
  readonly TESTER_FEEDBACK_REPORT: "/api/admin/tester-feedback/report";
122
+ readonly TESTER_FEEDBACK_EXPORT: "/api/admin/tester-feedback/export";
122
123
  readonly STORES: "/api/admin/stores";
123
124
  readonly STORE_BY_ID: (uid: string) => string;
124
125
  readonly PAYOUTS: "/api/admin/payouts";
@@ -561,6 +562,7 @@ export declare const API_ENDPOINTS: {
561
562
  readonly TESTER_FEEDBACK: "/api/admin/tester-feedback";
562
563
  readonly TESTER_FEEDBACK_BY_ID: (id: string) => string;
563
564
  readonly TESTER_FEEDBACK_REPORT: "/api/admin/tester-feedback/report";
565
+ readonly TESTER_FEEDBACK_EXPORT: "/api/admin/tester-feedback/export";
564
566
  readonly STORES: "/api/admin/stores";
565
567
  readonly STORE_BY_ID: (uid: string) => string;
566
568
  readonly PAYOUTS: "/api/admin/payouts";
@@ -1005,6 +1007,7 @@ export declare const API_ROUTES: {
1005
1007
  readonly TESTER_FEEDBACK: "/api/admin/tester-feedback";
1006
1008
  readonly TESTER_FEEDBACK_BY_ID: (id: string) => string;
1007
1009
  readonly TESTER_FEEDBACK_REPORT: "/api/admin/tester-feedback/report";
1010
+ readonly TESTER_FEEDBACK_EXPORT: "/api/admin/tester-feedback/export";
1008
1011
  readonly STORES: "/api/admin/stores";
1009
1012
  readonly STORE_BY_ID: (uid: string) => string;
1010
1013
  readonly PAYOUTS: "/api/admin/payouts";
@@ -146,6 +146,7 @@ export const ADMIN_ENDPOINTS = {
146
146
  TESTER_FEEDBACK: "/api/admin/tester-feedback",
147
147
  TESTER_FEEDBACK_BY_ID: (id) => `/api/admin/tester-feedback/${id}`,
148
148
  TESTER_FEEDBACK_REPORT: "/api/admin/tester-feedback/report",
149
+ TESTER_FEEDBACK_EXPORT: "/api/admin/tester-feedback/export",
149
150
  STORES: "/api/admin/stores",
150
151
  STORE_BY_ID: (uid) => `/api/admin/stores/${uid}`,
151
152
  PAYOUTS: "/api/admin/payouts",
@@ -41,23 +41,23 @@ export function AdminBidsView({ children, ...props }) {
41
41
  searchPlaceholder: "Search bids, products, or bidder IDs",
42
42
  emptyLabel: "No bids found",
43
43
  filterKeys: ["status"],
44
- defaultSort: sortBy("bidTime", "DESC"),
44
+ defaultSort: sortBy("bidDate", "DESC"),
45
45
  queryKey: ["admin", "bids", "listing"],
46
46
  endpoint: ADMIN_ENDPOINTS.BIDS,
47
47
  sortOptions: [
48
- { value: sortBy("bidTime", "DESC"), label: "Newest" },
49
- { value: "bidTime", label: "Oldest" },
50
- { value: sortBy("amount", "DESC"), label: "Highest amount" },
48
+ { value: sortBy("bidDate", "DESC"), label: "Newest" },
49
+ { value: "bidDate", label: "Oldest" },
50
+ { value: sortBy("bidAmount", "DESC"), label: "Highest amount" },
51
51
  ],
52
52
  mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
53
53
  id: toStringValue(item.id, `bid-${index}`),
54
54
  primary: [
55
55
  toStringValue(item.productName ?? item.productTitle, "Unknown item"),
56
- toRupees(item.amount),
56
+ toRupees(item.bidAmount ?? item.amount),
57
57
  ].join(" · "),
58
58
  secondary: toStringValue(item.bidderId ?? item.bidderName ?? item.userId, "Unknown bidder"),
59
59
  status: toStringValue(item.status, "active"),
60
- updatedAt: toRelativeDate(item.bidTime ?? item.bidDate ?? item.updatedAt ?? item.createdAt),
60
+ updatedAt: toRelativeDate(item.bidDate ?? item.updatedAt ?? item.createdAt),
61
61
  })),
62
62
  getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
63
63
  buildFilters: (state) => state.status && state.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, state.status) : undefined,
@@ -36,11 +36,11 @@ const ADMIN_BRANDS_CONFIG = {
36
36
  searchPlaceholder: "Search brands by name or slug",
37
37
  emptyLabel: "No brands found",
38
38
  filterKeys: ["isActive"],
39
- defaultSort: sortBy("displayOrder", "ASC"),
39
+ defaultSort: sortBy("order", "ASC"),
40
40
  queryKey: ["admin", "brands", "listing"],
41
41
  endpoint: ADMIN_ENDPOINTS.BRANDS,
42
42
  sortOptions: [
43
- { value: sortBy("displayOrder", "ASC"), label: "Display order" },
43
+ { value: sortBy("order", "ASC"), label: "Display order" },
44
44
  { value: sortBy("name", "ASC"), label: "Name A–Z" },
45
45
  { value: sortBy("name", "DESC"), label: "Name Z–A" },
46
46
  { value: sortBy("createdAt", "DESC"), label: "Newest" },
@@ -70,7 +70,7 @@ export function AdminPrizeDrawsView({ children, ...props }) {
70
70
  { value: sortBy("createdAt", "DESC"), label: "Newest" },
71
71
  { value: sortBy("createdAt", "ASC"), label: "Oldest" },
72
72
  { value: "title", label: "Title A–Z" },
73
- { value: "prizeDrawEndDate", label: "Draw Date Soon" },
73
+ { value: "prizeRevealWindowEnd", label: "Draw Date Soon" },
74
74
  ],
75
75
  columns: PRIZE_DRAW_COLUMNS,
76
76
  mapRows: (response) => toRecordArray(response.items).map((item, index) => {
@@ -83,8 +83,8 @@ export function AdminPrizeDrawsView({ children, ...props }) {
83
83
  status: toStringValue(item.status, "draft"),
84
84
  updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
85
85
  entryFee: priceRaw ? toRupees(priceRaw) : "Free",
86
- drawDate: item.prizeDrawEndDate
87
- ? toRelativeDate(item.prizeDrawEndDate)
86
+ drawDate: item.prizeRevealWindowEnd
87
+ ? toRelativeDate(item.prizeRevealWindowEnd)
88
88
  : "TBA",
89
89
  };
90
90
  }),
@@ -61,12 +61,12 @@ export function AdminReviewsView({ children, ...props }) {
61
61
  comment: toStringValue(raw.body ?? raw.comment, ""),
62
62
  userName: toStringValue(raw.userName ?? raw.sellerName, "Unknown"),
63
63
  userAvatar: toStringValue(raw.userAvatar, "") || undefined,
64
- verified: Boolean(raw.isVerifiedPurchase),
64
+ verified: Boolean(raw.verified),
65
65
  images: Array.isArray(raw.images)
66
66
  ? raw.images
67
67
  : [],
68
68
  helpfulCount: Number(raw.helpfulCount ?? 0),
69
- createdAt: toStringValue(raw.createdAt ?? raw.publishedAt, "") || undefined,
69
+ createdAt: toStringValue(raw.createdAt, "") || undefined,
70
70
  });
71
71
  }, []);
72
72
  if (React.Children.count(children) > 0) {
@@ -78,12 +78,12 @@ export function AdminReviewsView({ children, ...props }) {
78
78
  searchPlaceholder: "Search reviews, products, or seller names",
79
79
  emptyLabel: "No reviews found",
80
80
  filterKeys: ["status", "rating"],
81
- defaultSort: sortBy("publishedAt", "DESC"),
81
+ defaultSort: sortBy("createdAt", "DESC"),
82
82
  queryKey: ["admin", "reviews", "listing"],
83
83
  endpoint: ADMIN_ENDPOINTS.REVIEWS,
84
84
  sortOptions: [
85
- { value: sortBy("publishedAt", "DESC"), label: "Newest" },
86
- { value: "publishedAt", label: "Oldest" },
85
+ { value: sortBy("createdAt", "DESC"), label: "Newest" },
86
+ { value: sortBy("createdAt", "ASC"), label: "Oldest" },
87
87
  { value: sortBy("rating", "DESC"), label: "Highest rating" },
88
88
  { value: sortBy("rating", "ASC"), label: "Lowest rating" },
89
89
  ],
@@ -19,7 +19,7 @@ export function AdminSublistingCategoriesView({ onBulkDelete, }) {
19
19
  sortOptions: [
20
20
  { value: sortBy("name", "ASC"), label: "Name A–Z" },
21
21
  { value: sortBy("name", "DESC"), label: "Name Z–A" },
22
- { value: sortBy("productCount", "DESC"), label: "Most listings" },
22
+ { value: sortBy("metrics.productCount", "DESC"), label: "Most listings" },
23
23
  { value: sortBy("createdAt", "DESC"), label: "Newest" },
24
24
  ],
25
25
  mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
@@ -25,6 +25,8 @@ export declare class SessionRepository extends BaseRepository<SessionDocument> {
25
25
  findAllForAdmin(options?: {
26
26
  userId?: string;
27
27
  limit?: number;
28
+ isActive?: boolean;
29
+ sortAscending?: boolean;
28
30
  }): Promise<{
29
31
  sessions: SessionDocument[];
30
32
  stats: {
@@ -195,10 +195,13 @@ export class SessionRepository extends BaseRepository {
195
195
  }
196
196
  async findAllForAdmin(options) {
197
197
  try {
198
- let query = this.getCollection().orderBy(SESSION_FIELDS.LAST_ACTIVITY, "desc");
198
+ let query = this.getCollection().orderBy(SESSION_FIELDS.LAST_ACTIVITY, options?.sortAscending ? "asc" : "desc");
199
199
  if (options?.userId) {
200
200
  query = query.where(SESSION_FIELDS.USER_ID, "==", options.userId);
201
201
  }
202
+ if (options?.isActive !== undefined) {
203
+ query = query.where(SESSION_FIELDS.IS_ACTIVE, "==", options.isActive);
204
+ }
202
205
  if (options?.limit && options.limit > 0) {
203
206
  query = query.limit(options.limit);
204
207
  }
@@ -47,7 +47,7 @@ function numParam(url, key) {
47
47
  return Number.isFinite(n) ? n : null;
48
48
  }
49
49
  const SAFE_CATEGORY_FILTER_FIELDS = new Set([
50
- "type", "parentIds", "isFeatured", "showOnHomepage",
50
+ "categoryType", "parentIds", "isFeatured", "showOnHomepage",
51
51
  "tier", "isActive", "isLeaf", "isBrand", "isSearchable",
52
52
  ]);
53
53
  function validateSieveFilters(raw, allowedFields) {
@@ -96,7 +96,7 @@ export async function GET(request) {
96
96
  // -- Build Sieve filter string from query params ----------------------------
97
97
  const parts = [];
98
98
  if (type)
99
- parts.push(`type==${type}`);
99
+ parts.push(`categoryType==${type}`);
100
100
  if (parentId)
101
101
  parts.push(`parentIds@=${parentId}`);
102
102
  if (featured === "true")
@@ -43,7 +43,7 @@ const FILTER_KEYS = [TABLE_KEYS.IS_FEATURED, "isBrand", "rootOnly", "tier", "min
43
43
  const SORT_OPTIONS = [
44
44
  { value: sortBy(CATEGORY_FIELDS.NAME, "ASC"), label: "Name A–Z" },
45
45
  { value: sortBy(CATEGORY_FIELDS.NAME), label: "Name Z–A" },
46
- { value: sortBy("productCount", "DESC"), label: "Most Products" },
46
+ { value: sortBy("metrics.productCount", "DESC"), label: "Most Products" },
47
47
  ];
48
48
  export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
49
49
  const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
@@ -150,6 +150,7 @@ EventEntryRepository.SIEVE_FIELDS = {
150
150
  userId: { canFilter: true, canSort: false },
151
151
  userDisplayName: { canFilter: false, canSort: false },
152
152
  reviewStatus: { canFilter: true, canSort: false },
153
+ status: { canFilter: true, canSort: false },
153
154
  submittedAt: { canFilter: true, canSort: true },
154
155
  points: { canFilter: true, canSort: true },
155
156
  };
@@ -69,6 +69,8 @@ export type EventEntryDocument = {
69
69
  reviewedBy?: string;
70
70
  reviewedAt?: Date;
71
71
  reviewNote?: string;
72
+ /** RSVP-style status, distinct from `reviewStatus` (moderation workflow). Drives raffle eligibility + admin confirm/waitlist/cancel actions. */
73
+ status?: "CONFIRMED" | "WAITLISTED" | "CANCELLED";
72
74
  points?: number;
73
75
  raffleEligible?: boolean;
74
76
  spinUsed?: boolean;
@@ -79,7 +81,7 @@ export type EventEntryDocument = {
79
81
  submittedAt: Date;
80
82
  };
81
83
  export declare const EVENT_INDEXED_FIELDS: readonly ["type", "status", "startsAt", "endsAt", "createdAt"];
82
- export declare const EVENT_ENTRY_INDEXED_FIELDS: readonly ["eventId", "userId", "reviewStatus", "submittedAt", "points"];
84
+ export declare const EVENT_ENTRY_INDEXED_FIELDS: readonly ["eventId", "userId", "reviewStatus", "status", "submittedAt", "points"];
83
85
  export declare const EVENT_FIELDS: {
84
86
  readonly ID: "id";
85
87
  readonly TYPE: "type";
@@ -14,6 +14,7 @@ export const EVENT_ENTRY_INDEXED_FIELDS = [
14
14
  "eventId",
15
15
  "userId",
16
16
  "reviewStatus",
17
+ "status",
17
18
  "submittedAt",
18
19
  "points",
19
20
  ];
@@ -23,7 +23,7 @@ export function BottomNavbar({ user, userId, homeHref, shopHref, cartHref, wishl
23
23
  profile: "Profile",
24
24
  more: "More",
25
25
  };
26
- const slotClassName = "relative flex h-full w-full flex-col items-end justify-center gap-[var(--appkit-space-1)] pr-[var(--appkit-space-1)] text-right transition-colors duration-200";
26
+ const slotClassName = "relative flex h-full w-full flex-col items-center justify-center gap-[var(--appkit-space-1)] text-center transition-colors duration-200";
27
27
  const defaultIcon = (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", className: "w-5 h-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" }) }));
28
28
  if (navItems && navItems.length > 0) {
29
29
  // When a wishlist href is available, trade one nav-item slot for a
@@ -9,7 +9,7 @@ export function NavItem({ href, label, icon, isActive = false, variant = "horizo
9
9
  const stateClass = isActive ? activeClassName : inactiveClassName;
10
10
  if (variant === "vertical") {
11
11
  return (_jsxs(Link, { href: href, className: [
12
- "relative flex h-full w-full flex-col items-end justify-center gap-[var(--appkit-space-1)] py-[var(--appkit-space-1)] pr-[var(--appkit-space-1)] text-right transition-colors",
12
+ "relative flex h-full w-full flex-col items-center justify-center gap-[var(--appkit-space-1)] py-[var(--appkit-space-1)] text-center transition-colors",
13
13
  stateClass,
14
14
  className,
15
15
  ]
@@ -49,7 +49,7 @@ export function NavbarLayout({ items, activeHref, id = "main-navbar", ariaLabel
49
49
  }, [updateArrows, items]);
50
50
  const scroll = (dir) => scrollRef.current?.scrollBy({ left: dir === "left" ? -240 : 240, behavior: "smooth" });
51
51
  if (inline) {
52
- return (_jsx(Ul, { "aria-label": ariaLabel, className: "hidden xl:flex items-center justify-end gap-[var(--appkit-space-0-5)] xl:gap-[var(--appkit-space-1)] w-full", children: items.map((item) => (_jsx(Li, { children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }));
52
+ return (_jsx(Ul, { "aria-label": ariaLabel, className: "hidden xl:flex items-center justify-center gap-[var(--appkit-space-0-5)] xl:gap-[var(--appkit-space-1)] w-full", children: items.map((item) => (_jsx(Li, { children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }));
53
53
  }
54
- return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b", children: _jsxs(Row, { className: "container mx-auto sm:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)] max-w-[1920px] h-10", padding: "x-md", align: "center", children: [_jsxs(Div, { className: `relative flex-1 ${__O.hidden}`, children: [canScrollLeft && (_jsx(NavbarChevron, { direction: "left", onClick: () => scroll("left") })), _jsx(Ul, { ref: scrollRef, className: "flex items-center justify-end gap-[var(--appkit-space-0-5)] lg:gap-[var(--appkit-space-1)] overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: items.map((item) => (_jsx(Li, { className: "shrink-0", children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }), canScrollRight && (_jsx(NavbarChevron, { direction: "right", onClick: () => scroll("right") }))] }), rightSlot && _jsx(Div, { className: "shrink-0 ml-2", children: rightSlot })] }) }));
54
+ return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b", children: _jsxs(Row, { className: "container mx-auto sm:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)] max-w-[1920px] h-10", padding: "x-md", align: "center", children: [_jsxs(Div, { className: `relative flex-1 ${__O.hidden}`, children: [canScrollLeft && (_jsx(NavbarChevron, { direction: "left", onClick: () => scroll("left") })), _jsx(Ul, { ref: scrollRef, className: "flex items-center justify-center gap-[var(--appkit-space-0-5)] lg:gap-[var(--appkit-space-1)] overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: items.map((item) => (_jsx(Li, { className: "shrink-0", children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }), canScrollRight && (_jsx(NavbarChevron, { direction: "right", onClick: () => scroll("right") }))] }), rightSlot && _jsx(Div, { className: "shrink-0 ml-2", children: rightSlot })] }) }));
55
55
  }
@@ -153,12 +153,6 @@ export declare const BUNDLE_SORT_OPTIONS: readonly [...({
153
153
  }, {
154
154
  readonly value: string;
155
155
  readonly label: "Price: High to Low";
156
- }, {
157
- readonly value: string;
158
- readonly label: "Most Savings";
159
- }, {
160
- readonly value: string;
161
- readonly label: "Most Items";
162
156
  }];
163
157
  export declare const PRIZE_DRAW_SORT_OPTIONS: readonly [{
164
158
  readonly value: string;
@@ -76,11 +76,13 @@ export const PREORDER_SORT_OPTIONS = [
76
76
  // ---------------------------------------------------------------------------
77
77
  // Bundles
78
78
  // ---------------------------------------------------------------------------
79
+ // "Most Savings" / "Most Items" sorts were removed (2026-08-17) — they
80
+ // referenced `savingsAmount`/`bundleItemCount`, fields that were never added
81
+ // to ProductDocument, so the sorts silently no-opped. Re-add once those
82
+ // fields are computed and written at bundle create/update time.
79
83
  export const BUNDLE_SORT_OPTIONS = [
80
84
  ...BASE_TIME_SORTS.slice(0, 1), // Newest First
81
85
  ...BASE_PRICE_SORTS,
82
- { value: sortBy("savingsAmount", "DESC"), label: "Most Savings" },
83
- { value: sortBy("bundleItemCount", "DESC"), label: "Most Items" },
84
86
  ];
85
87
  // ---------------------------------------------------------------------------
86
88
  // Prize Draws
@@ -123,6 +123,14 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
123
123
  canFilter: boolean;
124
124
  canSort: boolean;
125
125
  };
126
+ isSold: {
127
+ canFilter: boolean;
128
+ canSort: boolean;
129
+ };
130
+ isPartOfBundle: {
131
+ canFilter: boolean;
132
+ canSort: boolean;
133
+ };
126
134
  createdAt: {
127
135
  canFilter: boolean;
128
136
  canSort: boolean;
@@ -135,6 +143,18 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
135
143
  canFilter: boolean;
136
144
  canSort: boolean;
137
145
  };
146
+ prizeRevealWindowStart: {
147
+ canFilter: boolean;
148
+ canSort: boolean;
149
+ };
150
+ prizeRevealWindowEnd: {
151
+ canFilter: boolean;
152
+ canSort: boolean;
153
+ };
154
+ prizeRevealStatus: {
155
+ canFilter: boolean;
156
+ canSort: boolean;
157
+ };
138
158
  startingBid: {
139
159
  canFilter: boolean;
140
160
  canSort: boolean;
@@ -479,9 +479,14 @@ ProductRepository.SIEVE_FIELDS = {
479
479
  viewCount: { canFilter: true, canSort: true },
480
480
  currentBid: { canFilter: true, canSort: true },
481
481
  bidCount: { canFilter: true, canSort: true },
482
+ isSold: { canFilter: true, canSort: true },
483
+ isPartOfBundle: { canFilter: true, canSort: false },
482
484
  createdAt: { canFilter: true, canSort: true },
483
485
  updatedAt: { canFilter: true, canSort: true },
484
486
  auctionEndDate: { canFilter: true, canSort: true },
487
+ prizeRevealWindowStart: { canFilter: true, canSort: true },
488
+ prizeRevealWindowEnd: { canFilter: true, canSort: true },
489
+ prizeRevealStatus: { canFilter: true, canSort: false },
485
490
  startingBid: { canFilter: true, canSort: true },
486
491
  buyNowPrice: { canFilter: true, canSort: true },
487
492
  minBidIncrement: { canFilter: true, canSort: false },
@@ -53,9 +53,10 @@ export function CouponsIndexListing({ initialCoupons, storeSlug: _storeSlug, sto
53
53
  const clearPending = useCallback(() => {
54
54
  setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
55
55
  }, []);
56
- // Build Sieve filter string from committed URL table values
56
+ // Build Sieve filter string from committed URL table values. `validity.isActive==true`
57
+ // is enforced server-side by /api/coupons itself — don't duplicate it here.
57
58
  const buildFilters = () => {
58
- const parts = [`${COUPON_FIELDS.VALIDITY_FIELDS.IS_ACTIVE}==true`];
59
+ const parts = [];
59
60
  const typeFilter = table.get(TABLE_KEYS.TYPE);
60
61
  if (typeFilter)
61
62
  parts.push(`${COUPON_FIELDS.TYPE}==${typeFilter}`);
@@ -76,11 +77,23 @@ export function CouponsIndexListing({ initialCoupons, storeSlug: _storeSlug, sto
76
77
  filters: buildFilters(),
77
78
  });
78
79
  // Use initial data on first load if available and no search/filter active
79
- const displayCoupons = !isLoading && coupons.length > 0
80
+ const baseCoupons = !isLoading && coupons.length > 0
80
81
  ? coupons
81
82
  : !isLoading && initialCoupons && !table.get(TABLE_KEYS.QUERY) && !table.get(TABLE_KEYS.TYPE)
82
83
  ? initialCoupons
83
84
  : coupons;
85
+ // The coupons dataset is always small (a handful to a few dozen per scope), and
86
+ // neither the sieve schema nor the listingProcessor "coupons" collection support
87
+ // substring/full-text search server-side — so the toolbar search filters the
88
+ // already-fetched page client-side against code/name/description instead of
89
+ // silently doing nothing (the search input previously wrote to the URL but was
90
+ // never read by buildFilters()).
91
+ const committedQuery = table.get(TABLE_KEYS.QUERY).trim().toLowerCase();
92
+ const displayCoupons = committedQuery
93
+ ? baseCoupons.filter((c) => [c.code, c.name, c.description]
94
+ .filter(Boolean)
95
+ .some((field) => field.toLowerCase().includes(committedQuery)))
96
+ : baseCoupons;
84
97
  const commitSearch = useCallback(() => {
85
98
  table.set(TABLE_KEYS.QUERY, searchInput.trim());
86
99
  }, [searchInput, table]);
@@ -3,6 +3,23 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
4
  import { SEARCH_ENDPOINTS } from "../../../constants/api-endpoints";
5
5
  const SUGGESTIONS_ENDPOINT = SEARCH_ENDPOINTS.SUGGESTIONS;
6
+ /**
7
+ * /api/search/suggestions only understands the 4 content categories it can
8
+ * actually produce typeahead rows for (singular: product/category/blog/event)
9
+ * — it has no dedicated suggestion category for the other 10 SearchResourceType
10
+ * values (auctions, stores, brands, …). Map the ones it does support; for
11
+ * everything else, omit `type` entirely so the endpoint returns its
12
+ * unfiltered "all" set rather than zero results. Previously this sent the
13
+ * plural resource-type value verbatim (e.g. "products", the search bar's
14
+ * own default), which matched none of the route's singular checks and made
15
+ * every default-state search return empty suggestions.
16
+ */
17
+ const SUGGESTION_TYPE_PARAM_MAP = {
18
+ products: "product",
19
+ categories: "category",
20
+ blog: "blog",
21
+ events: "event",
22
+ };
6
23
  /**
7
24
  * W1-19 — wired to `/api/search/suggestions` 2026-05-23. Fetches up to 20
8
25
  * matches (5 per resource type) and surfaces them as typeahead rows.
@@ -30,7 +47,9 @@ export function useNavSuggestions(query, selectedType, debounceMs = 250) {
30
47
  try {
31
48
  const params = new URLSearchParams({ q: trimmed });
32
49
  if (selectedType && selectedType !== "all") {
33
- params.set("type", selectedType);
50
+ const mappedType = SUGGESTION_TYPE_PARAM_MAP[selectedType];
51
+ if (mappedType)
52
+ params.set("type", mappedType);
34
53
  }
35
54
  const res = await fetch(`${SUGGESTIONS_ENDPOINT}?${params.toString()}`, { signal: ctrl.signal, credentials: "same-origin" });
36
55
  if (!res.ok) {
@@ -67,7 +67,7 @@ export function SellerCouponsView({ onCreateClick, onEditClick, onToggle, onDele
67
67
  isActive: getIsActive(item),
68
68
  })),
69
69
  getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
70
- buildFilters: (state) => state.isActive ? sieveFilter("isActive", SIEVE_OP.EQ, state.isActive) : undefined,
70
+ buildFilters: (state) => state.isActive ? sieveFilter("validity.isActive", SIEVE_OP.EQ, state.isActive) : undefined,
71
71
  primaryAction: onCreateClick
72
72
  ? { label: "Add Coupon", onClick: () => onCreateClick() }
73
73
  : undefined,
@@ -86,7 +86,7 @@ export async function StoreDetailLayoutView({ storeSlug, activeTab, children, sc
86
86
  .then((r) => r.total)
87
87
  .catch(() => 0),
88
88
  couponsRepository
89
- .list({ filters: sieveAnd(sieveFilter("sellerId", SIEVE_OP.EQ, storeId), sieveFilter("validity.isActive", SIEVE_OP.EQ, "true")), page: 1, pageSize: 1 })
89
+ .list({ filters: sieveAnd(sieveFilter("storeId", SIEVE_OP.EQ, storeId), sieveFilter("validity.isActive", SIEVE_OP.EQ, "true")), page: 1, pageSize: 1 })
90
90
  .then((r) => r.total)
91
91
  .catch(() => 0),
92
92
  reviewRepository
@@ -119,6 +119,9 @@ export class StoreRepository extends BaseRepository {
119
119
  [STORE_FIELDS.STATUS]: { canFilter: true, canSort: false },
120
120
  [STORE_FIELDS.IS_PUBLIC]: { canFilter: true, canSort: false },
121
121
  [STORE_FIELDS.CREATED_AT]: { canFilter: false, canSort: true },
122
+ "stats.itemsSold": { canFilter: false, canSort: true },
123
+ "stats.averageRating": { canFilter: false, canSort: true },
124
+ "stats.totalProducts": { canFilter: true, canSort: false },
122
125
  };
123
126
  let baseQuery = this.getCollection();
124
127
  if (activeOnly) {
@@ -1,9 +1,29 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Heading, Stack, Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui";
3
+ import { Button, Heading, Row, Stack, Tabs, TabsContent, TabsList, TabsTrigger, useToast } from "../../../ui";
4
+ import { apiClient } from "../../../http";
5
+ import { normalizeError } from "../../../errors/normalize";
6
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
7
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
4
8
  import { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
5
9
  import { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
6
10
  import { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
7
11
  export function AdminTesterFeedbackView() {
8
- return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Feedback" }), _jsxs(Tabs, { defaultValue: "report", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "report", children: "Report" }), _jsx(TabsTrigger, { value: "issues", children: "Main Issues" }), _jsx(TabsTrigger, { value: "submissions", children: "All Submissions" })] }), _jsx(TabsContent, { value: "report", children: _jsx(AdminTesterFeedbackReportView, {}) }), _jsx(TabsContent, { value: "issues", children: _jsx(AdminTesterFeedbackIssuesView, {}) }), _jsx(TabsContent, { value: "submissions", children: _jsx(AdminTesterFeedbackListView, {}) })] })] }));
12
+ const { showToast } = useToast();
13
+ const handleExportReport = async () => {
14
+ try {
15
+ const blob = await apiClient.blob(ADMIN_ENDPOINTS.TESTER_FEEDBACK_EXPORT);
16
+ const url = URL.createObjectURL(blob);
17
+ const a = document.createElement("a");
18
+ a.href = url;
19
+ a.download = `tester-feedback-report-${new Date().toISOString().slice(0, 10)}.md`;
20
+ a.click();
21
+ URL.revokeObjectURL(url);
22
+ }
23
+ catch (_err) {
24
+ void normalizeError(_err);
25
+ showToast("Report export failed.", "error");
26
+ }
27
+ };
28
+ return (_jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, children: [_jsx(Heading, { level: 1, children: "Tester Feedback" }), _jsx(Button, { variant: "ghost", action: ACTIONS.ADMIN["export-tester-feedback"], onClick: handleExportReport })] }), _jsxs(Tabs, { defaultValue: "report", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "report", children: "Report" }), _jsx(TabsTrigger, { value: "issues", children: "Main Issues" }), _jsx(TabsTrigger, { value: "submissions", children: "All Submissions" })] }), _jsx(TabsContent, { value: "report", children: _jsx(AdminTesterFeedbackReportView, {}) }), _jsx(TabsContent, { value: "issues", children: _jsx(AdminTesterFeedbackIssuesView, {}) }), _jsx(TabsContent, { value: "submissions", children: _jsx(AdminTesterFeedbackListView, {}) })] })] }));
9
29
  }
@@ -38,5 +38,13 @@ export declare class TesterChecklistResponseRepository extends BaseRepository<Te
38
38
  markReviewed(id: string): Promise<TesterChecklistResponseDocument>;
39
39
  /** Single collection scan (small table: testers x cases) — no join needed. */
40
40
  getCoverageReport(): Promise<CoverageReport>;
41
+ /**
42
+ * Markdown dump of every answered case, joined against the checklist item
43
+ * catalog for the human-readable label/href — optimized for a future dev
44
+ * (or Claude session) to read directly and go fix the reported issues.
45
+ * Mirrors appkit/scripts/export-tester-feedback.mjs's CLI output exactly;
46
+ * keep the two in sync.
47
+ */
48
+ getMarkdownReport(siteOrigin: string): Promise<string>;
41
49
  }
42
50
  export declare const testerChecklistResponseRepository: TesterChecklistResponseRepository;