@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.
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +1 -0
- package/dist/core/unit-of-work.js +3 -4
- package/dist/features/admin/components/DashboardStats.js +1 -1
- package/dist/features/auth/actions/profile-actions.js +4 -1
- package/dist/features/auth/repository/user.repository.d.ts +1 -0
- package/dist/features/auth/repository/user.repository.js +3 -0
- package/dist/features/blog/api/route.js +3 -0
- package/dist/features/blog/components/BlogIndexListing.js +2 -0
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/categories/api/route.js +1 -1
- package/dist/features/categories/components/BundleDetailView.js +5 -3
- package/dist/features/categories/components/CategoryBundlesListing.js +77 -41
- package/dist/features/categories/components/CategoryDetailPageView.js +12 -9
- package/dist/features/categories/components/CategoryProductsListing.js +3 -5
- package/dist/features/classified/components/ClassifiedIndexListing.js +3 -4
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +3 -4
- package/dist/features/events/actions/event-actions.d.ts +2 -3
- package/dist/features/events/actions/event-actions.js +3 -5
- package/dist/features/events/api/route.js +7 -7
- package/dist/features/events/components/AdminEventEditorView.js +80 -6
- package/dist/features/events/components/AdminEventEntriesView.js +33 -4
- package/dist/features/events/components/EventFilters.d.ts +3 -0
- package/dist/features/events/components/EventFilters.js +1 -0
- package/dist/features/events/components/EventLeaderboard.js +1 -1
- package/dist/features/events/components/EventsIndexListing.js +10 -7
- package/dist/features/events/repository/event-entry.repository.d.ts +2 -2
- package/dist/features/events/repository/event-entry.repository.js +32 -7
- package/dist/features/events/types/index.d.ts +3 -1
- package/dist/features/filters/SwitchFilter.js +2 -4
- package/dist/features/homepage/components/BrandsSection.js +2 -2
- package/dist/features/homepage/components/CustomCardsSection.js +1 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/live/components/LiveItemsIndexListing.js +3 -4
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +3 -4
- package/dist/features/products/components/AuctionsIndexListing.js +3 -4
- package/dist/features/products/components/PrizeDrawsIndexListing.js +3 -8
- package/dist/features/products/components/ProductDetailPageView.js +17 -2
- package/dist/features/products/components/ProductsIndexListing.js +10 -6
- package/dist/features/products/schemas/firestore.d.ts +2 -0
- package/dist/features/reviews/actions/review-actions.js +2 -0
- package/dist/features/reviews/components/ReviewDetailShell.js +3 -2
- package/dist/features/reviews/components/ReviewFilters.js +1 -1
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -2
- package/dist/features/reviews/hooks/useReviews.js +2 -0
- package/dist/features/reviews/schemas/firestore.d.ts +4 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/components/ScamRegistryView.js +41 -8
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +3 -4
- package/dist/features/stores/components/StoreDetailLayoutView.js +13 -5
- package/dist/features/stores/components/StorePreOrdersListing.js +3 -4
- package/dist/features/stores/components/StoreProductsListing.js +3 -4
- package/dist/index.d.ts +1 -0
- package/dist/providers/db-firebase/helpers.d.ts +4 -6
- package/dist/providers/db-firebase/helpers.js +18 -5
- package/dist/providers/db-firebase/sieve.js +26 -1
- package/dist/react/hooks/usePendingFilters.d.ts +2 -0
- package/dist/react/hooks/usePendingFilters.js +9 -0
- package/dist/react/hooks/usePendingTable.d.ts +5 -0
- package/dist/react/hooks/usePendingTable.js +1 -0
- package/dist/seed/homepage-sections-seed-data.js +2 -2
- package/dist/seed/products-standard-seed-data.js +1 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Drawer.style.css +13 -0
- package/dist/ui/components/FilterDrawer.d.ts +4 -2
- package/dist/ui/components/FilterDrawer.js +2 -2
- 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
|
-
|
|
107
|
-
|
|
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
|
}
|
|
@@ -9,7 +9,7 @@ const BRAND_FROM = "var(--appkit-color-primary-700, #1343de)";
|
|
|
9
9
|
const BRAND_MID = "var(--appkit-color-cobalt, #3570fc)";
|
|
10
10
|
const BRAND_TO = "var(--appkit-color-secondary-400, #84e122)";
|
|
11
11
|
function StatCard({ label, value, sub, icon: Icon, gradient, iconColor }) {
|
|
12
|
-
return (_jsxs("div", { className: "relative rounded-xl border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] overflow-hidden shadow-sm hover:shadow-md transition-shadow", children: [_jsx("div", { className: "absolute top-0 left-0 right-0 h-[3px]", style: { background: gradient }, "aria-hidden": "true" }), _jsxs("div", { className: "px-5 pb-5 pt-6 flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Text, { className: "text-[11px] font-semibold uppercase tracking-widest text-[var(--appkit-color-text-muted)]", children: label }), _jsx(Text, { className: "mt-2 text-
|
|
12
|
+
return (_jsxs("div", { className: "relative rounded-xl border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] overflow-hidden shadow-sm hover:shadow-md transition-shadow", children: [_jsx("div", { className: "absolute top-0 left-0 right-0 h-[3px]", style: { background: gradient }, "aria-hidden": "true" }), _jsxs("div", { className: "px-5 pb-5 pt-6 flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1 overflow-hidden", children: [_jsx(Text, { className: "text-[11px] font-semibold uppercase tracking-widest text-[var(--appkit-color-text-muted)] truncate", children: label }), _jsx(Text, { className: "mt-2 text-xl font-bold text-[var(--appkit-color-text)] tabular-nums leading-tight break-words", children: value }), sub && (_jsx(Text, { className: "mt-1.5 text-xs text-[var(--appkit-color-text-muted)]", children: sub }))] }), _jsx("div", { className: "flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center", style: { background: gradient, opacity: 0.92 }, children: _jsx(Icon, { className: "w-5 h-5 text-white drop-shadow-sm" }) })] })] }));
|
|
13
13
|
}
|
|
14
14
|
function SkeletonCard() {
|
|
15
15
|
return (_jsxs("div", { className: "relative rounded-xl border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] overflow-hidden p-5 animate-pulse", children: [_jsx("div", { className: "absolute top-0 left-0 right-0 h-[3px] bg-[var(--appkit-color-border)]" }), _jsxs("div", { className: "flex items-start justify-between gap-3 pt-1", children: [_jsxs("div", { className: "flex-1 space-y-2", children: [_jsx("div", { className: "h-2.5 w-20 rounded bg-[var(--appkit-color-border)]" }), _jsx("div", { className: "h-7 w-24 rounded bg-[var(--appkit-color-border)]" })] }), _jsx("div", { className: "w-10 h-10 rounded-lg bg-[var(--appkit-color-border)]" })] })] }));
|
|
@@ -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
|
-
|
|
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(
|
|
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("
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{
|
|
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
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
15
|
+
catFilter
|
|
13
16
|
? productRepository
|
|
14
17
|
.list({
|
|
15
|
-
filters: `status==published
|
|
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
|
-
|
|
25
|
+
catFilter
|
|
23
26
|
? productRepository
|
|
24
27
|
.list({
|
|
25
|
-
filters: `status==published
|
|
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
|
-
|
|
35
|
+
catFilter
|
|
33
36
|
? productRepository
|
|
34
37
|
.list({
|
|
35
|
-
filters: `status==published
|
|
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
|
-
|
|
45
|
+
catFilter
|
|
43
46
|
? productRepository
|
|
44
47
|
.list({
|
|
45
|
-
filters: `status==published
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
onFilterClear();
|
|
65
|
+
onResetAll({ q: "", sort: "" });
|
|
68
66
|
setSearchInput("");
|
|
69
|
-
}, [
|
|
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
|
-
|
|
58
|
-
onFilterClear();
|
|
57
|
+
onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
|
|
59
58
|
setSearchInput("");
|
|
60
|
-
}, [
|
|
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
|
-
|
|
55
|
-
onFilterClear();
|
|
54
|
+
onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "" });
|
|
56
55
|
setSearchInput("");
|
|
57
|
-
}, [
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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) {
|