@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.
Files changed (83) hide show
  1. package/dist/_internal/server/features/products/actions.js +29 -3
  2. package/dist/_internal/server/features/products/list-public.d.ts +1 -1
  3. package/dist/_internal/server/features/products/list-public.js +10 -2
  4. package/dist/_internal/server/features/stores/denormalize.d.ts +44 -0
  5. package/dist/_internal/server/features/stores/denormalize.js +58 -0
  6. package/dist/_internal/server/features/stores/index.d.ts +1 -0
  7. package/dist/_internal/server/features/stores/index.js +1 -0
  8. package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
  9. package/dist/_internal/server/jobs/core/storeNameBackfill.d.ts +27 -0
  10. package/dist/_internal/server/jobs/core/storeNameBackfill.js +90 -0
  11. package/dist/_internal/shared/features/products/to-product-item.js +7 -0
  12. package/dist/client.d.ts +1 -1
  13. package/dist/client.js +4 -1
  14. package/dist/features/about/components/PolicyPageView.js +1 -1
  15. package/dist/features/admin/actions/admin-actions.js +11 -1
  16. package/dist/features/admin/components/AdminSectionsView.js +17 -20
  17. package/dist/features/admin/components/sections/adminSectionsBuildParse.js +31 -14
  18. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +37 -7
  19. package/dist/features/admin/components/sections/adminSectionsTypes.js +11 -7
  20. package/dist/features/auctions/components/AuctionDetailPageView.js +56 -36
  21. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +9 -0
  22. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -2
  23. package/dist/features/blog/components/BlogFeaturedCard.d.ts +7 -1
  24. package/dist/features/blog/components/BlogFeaturedCard.js +3 -3
  25. package/dist/features/cart/repository/cart.repository.d.ts +27 -6
  26. package/dist/features/cart/repository/cart.repository.js +52 -21
  27. package/dist/features/cart/utils/guest-cart.js +5 -0
  28. package/dist/features/cart/utils/pending-ops.d.ts +15 -0
  29. package/dist/features/cart/utils/pending-ops.js +24 -0
  30. package/dist/features/homepage/components/BlogArticlesSection.d.ts +7 -1
  31. package/dist/features/homepage/components/BlogArticlesSection.js +5 -3
  32. package/dist/features/homepage/components/CTABannerSection.d.ts +18 -1
  33. package/dist/features/homepage/components/CTABannerSection.js +22 -4
  34. package/dist/features/homepage/components/CustomerReviewsSection.d.ts +7 -1
  35. package/dist/features/homepage/components/CustomerReviewsSection.js +2 -2
  36. package/dist/features/homepage/components/FAQSection.js +2 -11
  37. package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +3 -1
  38. package/dist/features/homepage/components/FeaturedAuctionsSection.js +5 -3
  39. package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +7 -1
  40. package/dist/features/homepage/components/FeaturedBundlesSection.js +6 -5
  41. package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +3 -1
  42. package/dist/features/homepage/components/FeaturedPreOrdersSection.js +4 -2
  43. package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -1
  44. package/dist/features/homepage/components/FeaturedProductsSection.js +5 -3
  45. package/dist/features/homepage/components/HeroCarousel.d.ts +23 -2
  46. package/dist/features/homepage/components/HeroCarousel.js +22 -9
  47. package/dist/features/homepage/components/HomepageCustomerReviewsSection.d.ts +6 -1
  48. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +11 -3
  49. package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +9 -1
  50. package/dist/features/homepage/components/MarketplaceHomepageView.js +7 -4
  51. package/dist/features/homepage/components/StatsCounterSection.d.ts +6 -1
  52. package/dist/features/homepage/components/StatsCounterSection.js +3 -3
  53. package/dist/features/homepage/constants/faq-category-labels.d.ts +15 -0
  54. package/dist/features/homepage/constants/faq-category-labels.js +23 -0
  55. package/dist/features/homepage/constants/section-copy.d.ts +107 -0
  56. package/dist/features/homepage/constants/section-copy.js +96 -0
  57. package/dist/features/homepage/lib/section-renderer.d.ts +16 -1
  58. package/dist/features/homepage/lib/section-renderer.js +58 -37
  59. package/dist/features/homepage/repository/homepage-sections.repository.d.ts +23 -0
  60. package/dist/features/homepage/repository/homepage-sections.repository.js +40 -0
  61. package/dist/features/homepage/schemas/firestore.d.ts +120 -44
  62. package/dist/features/media/MediaImage.d.ts +10 -1
  63. package/dist/features/media/MediaImage.js +6 -3
  64. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  65. package/dist/features/products/components/CardStoreLine.d.ts +32 -0
  66. package/dist/features/products/components/CardStoreLine.js +14 -0
  67. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -1
  68. package/dist/features/products/components/ProductGrid.js +3 -6
  69. package/dist/features/products/schemas/firestore.d.ts +17 -0
  70. package/dist/features/products/types/index.d.ts +2 -0
  71. package/dist/features/seller/actions/seller-actions.d.ts +17 -0
  72. package/dist/features/seller/actions/seller-actions.js +54 -0
  73. package/dist/features/stores/components/StoreAuctionsListing.js +3 -0
  74. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +137 -0
  75. package/dist/seed/carousel-slides-seed-data.js +154 -22
  76. package/dist/seed/carousels-seed-data.js +4 -1
  77. package/dist/seed/homepage-sections-seed-data.js +337 -254
  78. package/dist/seed/products-preorders-seed-data.d.ts +1 -0
  79. package/dist/server.d.ts +1 -0
  80. package/dist/server.js +4 -0
  81. package/dist/styles.css +1 -1
  82. package/dist/tailwind-utilities.css +1 -1
  83. 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
- export declare function renderSection(section: HomepageSectionDocument, adSlots: MarketplaceHomepageViewAdSlots | undefined, newsletterFormSlot: React.ReactNode, faqItems: FaqItem[], slides: CarouselSlide[], liveStats: LiveStatsMap, sectionData?: SectionData): React.ReactNode;
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
- return _jsx(HeroCarousel, { initialSlides: slides });
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) || "Discover Amazing Products", subtitle: parseWelcomeDescription(cfg?.description), pillLabel: "India's #1 Marketplace", showCTA: cfg?.showCTA ?? true, ctaLabel: cfg?.ctaText || "Shop Now", ctaHref: cfg?.ctaLink || ROUTES.PUBLIC.PRODUCTS, secondaryCtaLabel: "Browse All", secondaryCtaHref: ROUTES.PUBLIC.PRODUCTS, trustChips: [
49
- { key: "delivery", emoji: "🚀", label: "Fast Delivery" },
50
- { key: "secure", emoji: "🔒", label: "Secure Payments" },
51
- { key: "rating", emoji: "⭐", label: "4.8+ Rated" },
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) || "Shop by Category", viewMoreHref: ROUTES.PUBLIC.CATEGORIES, viewMoreLabel: "All categories \u2192", initialItems: sectionData.categories, cta: cfg?.cta, filters: cfg?.filters, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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) || "Featured Products", viewMoreHref: ROUTES.PUBLIC.PRODUCTS, viewMoreLabel: "View all products \u2192", filterByBrand: cfg?.filterByBrand, initialItems: sectionData.products, rows: cfg?.rows, maxItems: cfg?.maxItems, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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) || "Live Auctions", viewMoreHref: ROUTES.PUBLIC.AUCTIONS, viewMoreLabel: "View all auctions \u2192", filterByBrand: cfg?.filterByBrand, initialItems: sectionData.auctions, rows: cfg?.rows, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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) || "Reserve Before It Ships", viewMoreHref: ROUTES.PUBLIC.PRE_ORDERS, viewMoreLabel: "View all pre-orders \u2192", filterByBrand: cfg?.filterByBrand, initialItems: sectionData.preOrders, rows: cfg?.rows, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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) || "Featured Stores", viewMoreHref: ROUTES.PUBLIC.STORES, viewMoreLabel: "View all stores \u2192", initialItems: sectionData.stores, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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) || "Events & Offers", viewMoreHref: ROUTES.PUBLIC.EVENTS, viewMoreLabel: "View all events \u2192", initialItems: sectionData.events, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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) || "What Our Customers Say", viewMoreHref: ROUTES.PUBLIC.REVIEWS, viewMoreLabel: "See all reviews \u2192", autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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
- return (_jsx(CTABannerSection, { title: cfg?.content?.title || "Thousands of collectibles. One marketplace.", primaryLabel: "Shop All Products \u2192", primaryHref: ROUTES.PUBLIC.PRODUCTS, secondaryLabel: "Browse Auctions \u2192", secondaryHref: ROUTES.PUBLIC.AUCTIONS }));
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
- return (_jsx(TrustFeaturesSection, { title: cleanTitle(cfg?.title) || "Why Buyers Trust LetItRip", items: DEFAULT_TRUST_FEATURES }));
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
- return (_jsx(SecurityHighlightsSection, { title: cleanTitle(cfg?.title) || "Security You Can Trust", pillLabel: "Built for trust", items: DEFAULT_SECURITY_ITEMS, learnMoreHref: ROUTES.PUBLIC.SECURITY, learnMoreLabel: "Learn about our security \u2192" }));
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) || "Join Our WhatsApp Community", descriptionHtml: cfg?.description || "5,000+ members. Get deal alerts, auction updates, and exclusive drops before anyone else.", memberCount: cfg?.memberCount || 5000, groupLink: cfg?.groupLink || "https://chat.whatsapp.com/", benefits: cfg?.benefits ?? [], testimonial: cfg?.testimonial, buttonText: cfg?.buttonText }));
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) || "Frequently Asked Questions", 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: "View all FAQs \u2192", hasMore: hasMore, moreCount: totalFaqs - slicedItems.length }));
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) || "From Our Blog", viewMoreHref: ROUTES.BLOG.LIST, viewMoreLabel: "View all posts \u2192", initialItems: sectionData.blog }));
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) || "Stay Updated", subtitle: cfg?.description || "Get the latest drops, auctions, and deals in your inbox.", renderForm: () => newsletterFormSlot ?? null }));
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) || "Top Brands", subtitle: cfg?.subtitle, limit: cfg?.maxBrands || 12, viewMoreHref: ROUTES.PUBLIC.CATEGORIES, viewMoreLabel: "All brands \u2192", initialItems: sectionData.brands, cta: cfg?.cta, filters: cfg?.filters, autoScroll: cfg?.autoScroll, scrollInterval: cfg?.scrollInterval, loop: cfg?.loop }));
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) || "Curated Bundles", initialItems: sectionData.bundles }));
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