@mohasinac/appkit 4.32.6 → 4.33.1
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/products/actions.js +29 -3
- package/dist/_internal/server/features/products/list-public.d.ts +1 -1
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/stores/denormalize.d.ts +44 -0
- package/dist/_internal/server/features/stores/denormalize.js +58 -0
- package/dist/_internal/server/features/stores/index.d.ts +1 -0
- package/dist/_internal/server/features/stores/index.js +1 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/storeNameBackfill.d.ts +27 -0
- package/dist/_internal/server/jobs/core/storeNameBackfill.js +90 -0
- package/dist/_internal/shared/features/products/to-product-item.js +7 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4 -1
- package/dist/features/about/components/PolicyPageView.js +1 -1
- package/dist/features/admin/actions/admin-actions.js +11 -1
- package/dist/features/admin/components/AdminSectionsView.js +17 -20
- package/dist/features/admin/components/sections/adminSectionsBuildParse.js +31 -14
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +37 -7
- package/dist/features/admin/components/sections/adminSectionsTypes.js +11 -7
- package/dist/features/auctions/components/AuctionDetailPageView.js +56 -36
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +9 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -2
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +7 -1
- package/dist/features/blog/components/BlogFeaturedCard.js +3 -3
- package/dist/features/cart/repository/cart.repository.d.ts +27 -6
- package/dist/features/cart/repository/cart.repository.js +52 -21
- package/dist/features/cart/utils/guest-cart.js +5 -0
- package/dist/features/cart/utils/pending-ops.d.ts +15 -0
- package/dist/features/cart/utils/pending-ops.js +24 -0
- package/dist/features/homepage/components/BlogArticlesSection.d.ts +7 -1
- package/dist/features/homepage/components/BlogArticlesSection.js +5 -3
- package/dist/features/homepage/components/CTABannerSection.d.ts +18 -1
- package/dist/features/homepage/components/CTABannerSection.js +22 -4
- package/dist/features/homepage/components/CustomerReviewsSection.d.ts +7 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +2 -2
- package/dist/features/homepage/components/FAQSection.js +2 -11
- package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedAuctionsSection.js +5 -3
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +7 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +6 -5
- package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedPreOrdersSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -1
- package/dist/features/homepage/components/FeaturedProductsSection.js +5 -3
- package/dist/features/homepage/components/HeroCarousel.d.ts +23 -2
- package/dist/features/homepage/components/HeroCarousel.js +22 -9
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.d.ts +6 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +11 -3
- package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +9 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +7 -4
- package/dist/features/homepage/components/StatsCounterSection.d.ts +6 -1
- package/dist/features/homepage/components/StatsCounterSection.js +3 -3
- package/dist/features/homepage/constants/faq-category-labels.d.ts +15 -0
- package/dist/features/homepage/constants/faq-category-labels.js +23 -0
- package/dist/features/homepage/constants/section-copy.d.ts +107 -0
- package/dist/features/homepage/constants/section-copy.js +96 -0
- package/dist/features/homepage/lib/section-renderer.d.ts +16 -1
- package/dist/features/homepage/lib/section-renderer.js +58 -37
- package/dist/features/homepage/repository/homepage-sections.repository.d.ts +23 -0
- package/dist/features/homepage/repository/homepage-sections.repository.js +40 -0
- package/dist/features/homepage/schemas/firestore.d.ts +120 -44
- package/dist/features/media/MediaImage.d.ts +10 -1
- package/dist/features/media/MediaImage.js +6 -3
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/products/components/CardStoreLine.d.ts +32 -0
- package/dist/features/products/components/CardStoreLine.js +14 -0
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -1
- package/dist/features/products/components/ProductGrid.js +3 -6
- package/dist/features/products/schemas/firestore.d.ts +17 -0
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/seller/actions/seller-actions.d.ts +17 -0
- package/dist/features/seller/actions/seller-actions.js +54 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +3 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +137 -0
- package/dist/seed/carousel-slides-seed-data.js +154 -22
- package/dist/seed/carousels-seed-data.js +4 -1
- package/dist/seed/homepage-sections-seed-data.js +337 -254
- package/dist/seed/products-preorders-seed-data.d.ts +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +4 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category-neutral default copy for homepage sections.
|
|
3
|
+
*
|
|
4
|
+
* Every `cleanTitle(cfg?.title) || "…"` fallback in `lib/section-renderer.tsx`
|
|
5
|
+
* resolves here. Before this file the renderer carried ~25 hardcoded English
|
|
6
|
+
* strings, several of them brand- or catalogue-specific — "India's #1
|
|
7
|
+
* Marketplace", "Why Buyers Trust LetItRip", and an announcement-bar fallback
|
|
8
|
+
* naming a Pokémon TCG promotion. appkit is a library; a consumer's brand and
|
|
9
|
+
* catalogue must not be compiled into it.
|
|
10
|
+
*
|
|
11
|
+
* Three tiers own the copy on this page, and the split is deliberate:
|
|
12
|
+
*
|
|
13
|
+
* Tier A — THIS FILE. Generic defaults that read correctly for any
|
|
14
|
+
* marketplace regardless of what it sells.
|
|
15
|
+
* Tier B — `MarketplaceHomepageViewProps.brand`, passed by the consumer from
|
|
16
|
+
* its own single source of site identity.
|
|
17
|
+
* Tier C — optional `*SectionConfig` fields, so an admin can override any of
|
|
18
|
+
* these per section instance without a deploy.
|
|
19
|
+
*
|
|
20
|
+
* Not `appkit.config.js`: the renderer runs inside appkit's own RSC, and that
|
|
21
|
+
* file lives on the CONSUMER side of the seam. Importing it from here would
|
|
22
|
+
* invert the library dependency — the Duplication Framework's "Lane / API
|
|
23
|
+
* boundary" clause, and what `audit-ssr-in-appkit` Rule 3 protects.
|
|
24
|
+
*
|
|
25
|
+
* Keep every string here catalogue-neutral. "Shop by Category" survives the
|
|
26
|
+
* catalogue changing; "Burst & X Series" does not.
|
|
27
|
+
*/
|
|
28
|
+
/** Section headings. Overridable per instance via `config.title`. */
|
|
29
|
+
export declare const SECTION_TITLE: {
|
|
30
|
+
readonly welcome: "Discover Amazing Products";
|
|
31
|
+
readonly categories: "Shop by Category";
|
|
32
|
+
readonly products: "Featured Products";
|
|
33
|
+
readonly auctions: "Live Auctions";
|
|
34
|
+
readonly preOrders: "Reserve Before It Ships";
|
|
35
|
+
readonly stores: "Featured Stores";
|
|
36
|
+
readonly events: "Events & Offers";
|
|
37
|
+
readonly reviews: "What Our Customers Say";
|
|
38
|
+
readonly banner: "Thousands of collectibles. One marketplace.";
|
|
39
|
+
readonly trustIndicators: "Why Buyers Trust Us";
|
|
40
|
+
readonly features: "Security You Can Trust";
|
|
41
|
+
readonly whatsappCommunity: "Join Our Community";
|
|
42
|
+
readonly faq: "Frequently Asked Questions";
|
|
43
|
+
readonly blogArticles: "From Our Blog";
|
|
44
|
+
readonly newsletter: "Stay Updated";
|
|
45
|
+
readonly brands: "Top Brands";
|
|
46
|
+
readonly featuredBundles: "Curated Bundles";
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* "View all →" links. Overridable per instance via `config.viewMoreLabel`.
|
|
50
|
+
*
|
|
51
|
+
* These were hardcoded at every call site with no config path at all, so an
|
|
52
|
+
* admin could rename a section's heading and not its own link.
|
|
53
|
+
*/
|
|
54
|
+
export declare const VIEW_MORE_LABEL: {
|
|
55
|
+
readonly categories: "All categories →";
|
|
56
|
+
readonly products: "View all products →";
|
|
57
|
+
readonly auctions: "View all auctions →";
|
|
58
|
+
readonly preOrders: "View all pre-orders →";
|
|
59
|
+
readonly stores: "View all stores →";
|
|
60
|
+
readonly events: "View all events →";
|
|
61
|
+
readonly reviews: "See all reviews →";
|
|
62
|
+
readonly blogArticles: "View all posts →";
|
|
63
|
+
readonly brands: "All brands →";
|
|
64
|
+
readonly faq: "View all FAQs →";
|
|
65
|
+
};
|
|
66
|
+
/** Supporting copy that has no per-section config field of its own. */
|
|
67
|
+
export declare const SECTION_COPY: {
|
|
68
|
+
readonly welcomePill: "Buy, sell & collect";
|
|
69
|
+
readonly welcomePrimaryCta: "Shop Now";
|
|
70
|
+
readonly welcomeSecondaryCta: "Browse All";
|
|
71
|
+
readonly featuresPill: "Built for trust";
|
|
72
|
+
readonly featuresLearnMore: "Learn about our security →";
|
|
73
|
+
readonly bannerPrimaryCta: "Shop All Products →";
|
|
74
|
+
readonly bannerSecondaryCta: "Browse Auctions →";
|
|
75
|
+
readonly whatsappDescription: "Get deal alerts, auction updates, and exclusive drops before anyone else.";
|
|
76
|
+
/**
|
|
77
|
+
* Shown only when `siteSettings.announcementBar` has no message. Was a
|
|
78
|
+
* hardcoded seasonal Pokémon TCG discount, which would have gone out as
|
|
79
|
+
* live promotional copy on any install that left the bar unconfigured.
|
|
80
|
+
*/
|
|
81
|
+
readonly announcementFallback: "Free shipping on eligible orders — shop the latest arrivals.";
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Trust chips under the hero headline.
|
|
85
|
+
*
|
|
86
|
+
* Kept as a fixed generic default rather than a config field: making it
|
|
87
|
+
* editable needs a repeater UI in the section builder, which is
|
|
88
|
+
* disproportionate to four short labels that are true of any marketplace.
|
|
89
|
+
* Revisit if a consumer needs different ones.
|
|
90
|
+
*/
|
|
91
|
+
export declare const WELCOME_TRUST_CHIPS: readonly [{
|
|
92
|
+
readonly key: "delivery";
|
|
93
|
+
readonly emoji: "🚀";
|
|
94
|
+
readonly label: "Fast Delivery";
|
|
95
|
+
}, {
|
|
96
|
+
readonly key: "secure";
|
|
97
|
+
readonly emoji: "🔒";
|
|
98
|
+
readonly label: "Secure Payments";
|
|
99
|
+
}, {
|
|
100
|
+
readonly key: "rating";
|
|
101
|
+
readonly emoji: "⭐";
|
|
102
|
+
readonly label: "Buyer Rated";
|
|
103
|
+
}, {
|
|
104
|
+
readonly key: "returns";
|
|
105
|
+
readonly emoji: "↩️";
|
|
106
|
+
readonly label: "Easy Returns";
|
|
107
|
+
}];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category-neutral default copy for homepage sections.
|
|
3
|
+
*
|
|
4
|
+
* Every `cleanTitle(cfg?.title) || "…"` fallback in `lib/section-renderer.tsx`
|
|
5
|
+
* resolves here. Before this file the renderer carried ~25 hardcoded English
|
|
6
|
+
* strings, several of them brand- or catalogue-specific — "India's #1
|
|
7
|
+
* Marketplace", "Why Buyers Trust LetItRip", and an announcement-bar fallback
|
|
8
|
+
* naming a Pokémon TCG promotion. appkit is a library; a consumer's brand and
|
|
9
|
+
* catalogue must not be compiled into it.
|
|
10
|
+
*
|
|
11
|
+
* Three tiers own the copy on this page, and the split is deliberate:
|
|
12
|
+
*
|
|
13
|
+
* Tier A — THIS FILE. Generic defaults that read correctly for any
|
|
14
|
+
* marketplace regardless of what it sells.
|
|
15
|
+
* Tier B — `MarketplaceHomepageViewProps.brand`, passed by the consumer from
|
|
16
|
+
* its own single source of site identity.
|
|
17
|
+
* Tier C — optional `*SectionConfig` fields, so an admin can override any of
|
|
18
|
+
* these per section instance without a deploy.
|
|
19
|
+
*
|
|
20
|
+
* Not `appkit.config.js`: the renderer runs inside appkit's own RSC, and that
|
|
21
|
+
* file lives on the CONSUMER side of the seam. Importing it from here would
|
|
22
|
+
* invert the library dependency — the Duplication Framework's "Lane / API
|
|
23
|
+
* boundary" clause, and what `audit-ssr-in-appkit` Rule 3 protects.
|
|
24
|
+
*
|
|
25
|
+
* Keep every string here catalogue-neutral. "Shop by Category" survives the
|
|
26
|
+
* catalogue changing; "Burst & X Series" does not.
|
|
27
|
+
*/
|
|
28
|
+
/** Section headings. Overridable per instance via `config.title`. */
|
|
29
|
+
export const SECTION_TITLE = {
|
|
30
|
+
welcome: "Discover Amazing Products",
|
|
31
|
+
categories: "Shop by Category",
|
|
32
|
+
products: "Featured Products",
|
|
33
|
+
auctions: "Live Auctions",
|
|
34
|
+
preOrders: "Reserve Before It Ships",
|
|
35
|
+
stores: "Featured Stores",
|
|
36
|
+
events: "Events & Offers",
|
|
37
|
+
reviews: "What Our Customers Say",
|
|
38
|
+
banner: "Thousands of collectibles. One marketplace.",
|
|
39
|
+
trustIndicators: "Why Buyers Trust Us",
|
|
40
|
+
features: "Security You Can Trust",
|
|
41
|
+
whatsappCommunity: "Join Our Community",
|
|
42
|
+
faq: "Frequently Asked Questions",
|
|
43
|
+
blogArticles: "From Our Blog",
|
|
44
|
+
newsletter: "Stay Updated",
|
|
45
|
+
brands: "Top Brands",
|
|
46
|
+
featuredBundles: "Curated Bundles",
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* "View all →" links. Overridable per instance via `config.viewMoreLabel`.
|
|
50
|
+
*
|
|
51
|
+
* These were hardcoded at every call site with no config path at all, so an
|
|
52
|
+
* admin could rename a section's heading and not its own link.
|
|
53
|
+
*/
|
|
54
|
+
export const VIEW_MORE_LABEL = {
|
|
55
|
+
categories: "All categories →",
|
|
56
|
+
products: "View all products →",
|
|
57
|
+
auctions: "View all auctions →",
|
|
58
|
+
preOrders: "View all pre-orders →",
|
|
59
|
+
stores: "View all stores →",
|
|
60
|
+
events: "View all events →",
|
|
61
|
+
reviews: "See all reviews →",
|
|
62
|
+
blogArticles: "View all posts →",
|
|
63
|
+
brands: "All brands →",
|
|
64
|
+
faq: "View all FAQs →",
|
|
65
|
+
};
|
|
66
|
+
/** Supporting copy that has no per-section config field of its own. */
|
|
67
|
+
export const SECTION_COPY = {
|
|
68
|
+
welcomePill: "Buy, sell & collect",
|
|
69
|
+
welcomePrimaryCta: "Shop Now",
|
|
70
|
+
welcomeSecondaryCta: "Browse All",
|
|
71
|
+
featuresPill: "Built for trust",
|
|
72
|
+
featuresLearnMore: "Learn about our security →",
|
|
73
|
+
bannerPrimaryCta: "Shop All Products →",
|
|
74
|
+
bannerSecondaryCta: "Browse Auctions →",
|
|
75
|
+
whatsappDescription: "Get deal alerts, auction updates, and exclusive drops before anyone else.",
|
|
76
|
+
/**
|
|
77
|
+
* Shown only when `siteSettings.announcementBar` has no message. Was a
|
|
78
|
+
* hardcoded seasonal Pokémon TCG discount, which would have gone out as
|
|
79
|
+
* live promotional copy on any install that left the bar unconfigured.
|
|
80
|
+
*/
|
|
81
|
+
announcementFallback: "Free shipping on eligible orders — shop the latest arrivals.",
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Trust chips under the hero headline.
|
|
85
|
+
*
|
|
86
|
+
* Kept as a fixed generic default rather than a config field: making it
|
|
87
|
+
* editable needs a repeater UI in the section builder, which is
|
|
88
|
+
* disproportionate to four short labels that are true of any marketplace.
|
|
89
|
+
* Revisit if a consumer needs different ones.
|
|
90
|
+
*/
|
|
91
|
+
export const WELCOME_TRUST_CHIPS = [
|
|
92
|
+
{ key: "delivery", emoji: "🚀", label: "Fast Delivery" },
|
|
93
|
+
{ key: "secure", emoji: "🔒", label: "Secure Payments" },
|
|
94
|
+
{ key: "rating", emoji: "⭐", label: "Buyer Rated" },
|
|
95
|
+
{ key: "returns", emoji: "↩️", label: "Easy Returns" },
|
|
96
|
+
];
|
|
@@ -32,5 +32,20 @@ export type FaqItem = {
|
|
|
32
32
|
answer: string;
|
|
33
33
|
category: string;
|
|
34
34
|
};
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Consumer's site identity.
|
|
37
|
+
*
|
|
38
|
+
* appkit is a library and must not compile a consumer's brand into itself —
|
|
39
|
+
* the renderer used to hardcode `brandLogoText="LIR"` and
|
|
40
|
+
* `"Why Buyers Trust LetItRip"`. The consumer passes this from whatever single
|
|
41
|
+
* source it already uses for site identity, so there is no second definition
|
|
42
|
+
* to drift.
|
|
43
|
+
*/
|
|
44
|
+
export interface HomepageBrand {
|
|
45
|
+
/** Full display name, e.g. used in "Why Buyers Trust {name}". */
|
|
46
|
+
name?: string;
|
|
47
|
+
/** Short mark shown in the hero panel, e.g. "LT". */
|
|
48
|
+
shortName?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function renderSection(section: HomepageSectionDocument, adSlots: MarketplaceHomepageViewAdSlots | undefined, newsletterFormSlot: React.ReactNode, faqItems: FaqItem[], slides: CarouselSlide[], liveStats: LiveStatsMap, sectionData?: SectionData, brand?: HomepageBrand): React.ReactNode;
|
|
36
51
|
export { AnnouncementBar, hashBannerMessage };
|
|
@@ -3,6 +3,8 @@ import React from "react";
|
|
|
3
3
|
import { ROUTES } from "../../../next";
|
|
4
4
|
import { cleanTitle, parseWelcomeDescription } from "./section-helpers";
|
|
5
5
|
import { DEFAULT_TRUST_FEATURES, DEFAULT_SECURITY_ITEMS } from "./section-defaults";
|
|
6
|
+
import { SECTION_TITLE, SECTION_COPY, VIEW_MORE_LABEL, WELCOME_TRUST_CHIPS, } from "../constants/section-copy";
|
|
7
|
+
import { FAQ_CATEGORY_LABELS } from "../constants/faq-category-labels";
|
|
6
8
|
import { AnnouncementBar, hashBannerMessage } from "../components/AnnouncementBar";
|
|
7
9
|
import { HeroCarousel } from "../components/HeroCarousel";
|
|
8
10
|
import { StatsCounterSection } from "../components/StatsCounterSection";
|
|
@@ -38,23 +40,23 @@ const AD_SLOT_MAP = {
|
|
|
38
40
|
reviews: "afterReviews",
|
|
39
41
|
faq: "afterFAQ",
|
|
40
42
|
};
|
|
41
|
-
function renderSectionElement(section, newsletterFormSlot, faqItems, slides, liveStats, sectionData) {
|
|
43
|
+
function renderSectionElement(section, newsletterFormSlot, faqItems, slides, liveStats, sectionData, brand) {
|
|
42
44
|
const { type, config } = section;
|
|
43
45
|
switch (type) {
|
|
44
|
-
case "carousel":
|
|
45
|
-
|
|
46
|
+
case "carousel": {
|
|
47
|
+
const cfg = config;
|
|
48
|
+
return (_jsx(HeroCarousel, { initialSlides: slides, title: cfg?.title, height: cfg?.height, defaultAutoplayDelayMs: cfg?.defaultAutoplayDelayMs, pauseOnHover: cfg?.pauseOnHover, showDots: cfg?.showDots, showArrows: cfg?.showArrows }));
|
|
49
|
+
}
|
|
46
50
|
case "welcome": {
|
|
47
51
|
const cfg = config;
|
|
48
|
-
return (_jsx(WelcomeSection, { title: cleanTitle(cfg?.h1) ||
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{ key: "returns", emoji: "↩️", label: "Easy Returns" },
|
|
53
|
-
], brandLogoText: "LIR" }));
|
|
52
|
+
return (_jsx(WelcomeSection, { title: cleanTitle(cfg?.h1) || SECTION_TITLE.welcome,
|
|
53
|
+
// A real `subtitle` wins; `description` is the legacy source and is
|
|
54
|
+
// still parsed for documents that only carry it.
|
|
55
|
+
subtitle: cfg?.subtitle?.trim() || parseWelcomeDescription(cfg?.description), pillLabel: cfg?.pillLabel || SECTION_COPY.welcomePill, showCTA: cfg?.showCTA ?? true, ctaLabel: cfg?.ctaText || SECTION_COPY.welcomePrimaryCta, ctaHref: cfg?.ctaLink || ROUTES.PUBLIC.PRODUCTS, secondaryCtaLabel: cfg?.secondaryCtaText || SECTION_COPY.welcomeSecondaryCta, secondaryCtaHref: cfg?.secondaryCtaLink || ROUTES.PUBLIC.PRODUCTS, trustChips: [...WELCOME_TRUST_CHIPS], brandLogoText: brand?.shortName }));
|
|
54
56
|
}
|
|
55
57
|
case "categories": {
|
|
56
58
|
const cfg = config;
|
|
57
|
-
return (_jsx(ShopByCategorySection, { title: cleanTitle(cfg?.title) ||
|
|
59
|
+
return (_jsx(ShopByCategorySection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.categories, viewMoreHref: ROUTES.PUBLIC.CATEGORIES, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.categories, initialItems: sectionData.categories, limit: cfg?.maxCategories, cta: cfg?.cta, filters: cfg?.filters, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
58
60
|
}
|
|
59
61
|
case "stats": {
|
|
60
62
|
const cfg = config;
|
|
@@ -81,47 +83,75 @@ function renderSectionElement(section, newsletterFormSlot, faqItems, slides, liv
|
|
|
81
83
|
: [];
|
|
82
84
|
if (stats.length === 0)
|
|
83
85
|
return null;
|
|
84
|
-
return _jsx(StatsCounterSection, { stats: stats });
|
|
86
|
+
return _jsx(StatsCounterSection, { stats: stats, title: cleanTitle(cfg?.title) });
|
|
85
87
|
}
|
|
86
88
|
case "products": {
|
|
87
89
|
const cfg = config;
|
|
88
|
-
return (_jsx(FeaturedProductsSection, { title: cleanTitle(cfg?.title) ||
|
|
90
|
+
return (_jsx(FeaturedProductsSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.products, description: cfg?.subtitle, viewMoreHref: ROUTES.PUBLIC.PRODUCTS, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.products, filterByBrand: cfg?.filterByBrand, initialItems: sectionData.products, rows: cfg?.rows,
|
|
91
|
+
// `maxProducts` is what the admin builder's "Max items" input writes;
|
|
92
|
+
// `maxItems` is the schema's own name. The two never met, so the
|
|
93
|
+
// control had no effect. Read both, newest name first.
|
|
94
|
+
maxItems: cfg?.maxItems ?? cfg?.maxProducts, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
89
95
|
}
|
|
90
96
|
case "auctions": {
|
|
91
97
|
const cfg = config;
|
|
92
|
-
return (_jsx(FeaturedAuctionsSection, { title: cleanTitle(cfg?.title) ||
|
|
98
|
+
return (_jsx(FeaturedAuctionsSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.auctions, description: cfg?.subtitle, viewMoreHref: ROUTES.PUBLIC.AUCTIONS, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.auctions, filterByBrand: cfg?.filterByBrand, initialItems: sectionData.auctions, rows: cfg?.rows, maxItems: cfg?.maxAuctions, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
93
99
|
}
|
|
94
100
|
case "pre-orders": {
|
|
95
101
|
const cfg = config;
|
|
96
|
-
return (_jsx(FeaturedPreOrdersSection, { title: cleanTitle(cfg?.title) ||
|
|
102
|
+
return (_jsx(FeaturedPreOrdersSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.preOrders, description: cfg?.subtitle, viewMoreHref: ROUTES.PUBLIC.PRE_ORDERS, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.preOrders, filterByBrand: cfg?.filterByBrand, initialItems: sectionData.preOrders, rows: cfg?.rows, maxItems: cfg?.maxItems, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
97
103
|
}
|
|
98
104
|
case "stores": {
|
|
99
105
|
const cfg = config;
|
|
100
|
-
return (_jsx(FeaturedStoresSection, { title: cleanTitle(cfg?.title) ||
|
|
106
|
+
return (_jsx(FeaturedStoresSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.stores, description: cfg?.subtitle, viewMoreHref: ROUTES.PUBLIC.STORES, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.stores, initialItems: sectionData.stores, limit: cfg?.maxStores, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
101
107
|
}
|
|
102
108
|
case "events": {
|
|
103
109
|
const cfg = config;
|
|
104
|
-
return (_jsx(EventsSection, { title: cleanTitle(cfg?.title) ||
|
|
110
|
+
return (_jsx(EventsSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.events, description: cfg?.subtitle, viewMoreHref: ROUTES.PUBLIC.EVENTS, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.events, initialItems: sectionData.events, limit: cfg?.maxEvents, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
105
111
|
}
|
|
106
112
|
case "reviews": {
|
|
107
113
|
const cfg = config;
|
|
108
|
-
return (_jsx(HomepageCustomerReviewsSection, { title: cleanTitle(cfg?.title) ||
|
|
114
|
+
return (_jsx(HomepageCustomerReviewsSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.reviews, viewMoreHref: ROUTES.PUBLIC.REVIEWS, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.reviews, maxReviews: cfg?.maxReviews, itemsPerView: cfg?.itemsPerView, mobileItemsPerView: cfg?.mobileItemsPerView, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
109
115
|
}
|
|
110
116
|
case "banner": {
|
|
111
117
|
const cfg = config;
|
|
112
|
-
|
|
118
|
+
// CTAs come from the configured buttons; the component's own neutral
|
|
119
|
+
// defaults fill in when none are set. Previously both labels and both
|
|
120
|
+
// hrefs were hardcoded here and `buttons[]` was ignored entirely.
|
|
121
|
+
const [primaryBtn, secondaryBtn] = cfg?.buttons ?? [];
|
|
122
|
+
return (_jsx(CTABannerSection, { title: cfg?.content?.title || SECTION_TITLE.banner, subtitle: cfg?.content?.subtitle, description: cfg?.content?.description, height: cfg?.height, backgroundImage: cfg?.backgroundImage, primaryLabel: primaryBtn?.text, primaryHref: primaryBtn?.link, secondaryLabel: secondaryBtn?.text, secondaryHref: secondaryBtn?.link }));
|
|
113
123
|
}
|
|
114
124
|
case "trust-indicators": {
|
|
115
125
|
const cfg = config;
|
|
116
|
-
|
|
126
|
+
const configured = (cfg?.indicators ?? [])
|
|
127
|
+
.filter((ind) => ind?.title?.trim())
|
|
128
|
+
.map((ind) => ({
|
|
129
|
+
key: ind.id,
|
|
130
|
+
iconName: ind.icon,
|
|
131
|
+
title: ind.title,
|
|
132
|
+
description: ind.description,
|
|
133
|
+
}));
|
|
134
|
+
return (_jsx(TrustFeaturesSection, { title: cleanTitle(cfg?.title) ||
|
|
135
|
+
(brand?.name ? `Why Buyers Trust ${brand.name}` : SECTION_TITLE.trustIndicators), items: configured.length > 0 ? configured : DEFAULT_TRUST_FEATURES }));
|
|
117
136
|
}
|
|
118
137
|
case "features": {
|
|
119
138
|
const cfg = config;
|
|
120
|
-
|
|
139
|
+
// `items` is the real shape; `features: string[]` is the legacy one and
|
|
140
|
+
// becomes title-only cards. Neither reached the component before — it
|
|
141
|
+
// always rendered DEFAULT_SECURITY_ITEMS regardless of config.
|
|
142
|
+
const configured = cfg?.items?.filter((item) => item?.title?.trim()) ??
|
|
143
|
+
cfg?.features
|
|
144
|
+
?.filter((label) => label?.trim())
|
|
145
|
+
.map((label, index) => ({ key: `feature-${index}`, title: label, description: "" })) ??
|
|
146
|
+
[];
|
|
147
|
+
return (_jsx(SecurityHighlightsSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.features, pillLabel: cfg?.pillLabel || SECTION_COPY.featuresPill, items: configured.length > 0 ? configured : DEFAULT_SECURITY_ITEMS, learnMoreHref: cfg?.learnMoreLink || ROUTES.PUBLIC.SECURITY, learnMoreLabel: cfg?.learnMoreLabel || SECTION_COPY.featuresLearnMore }));
|
|
121
148
|
}
|
|
122
149
|
case "whatsapp-community": {
|
|
123
150
|
const cfg = config;
|
|
124
|
-
return (_jsx(WhatsAppCommunitySection, { title: cleanTitle(cfg?.title) ||
|
|
151
|
+
return (_jsx(WhatsAppCommunitySection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.whatsappCommunity, descriptionHtml: cfg?.description || SECTION_COPY.whatsappDescription,
|
|
152
|
+
// No `|| 5000` fallback: inventing a member count the site cannot
|
|
153
|
+
// verify is a fabricated social-proof number.
|
|
154
|
+
memberCount: cfg?.memberCount, groupLink: cfg?.groupLink || "https://chat.whatsapp.com/", benefits: cfg?.benefits ?? [], testimonial: cfg?.testimonial, buttonText: cfg?.buttonText }));
|
|
125
155
|
}
|
|
126
156
|
case "faq": {
|
|
127
157
|
const cfg = config;
|
|
@@ -129,15 +159,6 @@ function renderSectionElement(section, newsletterFormSlot, faqItems, slides, liv
|
|
|
129
159
|
return null;
|
|
130
160
|
// Build tabs from configured visibleTabs (or all categories in config)
|
|
131
161
|
const tabCategories = (cfg?.visibleTabs?.length ? cfg.visibleTabs : cfg?.categories) ?? [];
|
|
132
|
-
const FAQ_CATEGORY_LABELS = {
|
|
133
|
-
general: "General",
|
|
134
|
-
orders_payment: "Orders & Payment",
|
|
135
|
-
shipping_delivery: "Shipping",
|
|
136
|
-
returns_refunds: "Returns & Refunds",
|
|
137
|
-
product_information: "Products",
|
|
138
|
-
account_security: "Account",
|
|
139
|
-
technical_support: "Support",
|
|
140
|
-
};
|
|
141
162
|
const tabs = tabCategories.map((cat) => ({
|
|
142
163
|
value: cat,
|
|
143
164
|
label: FAQ_CATEGORY_LABELS[cat] ?? cat,
|
|
@@ -145,19 +166,19 @@ function renderSectionElement(section, newsletterFormSlot, faqItems, slides, liv
|
|
|
145
166
|
const slicedItems = cfg?.displayCount ? faqItems.slice(0, cfg.displayCount) : faqItems;
|
|
146
167
|
const totalFaqs = faqItems.length;
|
|
147
168
|
const hasMore = totalFaqs > slicedItems.length;
|
|
148
|
-
return (_jsx(FAQSection, { title: cleanTitle(cfg?.title) ||
|
|
169
|
+
return (_jsx(FAQSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.faq, subtitle: cfg?.subtitle, tabs: tabs, showCategoryTabs: cfg?.showCategoryTabs ?? false, allowMultipleOpen: cfg?.allowMultipleOpen ?? false, defaultOpenCount: cfg?.defaultOpenCount ?? 0, items: slicedItems, viewMoreHref: cfg?.linkToFullPage ? ROUTES.PUBLIC.FAQS : undefined, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.faq, hasMore: hasMore, moreCount: totalFaqs - slicedItems.length }));
|
|
149
170
|
}
|
|
150
171
|
case "blog-articles": {
|
|
151
172
|
const cfg = config;
|
|
152
|
-
return (_jsx(BlogArticlesSection, { title: cleanTitle(cfg?.title) ||
|
|
173
|
+
return (_jsx(BlogArticlesSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.blogArticles, viewMoreHref: ROUTES.BLOG.LIST, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.blogArticles, initialItems: sectionData.blog, limit: cfg?.maxArticles, showReadTime: cfg?.showReadTime, showAuthor: cfg?.showAuthor, showThumbnails: cfg?.showThumbnails }));
|
|
153
174
|
}
|
|
154
175
|
case "newsletter": {
|
|
155
176
|
const cfg = config;
|
|
156
|
-
return (_jsx(NewsletterSection, { title: cleanTitle(cfg?.title) ||
|
|
177
|
+
return (_jsx(NewsletterSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.newsletter, subtitle: cfg?.description, privacyLabel: cfg?.privacyText, privacyHref: cfg?.privacyLink, renderForm: () => newsletterFormSlot ?? null }));
|
|
157
178
|
}
|
|
158
179
|
case "brands": {
|
|
159
180
|
const cfg = config;
|
|
160
|
-
return (_jsx(BrandsSection, { title: cleanTitle(cfg?.title) ||
|
|
181
|
+
return (_jsx(BrandsSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.brands, subtitle: cfg?.subtitle, limit: cfg?.maxBrands || 12, viewMoreHref: ROUTES.PUBLIC.CATEGORIES, viewMoreLabel: cfg?.viewMoreLabel || VIEW_MORE_LABEL.brands, initialItems: sectionData.brands, cta: cfg?.cta, filters: cfg?.filters, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
|
|
161
182
|
}
|
|
162
183
|
case "social-feed": {
|
|
163
184
|
const cfg = config;
|
|
@@ -178,7 +199,7 @@ function renderSectionElement(section, newsletterFormSlot, faqItems, slides, liv
|
|
|
178
199
|
}
|
|
179
200
|
case "featured-bundles": {
|
|
180
201
|
const cfg = config;
|
|
181
|
-
return (_jsx(FeaturedBundlesSection, { title: cleanTitle(cfg?.title) ||
|
|
202
|
+
return (_jsx(FeaturedBundlesSection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.featuredBundles, description: cfg?.subtitle, initialItems: sectionData.bundles, maxItems: cfg?.maxItems, showSavingsBadge: cfg?.showSavingsBadge, viewMoreLabel: cfg?.viewMoreLabel }));
|
|
182
203
|
}
|
|
183
204
|
case "prize-draws": {
|
|
184
205
|
const cfg = config;
|
|
@@ -196,8 +217,8 @@ function renderSectionElement(section, newsletterFormSlot, faqItems, slides, liv
|
|
|
196
217
|
return null;
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
|
-
export function renderSection(section, adSlots, newsletterFormSlot, faqItems, slides, liveStats, sectionData = {}) {
|
|
200
|
-
const sectionElement = renderSectionElement(section, newsletterFormSlot, faqItems, slides, liveStats, sectionData);
|
|
220
|
+
export function renderSection(section, adSlots, newsletterFormSlot, faqItems, slides, liveStats, sectionData = {}, brand) {
|
|
221
|
+
const sectionElement = renderSectionElement(section, newsletterFormSlot, faqItems, slides, liveStats, sectionData, brand);
|
|
201
222
|
if (!sectionElement)
|
|
202
223
|
return null;
|
|
203
224
|
const adSlotKey = AD_SLOT_MAP[section.type];
|
|
@@ -11,6 +11,29 @@ export declare class HomepageSectionsRepository extends BaseRepository<HomepageS
|
|
|
11
11
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
12
12
|
list(model: SieveModel): Promise<FirebaseSieveResult<HomepageSectionDocument>>;
|
|
13
13
|
create(input: HomepageSectionCreateInput): Promise<HomepageSectionDocument>;
|
|
14
|
+
/**
|
|
15
|
+
* Merge `config` instead of replacing it.
|
|
16
|
+
*
|
|
17
|
+
* `BaseRepository.update` writes `config` as a top-level key, and a Firestore
|
|
18
|
+
* `update()` REPLACES a map field wholesale rather than merging it. So every
|
|
19
|
+
* config key a caller omitted was silently deleted — HTTP 200, no error. The
|
|
20
|
+
* admin section builder emits only the fields it has inputs for, which meant
|
|
21
|
+
* opening any section and clicking Save destroyed `products.rows`/`maxItems`/
|
|
22
|
+
* `filterByBrand`, `banner.backgroundImage`, `categories.cta`/`filters` and
|
|
23
|
+
* every other unbuilt field. Root Cause #76.
|
|
24
|
+
*
|
|
25
|
+
* Both write paths — `PATCH /api/admin/sections/[id]` and the
|
|
26
|
+
* `updateHomepageSection` server action — funnel through here, so the merge
|
|
27
|
+
* belongs in the write primitive rather than at either call site.
|
|
28
|
+
*
|
|
29
|
+
* Costs one extra read, and only when `config` is actually being written:
|
|
30
|
+
* a reorder or an enable/disable still costs exactly one write.
|
|
31
|
+
*
|
|
32
|
+
* Semantics: an omitted key is PRESERVED. Clearing one requires sending it
|
|
33
|
+
* explicitly (`null` / `""` / `[]`), not omitting it. `resetSectionToDefault`
|
|
34
|
+
* below deliberately keeps replace semantics — that is its whole purpose.
|
|
35
|
+
*/
|
|
36
|
+
update(id: string, data: Partial<HomepageSectionDocument>): Promise<HomepageSectionDocument>;
|
|
14
37
|
getEnabledSections(): Promise<HomepageSectionDocument[]>;
|
|
15
38
|
getDisabledSections(): Promise<HomepageSectionDocument[]>;
|
|
16
39
|
getSectionByType(type: SectionType): Promise<HomepageSectionDocument | null>;
|
|
@@ -28,6 +28,46 @@ export class HomepageSectionsRepository extends BaseRepository {
|
|
|
28
28
|
.set(prepareForFirestore(sectionData));
|
|
29
29
|
return { id, ...sectionData };
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Merge `config` instead of replacing it.
|
|
33
|
+
*
|
|
34
|
+
* `BaseRepository.update` writes `config` as a top-level key, and a Firestore
|
|
35
|
+
* `update()` REPLACES a map field wholesale rather than merging it. So every
|
|
36
|
+
* config key a caller omitted was silently deleted — HTTP 200, no error. The
|
|
37
|
+
* admin section builder emits only the fields it has inputs for, which meant
|
|
38
|
+
* opening any section and clicking Save destroyed `products.rows`/`maxItems`/
|
|
39
|
+
* `filterByBrand`, `banner.backgroundImage`, `categories.cta`/`filters` and
|
|
40
|
+
* every other unbuilt field. Root Cause #76.
|
|
41
|
+
*
|
|
42
|
+
* Both write paths — `PATCH /api/admin/sections/[id]` and the
|
|
43
|
+
* `updateHomepageSection` server action — funnel through here, so the merge
|
|
44
|
+
* belongs in the write primitive rather than at either call site.
|
|
45
|
+
*
|
|
46
|
+
* Costs one extra read, and only when `config` is actually being written:
|
|
47
|
+
* a reorder or an enable/disable still costs exactly one write.
|
|
48
|
+
*
|
|
49
|
+
* Semantics: an omitted key is PRESERVED. Clearing one requires sending it
|
|
50
|
+
* explicitly (`null` / `""` / `[]`), not omitting it. `resetSectionToDefault`
|
|
51
|
+
* below deliberately keeps replace semantics — that is its whole purpose.
|
|
52
|
+
*/
|
|
53
|
+
async update(id, data) {
|
|
54
|
+
if (!data.config) {
|
|
55
|
+
return super.update(id, data);
|
|
56
|
+
}
|
|
57
|
+
// A missing doc falls through to super.update, which fails the same way it
|
|
58
|
+
// did before this override existed — don't invent a new error path here.
|
|
59
|
+
const existing = await this.findById(id);
|
|
60
|
+
if (!existing) {
|
|
61
|
+
return super.update(id, data);
|
|
62
|
+
}
|
|
63
|
+
return super.update(id, {
|
|
64
|
+
...data,
|
|
65
|
+
config: {
|
|
66
|
+
...(existing.config ?? {}),
|
|
67
|
+
...data.config,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
31
71
|
async getEnabledSections() {
|
|
32
72
|
try {
|
|
33
73
|
const snapshot = await this.db
|