@mohasinac/appkit 2.7.46 → 2.7.47

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 (69) 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/auth/actions/profile-actions.js +4 -1
  5. package/dist/features/auth/repository/user.repository.d.ts +1 -0
  6. package/dist/features/auth/repository/user.repository.js +3 -0
  7. package/dist/features/blog/api/route.js +3 -0
  8. package/dist/features/blog/components/BlogIndexListing.js +2 -0
  9. package/dist/features/blog/components/BlogPostView.js +2 -1
  10. package/dist/features/categories/api/route.js +1 -1
  11. package/dist/features/categories/components/BundleDetailView.js +5 -3
  12. package/dist/features/categories/components/CategoryBundlesListing.js +77 -41
  13. package/dist/features/categories/components/CategoryDetailPageView.js +12 -9
  14. package/dist/features/categories/components/CategoryProductsListing.js +3 -5
  15. package/dist/features/classified/components/ClassifiedIndexListing.js +3 -4
  16. package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +3 -4
  17. package/dist/features/events/actions/event-actions.d.ts +2 -3
  18. package/dist/features/events/actions/event-actions.js +3 -5
  19. package/dist/features/events/api/route.js +7 -7
  20. package/dist/features/events/components/AdminEventEditorView.js +80 -6
  21. package/dist/features/events/components/AdminEventEntriesView.js +33 -4
  22. package/dist/features/events/components/EventFilters.d.ts +3 -0
  23. package/dist/features/events/components/EventFilters.js +1 -0
  24. package/dist/features/events/components/EventLeaderboard.js +1 -1
  25. package/dist/features/events/components/EventsIndexListing.js +10 -7
  26. package/dist/features/events/repository/event-entry.repository.d.ts +2 -2
  27. package/dist/features/events/repository/event-entry.repository.js +32 -7
  28. package/dist/features/events/types/index.d.ts +3 -1
  29. package/dist/features/homepage/components/BrandsSection.js +2 -2
  30. package/dist/features/homepage/components/CustomCardsSection.js +1 -1
  31. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  32. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  33. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  34. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  35. package/dist/features/live/components/LiveItemsIndexListing.js +3 -4
  36. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +3 -4
  37. package/dist/features/products/components/AuctionsIndexListing.js +3 -4
  38. package/dist/features/products/components/PrizeDrawsIndexListing.js +3 -8
  39. package/dist/features/products/components/ProductDetailPageView.js +17 -2
  40. package/dist/features/products/components/ProductsIndexListing.js +10 -6
  41. package/dist/features/products/schemas/firestore.d.ts +2 -0
  42. package/dist/features/reviews/actions/review-actions.js +2 -0
  43. package/dist/features/reviews/components/ReviewDetailShell.js +3 -2
  44. package/dist/features/reviews/components/ReviewFilters.js +1 -1
  45. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -2
  46. package/dist/features/reviews/hooks/useReviews.js +2 -0
  47. package/dist/features/reviews/schemas/firestore.d.ts +4 -0
  48. package/dist/features/reviews/types/index.d.ts +3 -0
  49. package/dist/features/scams/components/ScamRegistryView.js +41 -8
  50. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  51. package/dist/features/stores/components/StoreAuctionsListing.js +3 -4
  52. package/dist/features/stores/components/StoreDetailLayoutView.js +13 -5
  53. package/dist/features/stores/components/StorePreOrdersListing.js +3 -4
  54. package/dist/features/stores/components/StoreProductsListing.js +3 -4
  55. package/dist/index.d.ts +1 -0
  56. package/dist/providers/db-firebase/helpers.d.ts +4 -6
  57. package/dist/providers/db-firebase/helpers.js +18 -5
  58. package/dist/providers/db-firebase/sieve.js +26 -1
  59. package/dist/react/hooks/usePendingFilters.d.ts +2 -0
  60. package/dist/react/hooks/usePendingFilters.js +9 -0
  61. package/dist/react/hooks/usePendingTable.d.ts +5 -0
  62. package/dist/react/hooks/usePendingTable.js +1 -0
  63. package/dist/seed/homepage-sections-seed-data.js +2 -2
  64. package/dist/seed/products-standard-seed-data.js +1 -0
  65. package/dist/tailwind-utilities.css +1 -1
  66. package/dist/ui/components/Drawer.style.css +13 -0
  67. package/dist/ui/components/FilterDrawer.d.ts +4 -2
  68. package/dist/ui/components/FilterDrawer.js +2 -2
  69. package/package.json +1 -1
@@ -70,6 +70,7 @@ export declare const PRODUCT_FIELDS: {
70
70
  readonly PRIZE_REVEAL_WINDOW_START: "prizeRevealWindowStart";
71
71
  readonly PRIZE_REVEAL_WINDOW_END: "prizeRevealWindowEnd";
72
72
  readonly PRIZE_REVEAL_DEADLINE: "prizeRevealDeadline";
73
+ readonly IS_PART_OF_BUNDLE: "isPartOfBundle";
73
74
  readonly CREATED_AT: "createdAt";
74
75
  readonly UPDATED_AT: "updatedAt";
75
76
  readonly STATUS_VALUES: {
@@ -75,6 +75,7 @@ export const PRODUCT_FIELDS = {
75
75
  PRIZE_REVEAL_WINDOW_START: "prizeRevealWindowStart",
76
76
  PRIZE_REVEAL_WINDOW_END: "prizeRevealWindowEnd",
77
77
  PRIZE_REVEAL_DEADLINE: "prizeRevealDeadline",
78
+ IS_PART_OF_BUNDLE: "isPartOfBundle",
78
79
  CREATED_AT: "createdAt",
79
80
  UPDATED_AT: "updatedAt",
80
81
  STATUS_VALUES: {
@@ -103,10 +103,9 @@ export class UnitOfWork {
103
103
  serverLogger.debug("[UnitOfWork] Batch write committed successfully");
104
104
  }
105
105
  catch (error) {
106
- serverLogger.error("[UnitOfWork] Batch write failed", {
107
- error: error instanceof Error ? error.message : String(error),
108
- });
109
- throw new DatabaseError("Batch write failed", error);
106
+ const detail = error instanceof Error ? error.message : String(error);
107
+ serverLogger.error("[UnitOfWork] Batch write failed", { error: detail });
108
+ throw new DatabaseError(`Batch write failed: ${detail}`, error);
110
109
  }
111
110
  }
112
111
  }
@@ -23,7 +23,10 @@ export async function getUserSessions(userId) {
23
23
  return sessionRepository.findAllByUser(userId);
24
24
  }
25
25
  export async function getPublicUserProfile(userId) {
26
- const user = await userRepository.findById(userId);
26
+ let user = await userRepository.findById(userId);
27
+ // Fallback: userId may be a Firebase Auth UID rather than the document slug
28
+ if (!user)
29
+ user = await userRepository.findByUid(userId);
27
30
  if (!user)
28
31
  return null;
29
32
  return {
@@ -10,6 +10,7 @@ export declare class UserRepository extends BaseRepository<UserDocument> {
10
10
  protected mapDoc<D = UserDocument>(snap: DocumentSnapshot): D;
11
11
  createWithId(id: string, data: Partial<UserDocument>): Promise<UserDocument>;
12
12
  create(input: Omit<UserDocument, "id" | "createdAt" | "updatedAt">): Promise<UserDocument>;
13
+ findByUid(uid: string): Promise<UserDocument | null>;
13
14
  findByEmail(email: string): Promise<UserDocument | null>;
14
15
  findByPhone(phoneNumber: string): Promise<UserDocument | null>;
15
16
  findByRole(role: UserRole): Promise<UserDocument[]>;
@@ -58,6 +58,9 @@ export class UserRepository extends BaseRepository {
58
58
  .set(prepareForFirestore(encrypted));
59
59
  return { id, ...userData };
60
60
  }
61
+ async findByUid(uid) {
62
+ return this.findOneBy(USER_FIELDS.UID, uid);
63
+ }
61
64
  async findByEmail(email) {
62
65
  return this.findOneBy(USER_FIELDS.EMAIL_INDEX, piiBlindIndex(email));
63
66
  }
@@ -29,6 +29,9 @@ export async function GET(request) {
29
29
  const category = param(url, "category");
30
30
  if (category)
31
31
  parts.push(`category==${category}`);
32
+ const tags = param(url, "tags");
33
+ if (tags)
34
+ parts.push(`tags@=${tags}`);
32
35
  const q = param(url, "q");
33
36
  if (q)
34
37
  parts.push(`title@=*${q}`);
@@ -94,11 +94,13 @@ export function BlogIndexListing({ initialData }) {
94
94
  }, [table]);
95
95
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
96
96
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
97
+ !!table.get(TABLE_KEYS.TAGS) ||
97
98
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
98
99
  activeFilterCount > 0;
99
100
  const params = {
100
101
  q: table.get(TABLE_KEYS.QUERY) || undefined,
101
102
  category: (table.get(TABLE_KEYS.CATEGORY) || undefined),
103
+ tags: table.get(TABLE_KEYS.TAGS) || undefined,
102
104
  sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
103
105
  page: table.getNumber(TABLE_KEYS.PAGE, 1),
104
106
  perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, PAGE_SIZE),
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
+ import Link from "next/link";
4
5
  import { Div, Heading, RichText, Row, Span, Text } from "../../../ui";
5
6
  import { useBlogPost } from "../hooks/useBlog";
6
7
  import { BlogCard } from "./BlogListView";
@@ -45,5 +46,5 @@ export function BlogPostView({ slug, initialData, labels = {}, renderImage, rend
45
46
  })
46
47
  : "";
47
48
  const coverImageUrl = getMediaUrl(post.coverImage);
48
- return (_jsxs(Div, { className: `min-h-screen ${className}`, children: [coverImageUrl && (_jsxs(Div, { className: "relative h-72 md:h-96 overflow-hidden", children: [renderImage ? (renderImage(post)) : (_jsx(Div, { role: "img", "aria-label": post.title, className: "h-full w-full bg-cover bg-center", style: { backgroundImage: `url(${coverImageUrl})` } })), _jsx(Div, { className: "absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" })] })), _jsxs(Div, { className: "max-w-3xl mx-auto px-4 py-12", children: [renderBlogPostHeader(post, date, labels), post.tags && post.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-8", children: post.tags.map((tag) => (_jsxs(Span, { className: "inline-block px-3 py-1 rounded-full bg-neutral-100 text-neutral-600 text-xs font-medium", children: ["#", tag] }, tag))) })), renderAuthorBio && _jsx(Div, { className: "mb-6", children: renderAuthorBio(post) }), _jsx(Div, { className: "bg-white dark:bg-slate-900 rounded-xl border border-neutral-200 dark:border-slate-700 p-8 mb-12", children: renderContent ? renderContent(post) : (_jsx(RichText, { html: normalizeRichTextHtml(post.content ?? ""), proseClass: "prose max-w-none dark:prose-invert" })) }), renderBlogPostRelated(related, labels, renderRelatedCard), renderBackButton && _jsx(Div, { className: "mt-10 pt-8 border-t border-neutral-200", children: renderBackButton() })] })] }));
49
+ return (_jsxs(Div, { className: `min-h-screen ${className}`, children: [coverImageUrl && (_jsxs(Div, { className: "relative h-72 md:h-96 overflow-hidden", children: [renderImage ? (renderImage(post)) : (_jsx(Div, { role: "img", "aria-label": post.title, className: "h-full w-full bg-cover bg-center", style: { backgroundImage: `url(${coverImageUrl})` } })), _jsx(Div, { className: "absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" })] })), _jsxs(Div, { className: "max-w-3xl mx-auto px-4 py-12", children: [renderBlogPostHeader(post, date, labels), post.tags && post.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-8", children: post.tags.map((tag) => (_jsxs(Link, { href: `${String(ROUTES.PUBLIC.BLOG)}?tags=${encodeURIComponent(tag)}`, className: "inline-block px-3 py-1 rounded-full bg-neutral-100 hover:bg-primary/10 text-neutral-600 hover:text-primary text-xs font-medium transition-colors", children: ["#", tag] }, tag))) })), renderAuthorBio && _jsx(Div, { className: "mb-6", children: renderAuthorBio(post) }), _jsx(Div, { className: "bg-white dark:bg-slate-900 rounded-xl border border-neutral-200 dark:border-slate-700 p-8 mb-12", children: renderContent ? renderContent(post) : (_jsx(RichText, { html: normalizeRichTextHtml(post.content ?? ""), proseClass: "prose max-w-none dark:prose-invert" })) }), renderBlogPostRelated(related, labels, renderRelatedCard), renderBackButton && _jsx(Div, { className: "mt-10 pt-8 border-t border-neutral-200", children: renderBackButton() })] })] }));
49
50
  }
@@ -101,7 +101,7 @@ export async function GET(request) {
101
101
  if (featured === "true")
102
102
  parts.push("isFeatured==true");
103
103
  if (isBrand === "true")
104
- parts.push("type==brand");
104
+ parts.push("categoryType==brand");
105
105
  if (showOnHomepage === "true")
106
106
  parts.push("showOnHomepage==true");
107
107
  if (tier !== null)
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Badge, Container, Div, Heading, Main, Row, Section, Stack, Text, } from "../../../ui";
3
3
  import { formatCurrency } from "../../../utils/number.formatter";
4
+ import Link from "next/link";
4
5
  import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy";
5
6
  import { BundleBuyNowCta } from "./BundleBuyNowCta";
6
7
  import { BundleCollage } from "./BundleCollage";
8
+ import { ROUTES } from "../../../next/routing/route-map";
7
9
  const STOCK_BADGE_TEXT = {
8
10
  in_stock: BUNDLE_COPY.stockBadge.in_stock,
9
11
  out_of_stock: BUNDLE_COPY.stockBadge.out_of_stock,
@@ -16,7 +18,7 @@ export function BundleDetailView({ bundle, members = [], onBuyNow, }) {
16
18
  const priceLabel = bundle.bundlePriceInPaise
17
19
  ? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
18
20
  : BUNDLE_COPY.detail.priceFallback;
19
- return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { gap: "lg", align: "start", className: "flex-col md:flex-row", children: [_jsx(Div, { className: "aspect-video w-full overflow-hidden rounded-2xl bg-zinc-100 md:w-1/2 dark:bg-zinc-800", children: cover ? (
20
- // eslint-disable-next-line @next/next/no-img-element, lir/no-raw-media-elements
21
- _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-6xl", children: PLACEHOLDER_EMOJI })) }), _jsxs(Stack, { gap: "md", className: "flex-1", children: [_jsx(Heading, { level: 1, className: "text-3xl font-semibold text-zinc-900 dark:text-zinc-100", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Text, { size: "lg", weight: "bold", children: priceLabel }), _jsxs(Text, { size: "sm", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.detail.itemCount(memberCount)] }), _jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: STOCK_BADGE_TEXT[stock] })] }), bundle.description && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-sm font-semibold text-[var(--appkit-color-text-muted)]", children: BUNDLE_COPY.detailDescriptionHeading }), _jsx(Text, { className: "whitespace-pre-line", children: bundle.description })] })), onBuyNow ? (_jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow })) : (_jsxs(Div, { className: "rounded-xl border border-dashed border-zinc-300 bg-zinc-50 p-4 text-sm dark:border-zinc-700 dark:bg-zinc-900", "aria-live": "polite", children: [_jsx(Text, { weight: "semibold", className: "mb-1 block", children: BUNDLE_COPY.detail.ctaDisabled }), _jsx(Text, { size: "sm", color: "muted", children: BUNDLE_COPY.detail.ctaHint })] }))] })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, className: "text-xl font-semibold text-zinc-900 dark:text-zinc-100", children: BUNDLE_COPY.detailItemsHeading }), members.length === 0 ? (_jsx(Text, { color: "muted", children: BUNDLE_COPY.detail.emptyMembers })) : (_jsx(BundleCollage, { members: members }))] })] }) }) }) }));
21
+ return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { gap: "lg", align: "start", className: "flex-col md:flex-row", children: [_jsx(Div, { className: "w-full md:w-2/5", children: members.length > 0 ? (_jsx(BundleCollage, { members: members })) : (_jsx(Div, { className: "aspect-video w-full overflow-hidden rounded-2xl bg-zinc-100 dark:bg-zinc-800", children: cover ? (
22
+ // eslint-disable-next-line @next/next/no-img-element, lir/no-raw-media-elements
23
+ _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-6xl", children: PLACEHOLDER_EMOJI })) })) }), _jsxs(Stack, { gap: "md", className: "flex-1", children: [_jsx(Heading, { level: 1, className: "text-3xl font-semibold text-zinc-900 dark:text-zinc-100", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Text, { size: "lg", weight: "bold", children: priceLabel }), _jsxs(Text, { size: "sm", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.detail.itemCount(memberCount)] }), _jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: STOCK_BADGE_TEXT[stock] })] }), bundle.description && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-sm font-semibold text-[var(--appkit-color-text-muted)]", children: BUNDLE_COPY.detailDescriptionHeading }), _jsx(Text, { className: "whitespace-pre-line", children: bundle.description })] })), onBuyNow ? (_jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow })) : (_jsxs(Div, { className: "rounded-xl border border-dashed border-zinc-300 bg-zinc-50 p-4 text-sm dark:border-zinc-700 dark:bg-zinc-900", "aria-live": "polite", children: [_jsx(Text, { weight: "semibold", className: "mb-1 block", children: BUNDLE_COPY.detail.ctaDisabled }), _jsx(Text, { size: "sm", color: "muted", children: BUNDLE_COPY.detail.ctaHint })] }))] })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, className: "text-xl font-semibold text-zinc-900 dark:text-zinc-100", children: BUNDLE_COPY.detailItemsHeading }), members.length === 0 ? (_jsx(Text, { color: "muted", children: BUNDLE_COPY.detail.emptyMembers })) : (_jsx("ul", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: members.map((p, i) => (_jsxs("li", { className: "flex items-center gap-2 rounded-lg border border-zinc-100 dark:border-zinc-800 px-3 py-2 text-sm", children: [_jsxs("span", { className: "text-xs font-semibold text-zinc-400 w-5", children: ["#", i + 1] }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCT_DETAIL?.(p.slug ?? p.id) ?? "#"), className: "flex-1 truncate font-medium hover:underline hover:text-primary", children: p.title })] }, p.id))) }))] })] }) }) }) }));
22
24
  }
@@ -2,22 +2,30 @@
2
2
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  /**
4
4
  * CategoryBundlesListing — SB-UNI-D + V replacement for the deleted
5
- * BundlesByCategoryListing. Renders a sortable list of bundles that live
6
- * as `categoryType:"bundle"` rows on the categories collection.
5
+ * BundlesByCategoryListing. Renders a sortable, searchable, paginated list of
6
+ * bundles that live as `categoryType:"bundle"` rows on the categories collection.
7
7
  *
8
- * Parent server-fetches the list; client owns only the sort state.
8
+ * All data is server-fetched; this component owns URL-state driven
9
+ * sort/search/filter/pagination on top of the initial snapshot.
9
10
  */
10
- import { useMemo, useState } from "react";
11
+ import { useMemo, useCallback, useState } from "react";
11
12
  import Link from "next/link";
12
- import { Badge, Div, Row, Select, Stack, Text } from "../../../ui";
13
+ import { Badge, Div, Row, Text } from "../../../ui";
14
+ import { ListingToolbar, Pagination, FilterDrawer } from "../../../ui";
15
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
13
16
  import { ROUTES } from "../../../next/routing/route-map";
14
17
  import { formatCurrency } from "../../../utils/number.formatter";
15
18
  import { BundleBuyNowCta } from "./BundleBuyNowCta";
19
+ import { TABLE_KEYS } from "../../../constants/table-keys";
20
+ const PAGE_SIZE = 12;
21
+ const FILTER_KEYS = ["showOutOfStock"];
16
22
  const SORT_OPTIONS = [
17
- { label: "Newest", value: "newest" },
18
- { label: "Price ↑", value: "price-asc" },
19
- { label: "Price ↓", value: "price-desc" },
23
+ { value: "-createdAt", label: "Newest First" },
24
+ { value: "createdAt", label: "Oldest First" },
25
+ { value: "price", label: "Price: Low to High" },
26
+ { value: "-price", label: "Price: High to Low" },
20
27
  ];
28
+ const DEFAULT_SORT = "-createdAt";
21
29
  const STOCK_BADGE_TEXT = {
22
30
  in_stock: "",
23
31
  out_of_stock: "Not active",
@@ -27,37 +35,66 @@ const STOCK_BADGE_VARIANT = {
27
35
  out_of_stock: "warning",
28
36
  };
29
37
  const PLACEHOLDER_EMOJI = "📦";
30
- const COPY = {
31
- empty: "No bundles available",
32
- forBrand: (name) => ` for ${name}`,
33
- yetSuffix: " yet.",
34
- sortLabel: "Sort:",
35
- items: (n) => `${n} item${n !== 1 ? "s" : ""}`,
36
- priceFallback: "—",
37
- };
38
- function comparator(sort) {
39
- return (a, b) => {
40
- if (sort === "price-asc") {
41
- return (a.bundlePriceInPaise ?? 0) - (b.bundlePriceInPaise ?? 0);
42
- }
43
- if (sort === "price-desc") {
44
- return (b.bundlePriceInPaise ?? 0) - (a.bundlePriceInPaise ?? 0);
45
- }
46
- const aT = a.createdAt instanceof Date ? a.createdAt.getTime() : 0;
47
- const bT = b.createdAt instanceof Date ? b.createdAt.getTime() : 0;
48
- return bT - aT;
49
- };
50
- }
51
38
  export function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, }) {
52
- const [sort, setSort] = useState("newest");
53
- const filtered = useMemo(() => initialBundles
54
- .filter((c) => c.categoryType === "bundle")
55
- .slice()
56
- .sort(comparator(sort)), [initialBundles, sort]);
57
- if (filtered.length === 0) {
58
- return (_jsx(Div, { className: "rounded-2xl border border-dashed border-zinc-200 py-16 text-center dark:border-zinc-700", children: _jsxs(Text, { color: "muted", children: [COPY.empty, brandName ? COPY.forBrand(brandName) : "", COPY.yetSuffix] }) }));
39
+ const table = useUrlTable({ defaults: { sort: DEFAULT_SORT } });
40
+ const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
41
+ const [filterOpen, setFilterOpen] = useState(false);
42
+ const [pendingShowOutOfStock, setPendingShowOutOfStock] = useState(table.get("showOutOfStock") === "true");
43
+ const sort = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
44
+ const query = table.get(TABLE_KEYS.QUERY) || "";
45
+ const showOutOfStock = table.get("showOutOfStock") === "true";
46
+ const page = table.getNumber(TABLE_KEYS.PAGE, 1);
47
+ const commitSearch = useCallback(() => {
48
+ table.set(TABLE_KEYS.QUERY, searchInput.trim());
49
+ }, [searchInput, table]);
50
+ const openFilters = useCallback(() => {
51
+ setPendingShowOutOfStock(table.get("showOutOfStock") === "true");
52
+ setFilterOpen(true);
53
+ }, [table]);
54
+ const applyFilters = useCallback(() => {
55
+ table.setMany({ showOutOfStock: pendingShowOutOfStock ? "true" : "", page: "1" });
56
+ setFilterOpen(false);
57
+ }, [pendingShowOutOfStock, table]);
58
+ const resetAll = useCallback(() => {
59
+ table.setMany({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", showOutOfStock: "" });
60
+ setSearchInput("");
61
+ }, [table]);
62
+ const hasActiveState = !!query ||
63
+ sort !== DEFAULT_SORT ||
64
+ showOutOfStock;
65
+ const filtered = useMemo(() => {
66
+ let list = initialBundles.filter((c) => c.categoryType === "bundle");
67
+ if (!showOutOfStock)
68
+ list = list.filter((c) => c.bundleStockStatus !== "out_of_stock");
69
+ if (query) {
70
+ const q = query.toLowerCase();
71
+ list = list.filter((c) => c.name.toLowerCase().includes(q));
72
+ }
73
+ list = [...list].sort((a, b) => {
74
+ if (sort === "price")
75
+ return (a.bundlePriceInPaise ?? 0) - (b.bundlePriceInPaise ?? 0);
76
+ if (sort === "-price")
77
+ return (b.bundlePriceInPaise ?? 0) - (a.bundlePriceInPaise ?? 0);
78
+ if (sort === "createdAt") {
79
+ const aT = a.createdAt instanceof Date ? a.createdAt.getTime() : 0;
80
+ const bT = b.createdAt instanceof Date ? b.createdAt.getTime() : 0;
81
+ return aT - bT;
82
+ }
83
+ const aT = a.createdAt instanceof Date ? a.createdAt.getTime() : 0;
84
+ const bT = b.createdAt instanceof Date ? b.createdAt.getTime() : 0;
85
+ return bT - aT;
86
+ });
87
+ return list;
88
+ }, [initialBundles, showOutOfStock, query, sort]);
89
+ const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
90
+ const safePage = Math.min(Math.max(1, page), totalPages);
91
+ const pageItems = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
92
+ const filterActiveCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
93
+ if (filtered.length === 0 && !query && !showOutOfStock) {
94
+ return (_jsx(Div, { className: "rounded-2xl border border-dashed border-zinc-200 py-16 text-center dark:border-zinc-700", children: _jsxs(Text, { color: "muted", children: ["No bundles available", brandName ? ` for ${brandName}` : "", " yet."] }) }));
59
95
  }
60
- return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "end", children: [_jsx(Text, { size: "sm", color: "muted", children: COPY.sortLabel }), _jsx(Select, { options: SORT_OPTIONS, value: sort, onValueChange: setSort, "aria-label": COPY.sortLabel })] }), _jsx(Div, { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((bundle) => (_jsx(BundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }));
96
+ return (_jsxs(Div, { className: "min-h-[40vh]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search bundles...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: (e) => { if (e.key === "Enter")
97
+ commitSearch(); }, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: pageItems.length === 0 ? (_jsxs(Text, { className: "py-16 text-center text-sm text-zinc-500 dark:text-zinc-400", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: pageItems.map((bundle) => (_jsx(BundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: () => setPendingShowOutOfStock(false), activeCount: filterActiveCount, hideTrigger: true, children: _jsx("div", { className: "space-y-4 p-4", children: _jsxs("label", { className: "flex items-center justify-between gap-3 cursor-pointer", children: [_jsx("span", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Show out-of-stock bundles" }), _jsx("button", { type: "button", role: "switch", "aria-checked": pendingShowOutOfStock, onClick: () => setPendingShowOutOfStock((v) => !v), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${pendingShowOutOfStock ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${pendingShowOutOfStock ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }) })] }));
61
98
  }
62
99
  function BundleCard({ bundle, onBuyNow }) {
63
100
  const memberCount = bundle.bundleProductIds?.length ?? 0;
@@ -65,9 +102,8 @@ function BundleCard({ bundle, onBuyNow }) {
65
102
  const badge = STOCK_BADGE_TEXT[stock];
66
103
  const cover = bundle.display?.coverImage;
67
104
  const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
68
- return (_jsxs(Stack, { gap: "none", className: "rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-video overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
105
+ const price = bundle.bundlePriceInPaise;
106
+ return (_jsxs("div", { className: "flex flex-col rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block flex-1 p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-video overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
69
107
  // eslint-disable-next-line @next/next/no-img-element
70
- _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePriceInPaise
71
- ? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
72
- : COPY.priceFallback }), _jsx(Text, { size: "xs", color: "muted", children: COPY.items(memberCount) })] }), badge && (_jsx(Badge, { variant: STOCK_BADGE_VARIANT[stock], className: "mt-1", children: badge }))] }), onBuyNow && (_jsx(Div, { className: "border-t border-zinc-100 p-3 pt-2 dark:border-zinc-800", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
108
+ _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", children: [_jsx(Text, { size: "sm", weight: "bold", children: price ? formatCurrency(price / 100, "INR") : "—" }), _jsxs(Text, { size: "xs", color: "muted", children: [memberCount, " item", memberCount !== 1 ? "s" : ""] })] }), badge && (_jsx(Badge, { variant: STOCK_BADGE_VARIANT[stock], className: "mt-1", children: badge }))] }), onBuyNow && (_jsx(Div, { className: "border-t border-zinc-100 p-3 pt-2 dark:border-zinc-800", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
73
109
  }
@@ -8,41 +8,44 @@ export async function CategoryDetailPageView({ slug }) {
8
8
  const category = await categoriesRepository
9
9
  .getCategoryBySlug(slug)
10
10
  .catch(() => undefined);
11
+ // Use categorySlugs@=<id> (array-contains) — products store category FK in
12
+ // the categorySlugs[] array, not the legacy `category` string field.
13
+ const catFilter = category?.id ? `categorySlugs@=${category.id}` : null;
11
14
  const [productsResult, auctionsCountResult, preOrdersCountResult, prizeDrawsCountResult, bundlesResult, childCategories] = await Promise.all([
12
- category?.id
15
+ catFilter
13
16
  ? productRepository
14
17
  .list({
15
- filters: `status==published,category==${category.id},listingType==standard`,
18
+ filters: `status==published,${catFilter},listingType==standard`,
16
19
  sorts: "-createdAt",
17
20
  page: 1,
18
21
  pageSize: 24,
19
22
  })
20
23
  .catch(() => null)
21
24
  : Promise.resolve(null),
22
- category?.id
25
+ catFilter
23
26
  ? productRepository
24
27
  .list({
25
- filters: `status==published,category==${category.id},listingType==auction`,
28
+ filters: `status==published,${catFilter},listingType==auction`,
26
29
  sorts: "auctionEndDate",
27
30
  page: 1,
28
31
  pageSize: 1,
29
32
  })
30
33
  .catch(() => null)
31
34
  : Promise.resolve(null),
32
- category?.id
35
+ catFilter
33
36
  ? productRepository
34
37
  .list({
35
- filters: `status==published,category==${category.id},listingType==pre-order`,
38
+ filters: `status==published,${catFilter},listingType==pre-order`,
36
39
  sorts: "-createdAt",
37
40
  page: 1,
38
41
  pageSize: 1,
39
42
  })
40
43
  .catch(() => null)
41
44
  : Promise.resolve(null),
42
- category?.id
45
+ catFilter
43
46
  ? productRepository
44
47
  .list({
45
- filters: `status==published,category==${category.id},listingType==prize-draw`,
48
+ filters: `status==published,${catFilter},listingType==prize-draw`,
46
49
  sorts: "-createdAt",
47
50
  page: 1,
48
51
  pageSize: 1,
@@ -51,7 +54,7 @@ export async function CategoryDetailPageView({ slug }) {
51
54
  : Promise.resolve(null),
52
55
  // SB-UNI-D — bundles fetched from the categories collection. We pull
53
56
  // all active bundle rows; the carousel filters by category affinity.
54
- category?.id
57
+ catFilter
55
58
  ? categoriesRepository
56
59
  .listByType("bundle", { activeOnly: true, limit: 50 })
57
60
  .catch(() => [])
@@ -32,7 +32,7 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
32
32
  const localCart = useGuestCart();
33
33
  const localWishlist = useGuestWishlist();
34
34
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "product").map((i) => i.itemId));
35
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
35
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
36
36
  const params = {
37
37
  q: table.get("q") || undefined,
38
38
  category: categoryId || undefined,
@@ -48,7 +48,6 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
48
48
  const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
49
49
  const commitSearch = useCallback(() => {
50
50
  table.set("q", searchInput.trim());
51
- table.setPage(1);
52
51
  }, [searchInput, table]);
53
52
  const openFilters = useCallback(() => {
54
53
  onFilterReset();
@@ -63,10 +62,9 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
63
62
  table.set("view", next);
64
63
  };
65
64
  const resetAll = useCallback(() => {
66
- table.setMany({ q: "", sort: "" });
67
- onFilterClear();
65
+ onResetAll({ q: "", sort: "" });
68
66
  setSearchInput("");
69
- }, [table, onFilterClear]);
67
+ }, [onResetAll]);
70
68
  const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
71
69
  const hasActiveState = !!table.get("q") ||
72
70
  table.get("sort") !== "-createdAt" ||
@@ -44,7 +44,7 @@ export function ClassifiedIndexListing({ initialData }) {
44
44
  const { categories } = useCategoryTree();
45
45
  const categoryOptions = categoriesToFacetOptions(categories);
46
46
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "classified").map((i) => i.itemId));
47
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
47
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
48
48
  const openFilters = useCallback(() => {
49
49
  onFilterReset();
50
50
  setFilterOpen(true);
@@ -54,10 +54,9 @@ export function ClassifiedIndexListing({ initialData }) {
54
54
  setFilterOpen(false);
55
55
  }, [onFilterApply]);
56
56
  const resetAll = useCallback(() => {
57
- table.setMany({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
58
- onFilterClear();
57
+ onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
59
58
  setSearchInput("");
60
- }, [table, onFilterClear]);
59
+ }, [onResetAll]);
61
60
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
62
61
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
63
62
  filterActiveCount > 0;
@@ -41,7 +41,7 @@ export function DigitalCodesIndexListing({ initialData }) {
41
41
  const { categories } = useCategoryTree();
42
42
  const categoryOptions = categoriesToFacetOptions(categories);
43
43
  const wishlistedIds = new Set(localWishlist.items.filter((i) => i.type === "digital-code").map((i) => i.itemId));
44
- const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onFilterReset } = usePendingTable(table, FILTER_KEYS);
44
+ const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
45
45
  const openFilters = useCallback(() => {
46
46
  onFilterReset();
47
47
  setFilterOpen(true);
@@ -51,10 +51,9 @@ export function DigitalCodesIndexListing({ initialData }) {
51
51
  setFilterOpen(false);
52
52
  }, [onFilterApply]);
53
53
  const resetAll = useCallback(() => {
54
- table.setMany({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
55
- onFilterClear();
54
+ onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
56
55
  setSearchInput("");
57
- }, [table, onFilterClear]);
56
+ }, [onResetAll]);
58
57
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
59
58
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
60
59
  filterActiveCount > 0;
@@ -4,7 +4,6 @@
4
4
  * Business logic for event CRUD, status changes, entries, and public access.
5
5
  * Auth, rate-limiting, and input validation are handled by the calling server action.
6
6
  */
7
- import { maskPublicEventEntry } from "../../../security";
8
7
  import type { EventDocument, EventEntryDocument } from "../schemas";
9
8
  import type { EventStatus } from "../types";
10
9
  import type { FirebaseSieveResult } from "../../../providers/db-firebase";
@@ -57,7 +56,7 @@ export declare function createEvent(adminId: string, input: CreateEventInput): P
57
56
  export declare function updateEvent(adminId: string, id: string, input: UpdateEventInput): Promise<EventDocument>;
58
57
  export declare function deleteEvent(adminId: string, id: string): Promise<void>;
59
58
  export declare function changeEventStatus(adminId: string, id: string, status: EventStatus): Promise<EventDocument>;
60
- export declare function adminUpdateEventEntry(adminId: string, eventId: string, entryId: string, reviewStatus: "approved" | "flagged", reviewNote?: string): Promise<void>;
59
+ export declare function adminUpdateEventEntry(adminId: string, eventId: string, entryId: string, reviewStatus: "approved" | "flagged", reviewNote?: string, points?: number): Promise<void>;
61
60
  export declare function listPublicEvents(params?: string | {
62
61
  filters?: string;
63
62
  sorts?: string;
@@ -65,7 +64,7 @@ export declare function listPublicEvents(params?: string | {
65
64
  pageSize?: number;
66
65
  }): Promise<FirebaseSieveResult<EventDocument>>;
67
66
  export declare function getPublicEventById(id: string): Promise<EventDocument | null>;
68
- export declare function getEventLeaderboard(eventId: string): Promise<ReturnType<typeof maskPublicEventEntry<EventEntryDocument>>[]>;
67
+ export declare function getEventLeaderboard(eventId: string): Promise<import("../types").LeaderboardEntry[]>;
69
68
  export declare function adminListEvents(params?: {
70
69
  filters?: string;
71
70
  sorts?: string;
@@ -7,7 +7,6 @@
7
7
  import { serverLogger } from "../../../monitoring";
8
8
  import { eventRepository } from "../repository/events.repository";
9
9
  import { eventEntryRepository } from "../repository/event-entry.repository";
10
- import { maskPublicEventEntry } from "../../../security";
11
10
  import { ERROR_MESSAGES, AuthorizationError, ValidationError, NotFoundError, } from "../../../errors";
12
11
  import { resolveDate } from "../../../utils";
13
12
  const ERR_EVENT_NOT_FOUND = "Event not found";
@@ -83,8 +82,8 @@ export async function changeEventStatus(adminId, id, status) {
83
82
  return updated;
84
83
  }
85
84
  // --- Admin: Update Entry Review -------------------------------------------
86
- export async function adminUpdateEventEntry(adminId, eventId, entryId, reviewStatus, reviewNote) {
87
- await eventEntryRepository.reviewEntry(entryId, reviewStatus, adminId, reviewNote);
85
+ export async function adminUpdateEventEntry(adminId, eventId, entryId, reviewStatus, reviewNote, points) {
86
+ await eventEntryRepository.reviewEntry(entryId, reviewStatus, adminId, reviewNote, points);
88
87
  serverLogger.info("adminUpdateEventEntry", {
89
88
  adminId,
90
89
  eventId,
@@ -142,8 +141,7 @@ export async function getPublicEventById(id) {
142
141
  return event;
143
142
  }
144
143
  export async function getEventLeaderboard(eventId) {
145
- const entries = await eventEntryRepository.getLeaderboard(eventId);
146
- return entries.map(maskPublicEventEntry);
144
+ return eventEntryRepository.getLeaderboard(eventId);
147
145
  }
148
146
  export async function adminListEvents(params) {
149
147
  const sieve = {
@@ -43,16 +43,16 @@ export async function GET(request) {
43
43
  const page = numParam(url, "page", 1);
44
44
  const pageSize = numParam(url, "pageSize", 24);
45
45
  const sort = param(url, "sorts") ?? param(url, "sort") ?? "-startsAt";
46
- const parts = ["status==active"];
47
46
  const q = param(url, "q");
47
+ const raw = param(url, "filters");
48
+ const safe = raw ? validateSieveFilters(raw, SAFE_EVENT_FILTER_FIELDS) : "";
49
+ // Only default to active events when no explicit status filter is provided.
50
+ const hasStatusFilter = safe.includes("status==");
51
+ const parts = hasStatusFilter ? [] : ["status==active"];
48
52
  if (q)
49
53
  parts.push(`title@=*${q}`);
50
- const raw = param(url, "filters");
51
- if (raw) {
52
- const safe = validateSieveFilters(raw, SAFE_EVENT_FILTER_FIELDS);
53
- if (safe)
54
- parts.push(safe);
55
- }
54
+ if (safe)
55
+ parts.push(safe);
56
56
  const filters = parts.join(",");
57
57
  const { db } = getProviders();
58
58
  if (!db) {