@mohasinac/appkit 4.9.4 → 4.10.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/client/features/layout/DashboardLayoutClient.js +7 -2
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +5 -1
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +8 -2
- package/dist/_internal/client/features/layout/sidebarPositionClasses.d.ts +20 -0
- package/dist/_internal/client/features/layout/sidebarPositionClasses.js +20 -0
- package/dist/_internal/client/hand-mode/HandModeProvider.d.ts +21 -0
- package/dist/_internal/client/hand-mode/HandModeProvider.js +81 -0
- package/dist/_internal/client/hand-mode/index.d.ts +2 -0
- package/dist/_internal/client/hand-mode/index.js +1 -0
- package/dist/_internal/server/features/auctions/actions.js +3 -2
- package/dist/_internal/server/features/auctions/service.d.ts +4 -3
- package/dist/_internal/server/features/auctions/service.js +6 -6
- package/dist/_internal/server/features/bundles/data.d.ts +9 -0
- package/dist/_internal/server/features/bundles/data.js +14 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
- package/dist/_internal/server/features/catalogue/og.js +27 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +43 -0
- package/dist/_internal/server/features/checkout/actions.js +172 -81
- package/dist/_internal/server/features/checkout/index.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.js +1 -1
- package/dist/_internal/server/features/grouped/data.d.ts +12 -0
- package/dist/_internal/server/features/grouped/data.js +26 -1
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/products/data.d.ts +23 -0
- package/dist/_internal/server/features/products/data.js +159 -0
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/seo/robots.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +70 -5
- package/dist/_internal/shared/config/schema.d.ts +1 -0
- package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
- package/dist/_internal/shared/features/auctions/config.js +31 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/art/config.js +1 -1
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/classified/config.js +2 -2
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.js +2 -2
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/live/config.js +2 -2
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
- package/dist/client.d.ts +4 -6
- package/dist/client.js +10 -4
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/about/components/AboutView.js +2 -2
- package/dist/features/about/components/DeveloperView.js +2 -2
- package/dist/features/about/schemas/firestore.d.ts +1 -0
- package/dist/features/account/components/UserSidebar.js +8 -2
- package/dist/features/account/hooks/useProfile.d.ts +2 -0
- package/dist/features/admin/components/AdminSidebar.js +8 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +12 -4
- package/dist/features/admin/schemas/firestore.d.ts +9 -0
- package/dist/features/admin/schemas/firestore.js +6 -0
- package/dist/features/auctions/actions/bid-actions.js +4 -1
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
- package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
- package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
- package/dist/features/auth/schemas/firestore.d.ts +3 -0
- package/dist/features/cart/components/CartDrawer.js +4 -1
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
- package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
- package/dist/features/catalogue/index.d.ts +2 -0
- package/dist/features/catalogue/index.js +1 -0
- package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
- package/dist/features/categories/components/BundleDetailView.js +3 -2
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
- package/dist/features/contact/components/ContactPageView.js +8 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +8 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +16 -0
- package/dist/features/homepage/components/BrandsSection.js +3 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +12 -1
- package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
- package/dist/features/homepage/components/HeroCarousel.js +3 -1
- package/dist/features/homepage/components/SectionCarousel.js +5 -2
- package/dist/features/homepage/components/SocialFeedSection.js +2 -2
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +3 -1
- package/dist/features/layout/BackToTop.js +3 -1
- package/dist/features/layout/BottomActions.js +4 -3
- package/dist/features/layout/BottomActionsContext.d.ts +10 -0
- package/dist/features/layout/BottomActionsContext.js +5 -1
- package/dist/features/layout/SidebarLayout.js +7 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
- package/dist/features/media/MediaVideo.js +17 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
- package/dist/features/products/components/ProductDetailPageView.js +11 -116
- package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
- package/dist/features/products/components/RelatedItemsSection.js +17 -0
- package/dist/features/seller/components/SellerProductShell.js +1 -1
- package/dist/features/seller/components/SellerSidebar.js +8 -2
- package/dist/features/shell/QuickFormDrawer.js +3 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +114 -3
- package/dist/hand-mode/index.d.ts +12 -0
- package/dist/hand-mode/index.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +7 -0
- package/dist/react/contexts/SessionContext.d.ts +5 -0
- package/dist/react/contexts/SessionContext.js +2 -0
- package/dist/seed/bids-seed-data.js +48 -3
- package/dist/seed/grouped-listings-seed-data.js +167 -0
- package/dist/seed/payouts-seed-data.js +4 -4
- package/dist/seed/products-art-seed-data.js +188 -5
- package/dist/seed/products-auctions-seed-data.js +215 -6
- package/dist/seed/products-classifieds-seed-data.js +255 -8
- package/dist/seed/products-digital-codes-seed-data.js +258 -19
- package/dist/seed/products-live-items-seed-data.js +143 -10
- package/dist/seed/products-preorders-seed-data.js +213 -8
- package/dist/seed/products-prize-draws-seed-data.js +198 -0
- package/dist/seed/products-standard-seed-data.js +141 -0
- package/dist/seed/products-stickers-seed-data.js +178 -5
- package/dist/seed/site-settings-seed-data.js +13 -31
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +5 -2
- package/dist/server-entry.js +5 -2
- package/dist/server.d.ts +4 -2
- package/dist/server.js +4 -2
- package/dist/styles.css +30 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Drawer.js +7 -4
- package/dist/ui/components/FilterDrawer.js +1 -1
- package/dist/ui/components/HandModeRow.d.ts +12 -0
- package/dist/ui/components/HandModeRow.js +18 -0
- package/dist/ui/components/ImageLightbox.js +37 -3
- package/dist/ui/components/ImageLightbox.style.css +28 -0
- package/dist/ui/components/Layout.d.ts +5 -1
- package/dist/ui/components/Layout.js +2 -1
- package/dist/ui/components/Layout.style.css +1 -0
- package/dist/ui/components/ListingFilterDrawer.js +3 -1
- package/dist/ui/components/Modal.js +3 -1
- package/dist/ui/components/SideDrawer.js +8 -5
- package/dist/ui/components/SideModal.js +7 -4
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/utils/media-url.d.ts +9 -0
- package/dist/utils/media-url.js +39 -0
- package/package.json +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link";
|
|
3
|
-
import { SIEVE_OP, sieveAnd, sieveFilter, sortBy } from "@mohasinac/appkit";
|
|
4
3
|
import { productRepository, reviewRepository } from "../../../repositories";
|
|
5
4
|
const __P = {
|
|
6
5
|
p3: "p-[var(--appkit-space-3)]",
|
|
@@ -32,59 +31,18 @@ import { ProductGalleryClient } from "./ProductGalleryClient";
|
|
|
32
31
|
import { ProductTabsShell } from "./ProductTabsShell";
|
|
33
32
|
import { ProductFeatureBadges } from "./ProductFeatureBadges";
|
|
34
33
|
import { FeatureBadgeList } from "./FeatureBadge";
|
|
35
|
-
import { RelatedProductsCarousel } from "./RelatedProductsCarousel";
|
|
36
34
|
import { ShareButton } from "./ShareButton";
|
|
37
35
|
import { CustomSectionTabContent } from "./CustomSectionTabContent";
|
|
38
36
|
import { SublistingCarouselSection } from "./SublistingCarouselSection";
|
|
39
37
|
import { ShowGroupSection } from "./ShowGroupSection";
|
|
40
38
|
import { HistoryTracker } from "../../history/components/HistoryTracker";
|
|
39
|
+
import { RelatedItemsSection } from "./RelatedItemsSection";
|
|
40
|
+
import { computeRelatedItems, toReview } from "../../../_internal/server/features/products/data";
|
|
41
|
+
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
|
|
42
|
+
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
41
43
|
// ---------------------------------------------------------------------------
|
|
42
44
|
// Helpers
|
|
43
45
|
// ---------------------------------------------------------------------------
|
|
44
|
-
function toProductItem(doc) {
|
|
45
|
-
return {
|
|
46
|
-
id: String(doc.id ?? ""),
|
|
47
|
-
title: String(doc.title ?? doc.name ?? ""),
|
|
48
|
-
price: typeof doc.price === "number" ? doc.price : 0,
|
|
49
|
-
originalPrice: typeof doc.originalPrice === "number" ? doc.originalPrice : undefined,
|
|
50
|
-
mainImage: Array.isArray(doc.images)
|
|
51
|
-
? doc.images[0]
|
|
52
|
-
: typeof doc.mainImage === "string"
|
|
53
|
-
? doc.mainImage
|
|
54
|
-
: undefined,
|
|
55
|
-
status: doc.status ?? "published",
|
|
56
|
-
slug: typeof doc.slug === "string" ? doc.slug : undefined,
|
|
57
|
-
storeName: typeof doc.storeName === "string" ? doc.storeName : undefined,
|
|
58
|
-
rating: typeof doc.rating === "number" ? doc.rating : undefined,
|
|
59
|
-
reviewCount: typeof doc.reviewCount === "number" ? doc.reviewCount : undefined,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
function toReview(doc) {
|
|
63
|
-
const images = Array.isArray(doc.images) ? doc.images : undefined;
|
|
64
|
-
const createdAt = doc.createdAt instanceof Date
|
|
65
|
-
? doc.createdAt.toISOString()
|
|
66
|
-
: typeof doc.createdAt === "string"
|
|
67
|
-
? doc.createdAt
|
|
68
|
-
: undefined;
|
|
69
|
-
const rawRating = typeof doc.rating === "number" ? doc.rating : 3;
|
|
70
|
-
const rating = Math.min(5, Math.max(1, Math.round(rawRating)));
|
|
71
|
-
return {
|
|
72
|
-
id: String(doc.id ?? ""),
|
|
73
|
-
productId: String(doc.productId ?? ""),
|
|
74
|
-
userId: String(doc.userId ?? ""),
|
|
75
|
-
userName: String(doc.userName ?? "Anonymous"),
|
|
76
|
-
userAvatar: typeof doc.userAvatar === "string" ? doc.userAvatar : undefined,
|
|
77
|
-
rating,
|
|
78
|
-
title: typeof doc.title === "string" ? doc.title : undefined,
|
|
79
|
-
comment: typeof doc.comment === "string" ? doc.comment : undefined,
|
|
80
|
-
images,
|
|
81
|
-
status: doc.status ?? "approved",
|
|
82
|
-
helpfulCount: typeof doc.helpfulCount === "number" ? doc.helpfulCount : undefined,
|
|
83
|
-
verified: doc.verified === true,
|
|
84
|
-
featured: doc.featured === true,
|
|
85
|
-
createdAt,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
46
|
function toDescriptionHtml(raw) {
|
|
89
47
|
if (!raw)
|
|
90
48
|
return "";
|
|
@@ -206,74 +164,14 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
206
164
|
const isGroupParent = p.isGroupParent === true;
|
|
207
165
|
const groupTitle = typeof p.groupTitle === "string" ? p.groupTitle : undefined;
|
|
208
166
|
const storeId = typeof p.storeId === "string" ? p.storeId : null;
|
|
209
|
-
// -- Fetch reviews + 4 related-
|
|
210
|
-
const [reviewDocs,
|
|
211
|
-
reviewRepository
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
primaryCategorySlug
|
|
215
|
-
? productRepository.findByCategory(primaryCategorySlug).catch(() => [])
|
|
216
|
-
: Promise.resolve([]),
|
|
217
|
-
brand
|
|
218
|
-
? productRepository
|
|
219
|
-
.list({
|
|
220
|
-
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("brand", SIEVE_OP.EQ, brand)),
|
|
221
|
-
sorts: sortBy("createdAt", "DESC"),
|
|
222
|
-
page: 1,
|
|
223
|
-
pageSize: 9,
|
|
224
|
-
})
|
|
225
|
-
.then((r) => r.items)
|
|
226
|
-
.catch(() => [])
|
|
227
|
-
: Promise.resolve([]),
|
|
228
|
-
tags.length > 0
|
|
229
|
-
? productRepository.findByTagsOverlap(tags, 9).catch(() => [])
|
|
230
|
-
: Promise.resolve([]),
|
|
231
|
-
storeId
|
|
232
|
-
? productRepository
|
|
233
|
-
.list({
|
|
234
|
-
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("storeId", SIEVE_OP.EQ, storeId)),
|
|
235
|
-
sorts: sortBy("createdAt", "DESC"),
|
|
236
|
-
page: 1,
|
|
237
|
-
pageSize: 9,
|
|
238
|
-
})
|
|
239
|
-
.then((r) => r.items)
|
|
240
|
-
.catch(() => [])
|
|
241
|
-
: Promise.resolve([]),
|
|
167
|
+
// -- Fetch reviews + the shared 4-signal related-items computation + grouped listings in parallel --
|
|
168
|
+
const [reviewDocs, related, groups] = await Promise.all([
|
|
169
|
+
reviewRepository.findApprovedByProduct(product.id).catch(() => []),
|
|
170
|
+
computeRelatedItems(product),
|
|
171
|
+
getGroupsWithItemsForProduct(product.id),
|
|
242
172
|
]);
|
|
243
173
|
const reviews = reviewDocs.map(toReview);
|
|
244
|
-
const
|
|
245
|
-
const currentProductId = product.id;
|
|
246
|
-
function toRelatedItems(docs) {
|
|
247
|
-
return docs
|
|
248
|
-
.filter((r) => {
|
|
249
|
-
if (r.id === currentProductId)
|
|
250
|
-
return false;
|
|
251
|
-
const s = r.status;
|
|
252
|
-
if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
|
|
253
|
-
return false;
|
|
254
|
-
if (r.isSold === true)
|
|
255
|
-
return false;
|
|
256
|
-
if (r.availableQuantity === 0)
|
|
257
|
-
return false;
|
|
258
|
-
if (r.listingType === "auction" && r.auctionEndDate) {
|
|
259
|
-
const end = r.auctionEndDate;
|
|
260
|
-
const endDate = typeof end.toDate === "function"
|
|
261
|
-
? end.toDate()
|
|
262
|
-
: end instanceof Date ? end : new Date(String(end));
|
|
263
|
-
if (endDate <= _now)
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
if (r.listingType === "prize-draw" && r.prizeRevealStatus === "closed")
|
|
267
|
-
return false;
|
|
268
|
-
return true;
|
|
269
|
-
})
|
|
270
|
-
.slice(0, 8)
|
|
271
|
-
.map(toProductItem);
|
|
272
|
-
}
|
|
273
|
-
const relatedItems = toRelatedItems(sameCategoryDocs);
|
|
274
|
-
const relatedByBrand = toRelatedItems(sameBrandDocs);
|
|
275
|
-
const relatedByTags = toRelatedItems(tagOverlapDocs);
|
|
276
|
-
const relatedByStore = toRelatedItems(sameStoreDocs);
|
|
174
|
+
const { relatedItems, relatedByBrand, relatedByTags, relatedByStore } = related;
|
|
277
175
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: product.id, productType: "product", snapshot: {
|
|
278
176
|
title: product.title,
|
|
279
177
|
thumb: product.mainImage ?? product.images?.[0],
|
|
@@ -339,10 +237,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
339
237
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
340
238
|
})) })), renderBundleSection: bundleMemberships.length > 0
|
|
341
239
|
? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
|
|
342
|
-
: undefined, renderRelated: () => relatedItems
|
|
343
|
-
relatedByBrand.length > 0 ||
|
|
344
|
-
relatedByTags.length > 0 ||
|
|
345
|
-
relatedByStore.length > 0 ? (_jsxs(Stack, { gap: "xl", children: [relatedItems.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedItems, title: categoryName || category ? `More in ${categoryName || category}` : "More in this category" })), relatedByBrand.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByBrand, title: brand ? `More by ${brand}` : "More by this brand" })), relatedByTags.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByTags, title: "You might also like" })), relatedByStore.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByStore, title: storeName ? `More from ${storeName}` : "More from this seller" }))] })) : null }), renderPrimaryActions
|
|
240
|
+
: undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
|
|
346
241
|
? renderPrimaryActions({
|
|
347
242
|
productId: product.id,
|
|
348
243
|
productSlug: product.slug ?? slug,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ProductItem } from "../types";
|
|
2
|
+
export interface RelatedItemsSectionProps {
|
|
3
|
+
relatedItems: ProductItem[];
|
|
4
|
+
relatedByBrand: ProductItem[];
|
|
5
|
+
relatedByTags: ProductItem[];
|
|
6
|
+
relatedByStore: ProductItem[];
|
|
7
|
+
categoryLabel?: string;
|
|
8
|
+
brandLabel?: string;
|
|
9
|
+
storeLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The same-shape "More in category / More by brand / You might also like /
|
|
13
|
+
* More from store" carousel stack, shared by every listing-type detail page
|
|
14
|
+
* that surfaces computeRelatedItems() results.
|
|
15
|
+
*/
|
|
16
|
+
export declare function RelatedItemsSection({ relatedItems, relatedByBrand, relatedByTags, relatedByStore, categoryLabel, brandLabel, storeLabel, }: RelatedItemsSectionProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Stack } from "../../../ui";
|
|
3
|
+
import { RelatedProductsCarousel } from "./RelatedProductsCarousel";
|
|
4
|
+
/**
|
|
5
|
+
* The same-shape "More in category / More by brand / You might also like /
|
|
6
|
+
* More from store" carousel stack, shared by every listing-type detail page
|
|
7
|
+
* that surfaces computeRelatedItems() results.
|
|
8
|
+
*/
|
|
9
|
+
export function RelatedItemsSection({ relatedItems, relatedByBrand, relatedByTags, relatedByStore, categoryLabel, brandLabel, storeLabel, }) {
|
|
10
|
+
if (relatedItems.length === 0 &&
|
|
11
|
+
relatedByBrand.length === 0 &&
|
|
12
|
+
relatedByTags.length === 0 &&
|
|
13
|
+
relatedByStore.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return (_jsxs(Stack, { gap: "xl", children: [relatedItems.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedItems, title: categoryLabel ? `More in ${categoryLabel}` : "More in this category" })), relatedByBrand.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByBrand, title: brandLabel ? `More by ${brandLabel}` : "More by this brand" })), relatedByTags.length > 0 && _jsx(RelatedProductsCarousel, { items: relatedByTags, title: "You might also like" }), relatedByStore.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByStore, title: storeLabel ? `More from ${storeLabel}` : "More from this seller" }))] }));
|
|
17
|
+
}
|
|
@@ -87,7 +87,7 @@ function StepMedia({ values, onChange, storeSlug = "store", }) {
|
|
|
87
87
|
}
|
|
88
88
|
// ── Step 3: Auction Settings ───────────────────────────────────────────────
|
|
89
89
|
function StepAuctionSettings({ values, onChange, }) {
|
|
90
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "Prices are in Indian Rupees (\u20B9). Enter the rupee value \u2014 no paise." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: "Starting Bid (\u20B9)", type: "number", value: toRupeesString(values.startingBid), onChange: (v) => onChange({ startingBid: fromRupeesString(v) }), placeholder: "100" }), _jsx(FormField, { name: "auctionEndDate", label: "Auction End Date & Time", type: "datetime-local", value: values.auctionEndDate ?? "", onChange: (v) => onChange({ auctionEndDate: v }) })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "reservePrice", label: "Reserve Price (\u20B9, optional)", type: "number", value: toRupeesString(values.reservePrice), onChange: (v) => onChange({ reservePrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Minimum price below which you won't sell" }), _jsx(FormField, { name: "buyNowPrice", label: "Buy Now Price (\u20B9, optional)", type: "number", value: toRupeesString(values.buyNowPrice), onChange: (v) => onChange({ buyNowPrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Allows instant purchase before auction ends" })] }), _jsx(FormField, { name: "minBidIncrement", label: "Minimum Bid Increment (\u20B9)", type: "number", value: toRupeesString(values.minBidIncrement), onChange: (v) => onChange({ minBidIncrement: fromRupeesString(v) }), placeholder: "50" }), _jsx(FormField, { name: "auctionShippingPaidBy", label: "Shipping paid by", type: "select", value: values.auctionShippingPaidBy ?? "winner", onChange: (v) => onChange({ auctionShippingPaidBy: v }), options: [
|
|
90
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "Prices are in Indian Rupees (\u20B9). Enter the rupee value \u2014 no paise." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: "Starting Bid (\u20B9)", type: "number", value: toRupeesString(values.startingBid), onChange: (v) => onChange({ startingBid: fromRupeesString(v) }), placeholder: "100" }), _jsx(FormField, { name: "auctionEndDate", label: "Auction End Date & Time", type: "datetime-local", value: values.auctionEndDate ?? "", onChange: (v) => onChange({ auctionEndDate: v }) })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "reservePrice", label: "Reserve Price (\u20B9, optional)", type: "number", value: toRupeesString(values.reservePrice), onChange: (v) => onChange({ reservePrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Minimum price below which you won't sell" }), _jsx(FormField, { name: "buyNowPrice", label: "Buy Now Price (\u20B9, optional)", type: "number", value: toRupeesString(values.buyNowPrice), onChange: (v) => onChange({ buyNowPrice: v ? fromRupeesString(v) : undefined }), placeholder: "0", helpText: "Allows instant purchase before auction ends" })] }), _jsx(FormField, { name: "minBidIncrement", label: "Minimum Bid Increment (\u20B9)", type: "number", value: toRupeesString(values.minBidIncrement), onChange: (v) => onChange({ minBidIncrement: fromRupeesString(v) }), placeholder: "50", helpText: "Optional \u2014 can only raise the platform's tiered minimum for this listing, never lower it. Leave blank to use the tier as-is." }), _jsx(FormField, { name: "auctionShippingPaidBy", label: "Shipping paid by", type: "select", value: values.auctionShippingPaidBy ?? "winner", onChange: (v) => onChange({ auctionShippingPaidBy: v }), options: [
|
|
91
91
|
{ value: "winner", label: "Winner pays shipping" },
|
|
92
92
|
{ value: "seller", label: "Seller includes shipping" },
|
|
93
93
|
] }), _jsx(Toggle, { checked: !!values.autoExtendable, onChange: (checked) => onChange({ autoExtendable: checked }), label: "Auto-extend auction if bid placed in final minutes" }), values.autoExtendable && (_jsx(FormField, { name: "auctionExtensionMinutes", label: "Extension period (minutes)", type: "number", value: String(values.auctionExtensionMinutes ?? 5), onChange: (v) => onChange({ auctionExtensionMinutes: Number(v) || 5 }), placeholder: "5" }))] }));
|
|
@@ -9,6 +9,8 @@ import { BottomSheet } from "../../layout/BottomSheet";
|
|
|
9
9
|
import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
|
|
10
10
|
import { useSidebarSearch } from "../../../_internal/client/features/layout/useSidebarSearch";
|
|
11
11
|
import { findActiveNavGroup, findActiveNavItem } from "../../../_internal/client/features/layout/navActive";
|
|
12
|
+
import { getSidebarRailClasses, getSidebarOverlayClasses } from "../../../_internal/client/features/layout/sidebarPositionClasses";
|
|
13
|
+
import { useHandMode } from "../../../_internal/client/hand-mode";
|
|
12
14
|
const __O = {
|
|
13
15
|
hidden: "overflow-hidden",
|
|
14
16
|
yAuto: "overflow-y-auto",
|
|
@@ -64,10 +66,14 @@ function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClic
|
|
|
64
66
|
})] })] }));
|
|
65
67
|
}
|
|
66
68
|
function DrawerPanel({ title, onClose, children }) {
|
|
67
|
-
|
|
69
|
+
const { hand } = useHandMode();
|
|
70
|
+
const { edgeClass, borderClass } = getSidebarOverlayClasses(hand);
|
|
71
|
+
return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: `fixed top-0 ${edgeClass} z-50 h-full w-64 ${borderClass}`, surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
|
|
68
72
|
}
|
|
69
73
|
export function StoreSidebar({ items, groups, activeHref, storeName, storeLogoURL, mobileOpen = false, desktopOpen = false, variant = "overlay", onCloseMobile, onToggle, renderFooter, }) {
|
|
70
74
|
const close = onCloseMobile ?? (() => { });
|
|
75
|
+
const { hand } = useHandMode();
|
|
76
|
+
const rail = getSidebarRailClasses(hand);
|
|
71
77
|
const [mounted, setMounted] = useState(false);
|
|
72
78
|
useEffect(() => { setMounted(true); }, []);
|
|
73
79
|
useEffect(() => {
|
|
@@ -78,7 +84,7 @@ export function StoreSidebar({ items, groups, activeHref, storeName, storeLogoUR
|
|
|
78
84
|
const navContent = groups ? (_jsx(GroupsContent, { groups: groups, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) : (_jsx(FlatContent, { items: items, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close }));
|
|
79
85
|
if (variant === "sidebar") {
|
|
80
86
|
const handleToggle = onToggle ?? close;
|
|
81
|
-
return (_jsxs(_Fragment, { children: [desktopOpen && (_jsx(Div, { surface: "overlay-xs", className: "hidden lg:block fixed inset-0 backdrop-blur-[2px] z-30", onClick: handleToggle, "aria-hidden": "true" })), _jsxs(Div, { className: `hidden lg:flex fixed
|
|
87
|
+
return (_jsxs(_Fragment, { children: [desktopOpen && (_jsx(Div, { surface: "overlay-xs", className: "hidden lg:block fixed inset-0 backdrop-blur-[2px] z-30", onClick: handleToggle, "aria-hidden": "true" })), _jsxs(Div, { className: `hidden lg:flex fixed ${rail.edgeClass} z-40 transition-transform duration-300 top-[var(--header-height,3.5rem)] h-[calc(100vh-var(--header-height,3.5rem))] w-[18rem] ${desktopOpen ? "translate-x-0" : rail.collapsedTranslateClass}`, children: [_jsxs(Stack, { border: "default", surface: "sidePanel", className: `flex-1 ${rail.borderClass} ${__O.hidden}`, shadow: "xl", children: [_jsx(Div, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", children: _jsxs(Row, { className: "min-w-0", align: "center", gap: "3", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName ?? "", size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName?.[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName || panelTitle })] }) }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: navContent }), renderFooter && _jsx(Div, { border: "top", padding: "inline", children: renderFooter() })] }), _jsx(SidebarCollapseToggle, { expanded: desktopOpen, onToggle: handleToggle, edge: hand })] }), _jsx(Div, { className: "lg:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: panelTitle, children: groups ? (_jsx(GroupsContent, { groups: groups, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) : (_jsx(FlatContent, { items: items, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) }) })] }));
|
|
82
88
|
}
|
|
83
89
|
return (_jsxs(_Fragment, { children: [mounted && mobileOpen &&
|
|
84
90
|
createPortal(_jsx(DrawerPanel, { title: panelTitle, onClose: close, children: navContent }), document.body), _jsx(Div, { className: "lg:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: panelTitle, children: groups ? (_jsx(GroupsContent, { groups: groups, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) : (_jsx(FlatContent, { items: items, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) }) })] }));
|
|
@@ -7,6 +7,7 @@ import { IconButton } from "../../ui/components/IconButton";
|
|
|
7
7
|
import { FormField } from "../../ui/components/FormField";
|
|
8
8
|
import { Toggle } from "../../ui/components/Toggle";
|
|
9
9
|
import { Div, Form, Row, Stack, Text } from "../../ui";
|
|
10
|
+
import { useHandMode } from "../../_internal/client/hand-mode";
|
|
10
11
|
function initValues(fields, defaults = {}) {
|
|
11
12
|
const out = {};
|
|
12
13
|
for (const f of fields) {
|
|
@@ -19,6 +20,7 @@ function initValues(fields, defaults = {}) {
|
|
|
19
20
|
}
|
|
20
21
|
const FOCUSABLE = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
21
22
|
export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues, onSubmit, submitLabel = "Save", cancelLabel = "Cancel", isLoading = false, schema, renderExtra, }) {
|
|
23
|
+
const { hand } = useHandMode();
|
|
22
24
|
const drawerRef = useRef(null);
|
|
23
25
|
const [values, setValues] = useState(() => initValues(fields, defaultValues));
|
|
24
26
|
const [errors, setErrors] = useState({});
|
|
@@ -122,7 +124,7 @@ export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues,
|
|
|
122
124
|
if (!isOpen)
|
|
123
125
|
return null;
|
|
124
126
|
const busy = submitting || isLoading;
|
|
125
|
-
return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 backdrop-blur-[2px] [z-index:calc(var(--appkit-z-modal)+1)]", "aria-hidden": "true", onClick: handleClose }), _jsxs(Div, { ref: drawerRef, role: "dialog", "aria-modal": "true", "aria-label": title, layout: "flex-col", shadow: "2xl", className:
|
|
127
|
+
return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 backdrop-blur-[2px] [z-index:calc(var(--appkit-z-modal)+1)]", "aria-hidden": "true", onClick: handleClose }), _jsxs(Div, { ref: drawerRef, role: "dialog", "aria-modal": "true", "aria-label": title, layout: "flex-col", shadow: "2xl", className: `fixed inset-y-0 ${hand === "left" ? "left-0" : "right-0"} bg-[var(--appkit-color-surface)] w-full lg:w-[40%] [z-index:calc(var(--appkit-z-modal)+2)]`, children: [_jsxs(Row, { className: "flex-shrink-0 border-b border-[var(--appkit-color-border)]", padding: "md", align: "center", gap: "3", reverse: hand === "left", children: [_jsx(Text, { className: "flex-1 text-[var(--appkit-color-text)]", size: "base", weight: "semibold", children: title }), _jsx(IconButton, { type: "button", onClick: handleClose, "aria-label": "Close", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsxs(Form, { className: "flex-1", overflow: "y-auto", paddingX: "x-md", paddingY: "y-md-lg", spacing: "md", onSubmit: handleSubmit, noValidate: true, children: [fields.map((field) => {
|
|
126
128
|
const value = values[field.name];
|
|
127
129
|
if (field.type === "toggle") {
|
|
128
130
|
return (_jsxs(Stack, { gap: "xs", children: [_jsx(Toggle, { checked: !!value, onChange: (checked) => set(field.name, checked), label: field.label }), field.helperText && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: field.helperText }))] }, field.name));
|
|
@@ -55,7 +55,29 @@ const rawTesterChecklistItems = [
|
|
|
55
55
|
cases: [
|
|
56
56
|
{ key: "email-signup", label: "Sign up with email works", href: "/auth/register" },
|
|
57
57
|
{ key: "google-oauth", label: "Sign up / log in with Google works", href: "/auth/login" },
|
|
58
|
-
{
|
|
58
|
+
{
|
|
59
|
+
key: "google-link-existing",
|
|
60
|
+
label: "Signing in with Google using the SAME email as an existing password account logs into that same account automatically — no duplicate account, no explicit linking needed",
|
|
61
|
+
description: "This is the auto-merge path: register with email/password (email A), log out, then \"Sign in with Google\" using a Google account whose email is also A. You should land back in the same account (same orders/wishlist/etc.), and Settings > Account tab should now show Google as Connected.",
|
|
62
|
+
href: "/auth/login",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "google-link-different-email",
|
|
66
|
+
label: "Linking a Google account whose email differs from your original signup email requires the explicit \"Connect Google\" button (My Account dashboard or Settings > Account tab) — it does not happen automatically just by signing in with Google",
|
|
67
|
+
description: "Sign up/log in normally with email A (password). While logged in as A, use the \"Connect Google\" button and complete the popup with a Google account using a DIFFERENT email B. Afterward, Settings > Account tab should show Google as Connected with email B, while your account's primary login email is still A — the account now has two associated emails, not a second account.",
|
|
68
|
+
href: "/user/settings",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "google-link-confirmation-shown",
|
|
72
|
+
label: "After Google is connected, the My Account dashboard shows a \"Google account connected\" confirmation with a Connected badge and your linked email — it does not just silently stop showing the \"Connect Google\" prompt",
|
|
73
|
+
description: "Fixed 2026-08-20 — the /user dashboard only ever handled the unlinked case; once linked, the whole alert block disappeared with no confirmation, unlike Settings > Account tab which already showed a Connected badge. Link a Google account (or use an already-linked test account) and confirm the green confirmation alert appears on /user, not just an absence of the blue prompt.",
|
|
74
|
+
href: "/user",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "google-link-conflict-rejected",
|
|
78
|
+
label: "Trying to link a Google account whose email is already used by a DIFFERENT existing account is rejected with a clear error — it does not silently merge the two accounts or move the email over",
|
|
79
|
+
href: "/user/settings",
|
|
80
|
+
},
|
|
59
81
|
{ key: "google-popup-blocked-fallback", label: "If the Google sign-in popup is blocked, the fallback (RTDB signal + postMessage) still completes sign-in" },
|
|
60
82
|
{ key: "login", label: "Log in with email + password works", href: "/auth/login" },
|
|
61
83
|
{ key: "logout", label: "Log out works and clears the session" },
|
|
@@ -83,6 +105,16 @@ const rawTesterChecklistItems = [
|
|
|
83
105
|
description: "Each of the three links should open your own public-facing profile page (/profile/[your uid]) — not the edit-profile page. If your profile visibility is set to Private, confirm the page still loads for you (the owner) even though other users would get a 404.",
|
|
84
106
|
href: "/user",
|
|
85
107
|
},
|
|
108
|
+
{
|
|
109
|
+
key: "hand-mode-toggle-exists",
|
|
110
|
+
label: "A \"Left-hand mode\" toggle is visible on the Appearance tab of Settings, and also on the /user/profile page, and switching it saves without a page reload",
|
|
111
|
+
href: "/user/settings",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: "hand-mode-persists-reload",
|
|
115
|
+
label: "After turning on Left-hand mode, reloading the page (and logging out/back in) keeps the setting on",
|
|
116
|
+
href: "/user/settings",
|
|
117
|
+
},
|
|
86
118
|
],
|
|
87
119
|
},
|
|
88
120
|
{
|
|
@@ -156,6 +188,11 @@ const rawTesterChecklistItems = [
|
|
|
156
188
|
label: "Applying multiple filters together (price range + brand + category + condition) narrows results correctly, and clearing filters restores the full list — on Products, Auctions, and Pre-Orders",
|
|
157
189
|
href: "/products",
|
|
158
190
|
},
|
|
191
|
+
{
|
|
192
|
+
key: "filter-drawer-flips-hand-mode",
|
|
193
|
+
label: "With Left-hand mode ON, the product/auction filter drawer opens from the left instead of the right",
|
|
194
|
+
href: "/products",
|
|
195
|
+
},
|
|
159
196
|
{
|
|
160
197
|
key: "search-filter-sort-combo",
|
|
161
198
|
label: "Typing a search query, then applying a filter, then changing sort — all three stay applied together and pagination reflects the combined result count (not just the last action applied)",
|
|
@@ -173,7 +210,11 @@ const rawTesterChecklistItems = [
|
|
|
173
210
|
pageLabel: "Product / Auction / Pre-order Detail",
|
|
174
211
|
cases: [
|
|
175
212
|
{ key: "standard-detail", label: "Standard product detail page loads correctly" },
|
|
176
|
-
{
|
|
213
|
+
{
|
|
214
|
+
key: "auction-detail",
|
|
215
|
+
label: "Auction detail page shows current bid + a live, ticking countdown correctly",
|
|
216
|
+
description: "The countdown (\"Ends in 2d 5h 30m\", ticking every second — not a static date) must appear directly above every \"Place a bid\" button: the info panel, the desktop and mobile compact bid-summary cards, inside the \"Place your bid\" modal above the submit button, and in the mobile sticky bottom bar (as a row above the current-bid/bid-count line). It should switch to \"Ended\" once the end time passes.",
|
|
217
|
+
},
|
|
177
218
|
{ key: "preorder-detail", label: "Pre-order detail page shows expected ship date correctly" },
|
|
178
219
|
{
|
|
179
220
|
key: "image-gallery",
|
|
@@ -252,6 +293,24 @@ const rawTesterChecklistItems = [
|
|
|
252
293
|
label: "Choosing manual payment (UPI/Cash) at checkout shows a how-it-works guide (payment steps, refund summary, what happens if an item sells out) and requires a consent checkbox before the \"Pay via UPI / Cash\" button becomes clickable",
|
|
253
294
|
href: "/checkout",
|
|
254
295
|
},
|
|
296
|
+
{
|
|
297
|
+
key: "checkout-cash-payment-no-validation-error",
|
|
298
|
+
label: "Checking the consent box and clicking \"Pay via UPI / Cash\" places the order successfully — no red \"Validation failed\" error appears",
|
|
299
|
+
description: "Fixed 2026-08-20 — the checkout API's schema only accepted paymentMethod \"cod\"/\"online\"/\"upi_manual\"/\"emi\"; the manual-payment button has always sent \"cash\", which was never in that list, so every attempt was rejected with a generic \"Validation failed\" message even with the out-of-stock policy and consent checkbox correctly filled in. Add items to cart, reach the payment step, check the consent box, and click \"Pay via UPI / Cash\" — confirm it succeeds and lands on the order confirmation / payment-proof page.",
|
|
300
|
+
href: "/checkout",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
key: "checkout-order-summary-full-breakdown",
|
|
304
|
+
label: "The checkout Order Summary panel shows Shipping, COD handling fee (when COD is the likely method), the selected add-on fees (WhatsApp updates / gift wrap / shipment protection — only the ones checked), GST (when enabled), and Coupon discount as separate line items with icons before you pay — not just Subtotal and Total",
|
|
305
|
+
description: "Fixed 2026-08-20 — the Order Summary previously showed Total = Subtotal − coupon discount only; shipping and every fee were computed and charged server-side but never shown to the buyer beforehand. Reach the payment step with an address selected, toggle an add-on checkbox, and confirm the matching line appears in the summary a moment later and the Total updates to match — then confirm the same Total appears on the order confirmation page afterward.",
|
|
306
|
+
href: "/checkout",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
key: "checkout-razorpay-charge-includes-shipping",
|
|
310
|
+
label: "When Razorpay online payment is enabled, the amount charged in the Razorpay checkout modal matches the order's recorded Total exactly, including shipping fee",
|
|
311
|
+
description: "Fixed 2026-08-20 — the Razorpay pre-charge amount (computed before opening the payment modal) omitted the seller's shipping fee entirely, while the order created afterward recorded a higher total that included it, so the buyer was silently charged less via the gateway than what got recorded as owed. Only testable when Site Settings → Payments → Razorpay is enabled and the seller has a configured shipping fee. Compare the amount shown in the Razorpay modal against the order's Total on the confirmation page — they should match exactly.",
|
|
312
|
+
href: "/checkout",
|
|
313
|
+
},
|
|
255
314
|
],
|
|
256
315
|
},
|
|
257
316
|
{
|
|
@@ -293,6 +352,20 @@ const rawTesterChecklistItems = [
|
|
|
293
352
|
{ key: "win-auction", label: "Winning an auction creates a payable order correctly" },
|
|
294
353
|
{ key: "bid-history", label: "My Bids page shows accurate bid history, paginated with the most recent bid first", href: "/user/bids" },
|
|
295
354
|
{ key: "bid-history-auction-detail-pagination", label: "An auction detail page's Bid History section shows the most recent bid first and paginates once there are more bids than fit on one page (try the L-Drago auction — 13 seeded bids)" },
|
|
355
|
+
{
|
|
356
|
+
key: "bid-increment-tiered",
|
|
357
|
+
label: "The \"min increment\" shown on an auction matches the admin-configured tier for the current bid amount, not a flat ₹1 — and a bid below that increment is rejected",
|
|
358
|
+
description: "Default tiers: current bid ≤₹100 → ₹10 increment, ≤₹1,000 → ₹100, ≤₹5,000 → ₹200, ≤₹10,000 → ₹500, above that → ₹1,000. On an auction with no per-listing \"Minimum Bid Increment\" set, confirm the displayed \"min increment\" matches the tier for its current bid, and that trying to bid only current+1 (below the tier) is rejected with a minimum-increment error, while current+tier (or more) is accepted.",
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
key: "bid-increment-override-floor-raising",
|
|
362
|
+
label: "A seller's per-listing \"Minimum Bid Increment\" can require MORE than the admin tier, but can never let a bid undercut the tier",
|
|
363
|
+
description: "Set a listing's \"Minimum Bid Increment\" below the current tier's value (e.g. 1) and confirm the effective minimum bid still enforces the tier, not the smaller override. Then set it above the tier (e.g. 500 when the tier is 100) and confirm the higher override is now enforced instead.",
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
key: "bid-increment-live-tier-change",
|
|
367
|
+
label: "The displayed \"min increment\" on an open auction updates live (without a page refresh) if another bidder's bid pushes the current bid across a tier boundary",
|
|
368
|
+
},
|
|
296
369
|
],
|
|
297
370
|
},
|
|
298
371
|
{
|
|
@@ -447,6 +520,7 @@ const rawTesterChecklistItems = [
|
|
|
447
520
|
{ key: "list-preorder", label: "Listing a pre-order works" },
|
|
448
521
|
{ key: "edit-listing", label: "Editing an existing listing works" },
|
|
449
522
|
{ key: "media-upload", label: "Uploading product images/video during listing works" },
|
|
523
|
+
{ key: "seller-quick-add-drawer-flips", label: "With Left-hand mode ON, the seller's quick-add-listing side drawer opens from the left instead of the right" },
|
|
450
524
|
],
|
|
451
525
|
},
|
|
452
526
|
{
|
|
@@ -734,11 +808,32 @@ const rawTesterChecklistItems = [
|
|
|
734
808
|
},
|
|
735
809
|
],
|
|
736
810
|
},
|
|
811
|
+
{
|
|
812
|
+
pageKey: "hand-mode-layout",
|
|
813
|
+
pageLabel: "Left-Hand Mode",
|
|
814
|
+
cases: [
|
|
815
|
+
{ key: "sidebar-flips", label: "With Left-hand mode ON, the admin/store/user dashboard's persistent left navigation sidebar (and its collapse handle) moves to the right side of the screen; with it OFF (default), the sidebar stays on the left", href: "/admin/dashboard" },
|
|
816
|
+
{ key: "drawers-flip", label: "With Left-hand mode ON, side drawers/panels (filters, quick-add forms, cart, edit/create panels) that normally slide in from the right now slide in from the left", href: "/products" },
|
|
817
|
+
{ key: "close-buttons-flip", label: "With Left-hand mode ON, the X close button on drawers/panels/modals sits on the left edge of its header; with it OFF (default), it sits on the right edge" },
|
|
818
|
+
{ key: "back-to-top-cta-flips", label: "With Left-hand mode ON, the floating \"back to top\" button appears in the bottom-left corner instead of bottom-right after scrolling down a long listing page", href: "/products" },
|
|
819
|
+
{ key: "quick-links-unaffected", label: "Turning on Left-hand mode does not reorder or mirror the My Account / Admin dashboard quick-links tile grid — only its surrounding column shifts with the sidebar", href: "/user" },
|
|
820
|
+
{ key: "hero-carousel-arrows-flip", label: "With Left-hand mode ON, the homepage hero carousel's Prev/Next arrow pair appears in the bottom-left corner instead of bottom-right; the dots stay centered either way", href: "/" },
|
|
821
|
+
{ key: "gallery-arrows-unaffected", label: "Left-hand mode does NOT change a product image gallery/lightbox's Prev/Next arrows — Prev stays on the left edge and Next stays on the right edge in both modes" },
|
|
822
|
+
{ key: "homepage-section-buttons-mirror", label: "With Left-hand mode ON, homepage section header rows (e.g. Brands, Featured Bundles, Google Reviews) have their \"View All\" button on the opposite side from default — title and button swap sides", href: "/" },
|
|
823
|
+
{ key: "hand-mode-no-fouc", label: "On a hard page reload with Left-hand mode already ON, panels/sidebars render on the left immediately — there is no visible flash of them briefly appearing on the right before snapping left" },
|
|
824
|
+
],
|
|
825
|
+
},
|
|
737
826
|
{
|
|
738
827
|
pageKey: "footer-theme",
|
|
739
828
|
pageLabel: "Footer & Dark Mode",
|
|
740
829
|
cases: [
|
|
741
830
|
{ key: "footer-dark-mode", label: "Footer background and all link/text colors switch correctly between light and dark mode", href: "/" },
|
|
831
|
+
{
|
|
832
|
+
key: "footer-github-icon",
|
|
833
|
+
label: "The footer's social icon row (brand column, bottom-left) shows a GitHub icon alongside Instagram/Twitter/WhatsApp, and clicking it opens the developer's real GitHub profile in a new tab",
|
|
834
|
+
description: "Added 2026-08-20 — GITHUB was a new socialUrls key with no footer icon before this. Confirm the icon renders (not a broken/missing glyph) and the link target is a real, working github.com profile, not a placeholder or dead link.",
|
|
835
|
+
href: "/",
|
|
836
|
+
},
|
|
742
837
|
],
|
|
743
838
|
},
|
|
744
839
|
{
|
|
@@ -776,7 +871,17 @@ const rawTesterChecklistItems = [
|
|
|
776
871
|
cases: [
|
|
777
872
|
{ key: "homepage-loads", label: "The homepage loads without errors on both desktop and mobile", href: "/" },
|
|
778
873
|
{ key: "about-us-in-main-nav", label: "\"About Us\" appears as an item in the main public top navigation, not just the footer", href: "/about" },
|
|
779
|
-
{
|
|
874
|
+
{
|
|
875
|
+
key: "about-team-real-founder",
|
|
876
|
+
label: "The About page's \"Who's Behind LetItRip\" team section shows the real founder's name (not a placeholder persona) and a working \"GitHub ↗\" link on their card",
|
|
877
|
+
description: "Updated 2026-08-20 — the founder card previously used a fictional placeholder name with no GitHub link. Confirm the founder card's name matches the real developer and the GitHub link opens their actual profile.",
|
|
878
|
+
href: "/about",
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
key: "developer-page-loads",
|
|
882
|
+
label: "The Developer page loads correctly, is linked from the footer's Support column next to About Us, shows the real developer's name, and its \"GitHub ↗\" link opens their actual GitHub profile",
|
|
883
|
+
href: "/developer",
|
|
884
|
+
},
|
|
780
885
|
{ key: "products-listing-page", label: "The products listing page loads and paginates correctly", href: "/products" },
|
|
781
886
|
{ key: "auctions-listing-page", label: "The auctions listing page loads correctly", href: "/auctions" },
|
|
782
887
|
{ key: "preorders-listing-page", label: "The pre-orders listing page loads correctly", href: "/pre-orders" },
|
|
@@ -1017,6 +1122,12 @@ const rawTesterChecklistItems = [
|
|
|
1017
1122
|
pageLabel: "Site & System",
|
|
1018
1123
|
cases: [
|
|
1019
1124
|
{ key: "site-settings-admin", label: "Admin site settings page saves correctly", href: "/admin/site" },
|
|
1125
|
+
{
|
|
1126
|
+
key: "auction-bid-tiers-admin",
|
|
1127
|
+
label: "Admin can add, edit, and remove bid-increment tiers in Site Settings → Auction Config, and the changes persist after saving and reloading the page",
|
|
1128
|
+
description: "Each tier row has an editable \"up to (₹)\" amount and \"increment (₹)\" amount; the last row is fixed as \"and above\" (open-ended) and can't have its threshold edited. Add a new tier, edit an existing increment, remove a tier, save, then reload the Site Settings page and confirm the tier list matches what was saved — not reverted to the old defaults.",
|
|
1129
|
+
href: "/admin/site",
|
|
1130
|
+
},
|
|
1020
1131
|
{ key: "admin-dashboard-widgets", label: "Admin dashboard widgets show accurate data", href: "/admin/dashboard" },
|
|
1021
1132
|
{ key: "analytics-admin", label: "Admin analytics dashboard shows accurate data", href: "/admin/analytics" },
|
|
1022
1133
|
{ key: "maintenance-pages-admin", label: "The maintenance pages (analysis, client-errors, cloud-logs, function-errors, payment-rollbacks, server-errors + detail) all load correctly", href: "/admin/maintenance" },
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public re-export of the appkit hand-mode primitives.
|
|
3
|
+
*
|
|
4
|
+
* `HandModeProvider`, `useHandMode`, and the related types live in
|
|
5
|
+
* `_internal/client/hand-mode/` because they are consumed by both the public
|
|
6
|
+
* `client.ts` barrel and other internal modules (drawers, sidebars). This
|
|
7
|
+
* file is the canonical public surface — `client.ts` re-exports through
|
|
8
|
+
* here so the `audit-appkit-reexports` rule (no `_internal/` in public
|
|
9
|
+
* barrels) stays green. Mirrors `theme/index.ts`.
|
|
10
|
+
*/
|
|
11
|
+
export { HandModeProvider, useHandMode } from "../_internal/client/hand-mode";
|
|
12
|
+
export type { HandMode, HandModeContextValue, HandModeProviderProps, } from "../_internal/client/hand-mode";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public re-export of the appkit hand-mode primitives.
|
|
3
|
+
*
|
|
4
|
+
* `HandModeProvider`, `useHandMode`, and the related types live in
|
|
5
|
+
* `_internal/client/hand-mode/` because they are consumed by both the public
|
|
6
|
+
* `client.ts` barrel and other internal modules (drawers, sidebars). This
|
|
7
|
+
* file is the canonical public surface — `client.ts` re-exports through
|
|
8
|
+
* here so the `audit-appkit-reexports` rule (no `_internal/` in public
|
|
9
|
+
* barrels) stays green. Mirrors `theme/index.ts`.
|
|
10
|
+
*/
|
|
11
|
+
export { HandModeProvider, useHandMode } from "../_internal/client/hand-mode";
|
package/dist/index.d.ts
CHANGED
|
@@ -2321,6 +2321,8 @@ export { CatalogueItemEditorView } from "./features/catalogue/index";
|
|
|
2321
2321
|
export type { CatalogueItemEditorViewProps } from "./features/catalogue/index";
|
|
2322
2322
|
export { PublicCatalogueView } from "./features/catalogue/index";
|
|
2323
2323
|
export type { PublicCatalogueViewProps } from "./features/catalogue/index";
|
|
2324
|
+
export { PublicCatalogueItemDetailView } from "./features/catalogue/index";
|
|
2325
|
+
export type { PublicCatalogueItemDetailViewProps } from "./features/catalogue/index";
|
|
2324
2326
|
export { AdminCatalogueApprovalsView } from "./features/catalogue/index";
|
|
2325
2327
|
export { PAYOUT_FIELDS } from "./features/payments/index";
|
|
2326
2328
|
export { PAYOUT_INDEXED_FIELDS } from "./features/payments/index";
|
package/dist/index.js
CHANGED
|
@@ -4215,6 +4215,7 @@ export { approveCatalogueListingAction, rejectCatalogueListingAction, } from "./
|
|
|
4215
4215
|
export { UserCatalogueView } from "./features/catalogue/index";
|
|
4216
4216
|
export { CatalogueItemEditorView } from "./features/catalogue/index";
|
|
4217
4217
|
export { PublicCatalogueView } from "./features/catalogue/index";
|
|
4218
|
+
export { PublicCatalogueItemDetailView } from "./features/catalogue/index";
|
|
4218
4219
|
export { AdminCatalogueApprovalsView } from "./features/catalogue/index";
|
|
4219
4220
|
// [SCHEMA]-Schema / data-shape constant â€" Zod validator, default-value object, or Firestore collection/field name constant.
|
|
4220
4221
|
// PAYOUT_FIELDS - Constant used across modules.
|
|
@@ -105,6 +105,7 @@ export declare const DEFAULT_ROUTE_MAP: {
|
|
|
105
105
|
readonly LIVE: "/live";
|
|
106
106
|
readonly LIVE_DETAIL: (slug: string) => string;
|
|
107
107
|
readonly ART: "/art";
|
|
108
|
+
readonly CATALOGUE_ITEM_DETAIL: (ownerSlug: string, itemId: string) => string;
|
|
108
109
|
};
|
|
109
110
|
readonly ERRORS: {
|
|
110
111
|
readonly UNAUTHORIZED: "/unauthorized";
|
|
@@ -470,6 +471,7 @@ export declare const ROUTES: {
|
|
|
470
471
|
readonly LIVE: "/live";
|
|
471
472
|
readonly LIVE_DETAIL: (slug: string) => string;
|
|
472
473
|
readonly ART: "/art";
|
|
474
|
+
readonly CATALOGUE_ITEM_DETAIL: (ownerSlug: string, itemId: string) => string;
|
|
473
475
|
};
|
|
474
476
|
readonly ERRORS: {
|
|
475
477
|
readonly UNAUTHORIZED: "/unauthorized";
|
|
@@ -99,6 +99,13 @@ export const DEFAULT_ROUTE_MAP = {
|
|
|
99
99
|
// detail page stays PRODUCT_DETAIL (`/products/{slug}`), already wired in
|
|
100
100
|
// `_internal/shared/listing-types/{art,stickers}/config.ts`.
|
|
101
101
|
ART: "/art",
|
|
102
|
+
// Personal catalogue (Feature B) — a user's own photo inventory of owned
|
|
103
|
+
// items, distinct from marketplace listings. Public when the owner sets
|
|
104
|
+
// an item's visibility to "public". The owner's full catalogue listing
|
|
105
|
+
// already exists at PROFILE(userId)/catalogue (PublicProfileView's
|
|
106
|
+
// "Catalogue" tab) — this is only the per-item detail route, which had
|
|
107
|
+
// no page at all before this.
|
|
108
|
+
CATALOGUE_ITEM_DETAIL: (ownerSlug, itemId) => `/catalogue/${ownerSlug}/${itemId}`,
|
|
102
109
|
},
|
|
103
110
|
ERRORS: {
|
|
104
111
|
UNAUTHORIZED: "/unauthorized",
|
|
@@ -41,6 +41,11 @@ export interface SessionUser {
|
|
|
41
41
|
permissions?: string[];
|
|
42
42
|
/** Hash of the last dismissed announcement banner. */
|
|
43
43
|
dismissedBannerHash?: string;
|
|
44
|
+
/** Persisted UI layout preferences (collapsed sections, left-hand mode, ...). */
|
|
45
|
+
uiPreferences?: {
|
|
46
|
+
collapsedSections?: string[];
|
|
47
|
+
handMode?: "left" | "right";
|
|
48
|
+
};
|
|
44
49
|
}
|
|
45
50
|
export interface SessionContextValue {
|
|
46
51
|
user: SessionUser | null;
|
|
@@ -79,6 +79,7 @@ function buildSessionUser(authUser, serverData) {
|
|
|
79
79
|
? new Date(serverData.scamAwarenessAcknowledgedAt)
|
|
80
80
|
: null,
|
|
81
81
|
dismissedBannerHash: serverData.dismissedBannerHash,
|
|
82
|
+
uiPreferences: serverData.uiPreferences,
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
// ---------------------------------------------------------------------------
|
|
@@ -150,6 +151,7 @@ export function SessionProvider({ children, initialUser, endpoints: endpointOver
|
|
|
150
151
|
? new Date(data.scamAwarenessAcknowledgedAt)
|
|
151
152
|
: null,
|
|
152
153
|
dismissedBannerHash: data.dismissedBannerHash,
|
|
154
|
+
uiPreferences: data.uiPreferences,
|
|
153
155
|
};
|
|
154
156
|
}
|
|
155
157
|
catch (_err) {
|