@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.
- package/dist/_internal/server/features/categories/data.js +2 -2
- package/dist/_internal/shared/actions/action-registry.js +8 -0
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/features/admin/components/AdminBidsView.js +6 -6
- package/dist/features/admin/components/AdminBrandsView.js +2 -2
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminReviewsView.js +5 -5
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +1 -1
- package/dist/features/auth/repository/session.repository.d.ts +2 -0
- package/dist/features/auth/repository/session.repository.js +4 -1
- package/dist/features/categories/api/route.js +2 -2
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/events/repository/event-entry.repository.js +1 -0
- package/dist/features/events/schemas/firestore.d.ts +3 -1
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/layout/BottomNavbar.js +1 -1
- package/dist/features/layout/NavItem.js +1 -1
- package/dist/features/layout/NavbarLayout.js +2 -2
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +4 -2
- package/dist/features/products/repository/products.repository.d.ts +20 -0
- package/dist/features/products/repository/products.repository.js +5 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +16 -3
- package/dist/features/search/hooks/useNavSuggestions.js +20 -1
- package/dist/features/seller/components/SellerCouponsView.js +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +1 -1
- package/dist/features/stores/repository/store.repository.js +3 -0
- package/dist/features/tester/components/AdminTesterFeedbackView.js +22 -2
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +8 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +98 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +1 -1
- package/dist/seed/bids-seed-data.js +63 -184
- package/dist/styles.css +19 -19
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +9 -9
- package/dist/tokens/themes/default-light.js +9 -9
- package/dist/tokens/tokens.css +18 -18
- package/dist/ui/components/ListingToolbar.js +1 -1
- 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,
|
|
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: "
|
|
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("
|
|
44
|
+
defaultSort: sortBy("bidDate", "DESC"),
|
|
45
45
|
queryKey: ["admin", "bids", "listing"],
|
|
46
46
|
endpoint: ADMIN_ENDPOINTS.BIDS,
|
|
47
47
|
sortOptions: [
|
|
48
|
-
{ value: sortBy("
|
|
49
|
-
{ value: "
|
|
50
|
-
{ value: sortBy("
|
|
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.
|
|
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("
|
|
39
|
+
defaultSort: sortBy("order", "ASC"),
|
|
40
40
|
queryKey: ["admin", "brands", "listing"],
|
|
41
41
|
endpoint: ADMIN_ENDPOINTS.BRANDS,
|
|
42
42
|
sortOptions: [
|
|
43
|
-
{ value: sortBy("
|
|
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: "
|
|
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.
|
|
87
|
-
? toRelativeDate(item.
|
|
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.
|
|
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
|
|
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("
|
|
81
|
+
defaultSort: sortBy("createdAt", "DESC"),
|
|
82
82
|
queryKey: ["admin", "reviews", "listing"],
|
|
83
83
|
endpoint: ADMIN_ENDPOINTS.REVIEWS,
|
|
84
84
|
sortOptions: [
|
|
85
|
-
{ value: sortBy("
|
|
86
|
-
{ value: "
|
|
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
|
-
"
|
|
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(`
|
|
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";
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 = [
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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;
|