@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
@@ -9,6 +9,8 @@
9
9
  * (src/actions/seller.actions.ts) alongside the other seller server actions.
10
10
  */
11
11
  import { serverLogger } from "../../../monitoring";
12
+ import { normalizeError } from "../../../errors/normalize";
13
+ import { enqueueJob } from "../../jobs/actions/enqueue-job";
12
14
  import { isAdminUser, isStrictSellerUser, isTesterUser } from "../../auth/role-predicates";
13
15
  import { ApiError, AuthorizationError, NotFoundError, ValidationError, } from "../../../errors";
14
16
  import { userRepository } from "../../auth/repository/user.repository";
@@ -155,6 +157,29 @@ export async function updateStore(userId, input) {
155
157
  ...(vacationMessage !== undefined && { vacationMessage }),
156
158
  ...(isPublic !== undefined && { isPublic }),
157
159
  });
160
+ // A rename has to reach the products, or every card this store owns keeps
161
+ // showing the old name forever — the denormalized `storeName`/`storeSlug` on
162
+ // ProductDocument have no other writer after creation (Root Cause #42).
163
+ //
164
+ // Enqueued, not inline: the write is unbounded in the number of listings and
165
+ // would blow the 10s route ceiling (Rule #6). Best-effort — a failed enqueue
166
+ // must not fail the rename the seller just made, and the job is idempotent so
167
+ // the standalone backfill script can always catch up.
168
+ if (storeName !== undefined && storeName !== store.storeName) {
169
+ try {
170
+ await enqueueJob({
171
+ jobType: "storeNameBackfill",
172
+ payload: { storeId: store.storeSlug },
173
+ requestedBy: userId,
174
+ });
175
+ }
176
+ catch (err) {
177
+ void normalizeError(err);
178
+ serverLogger.warn("updateSellerStore: storeNameBackfill enqueue failed", {
179
+ storeId: store.storeSlug,
180
+ });
181
+ }
182
+ }
158
183
  return { store: updated };
159
184
  }
160
185
  export async function updatePayoutSettings(userId, input) {
@@ -343,13 +368,41 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
343
368
  };
344
369
  }
345
370
  // --- Create Seller Product ----------------------------------------------------
371
+ /**
372
+ * The real "create a listing" path — every seller `new` page under `/store`
373
+ * routes here.
374
+ *
375
+ * The store fields are resolved from the OWNER, never taken from the draft.
376
+ * `SellerProductDraft` has no `storeId`, `storeName` or `storeSlug` field at
377
+ * all, so this used to write `sellerId`/`sellerName`/`sellerEmail` and nothing
378
+ * else about the store — leaving every seller-created listing with no `storeId`
379
+ * whatsoever. That is not just a blank "by …" line on the card: `storeId` is
380
+ * what the store page queries on, what `splitCartIntoOrderGroups` groups on,
381
+ * and what a payout hangs off. Seed data set all three by hand, so a seeded
382
+ * environment looked entirely correct.
383
+ *
384
+ * Resolving server-side rather than accepting them from the client is also the
385
+ * only safe shape — a caller-supplied `storeId` would let a seller file a
386
+ * listing under someone else's store.
387
+ */
346
388
  export async function createSellerProduct(userId, userName, userEmail, input) {
389
+ const store = await storeRepository.findByOwnerId(userId);
390
+ // Refuse rather than write an orphan. A listing with no store is invisible on
391
+ // its own storefront, forms no order group at checkout, and cannot be paid
392
+ // out — a failed create the seller can act on beats a saved one that silently
393
+ // never appears.
394
+ if (!store) {
395
+ throw new NotFoundError("No store found for this account. Create your store first.");
396
+ }
347
397
  const finalizedData = await finalizeProductMediaReferences(input);
348
398
  await productRepository.create({
349
399
  ...finalizedData,
350
400
  sellerId: userId,
351
401
  sellerName: userName,
352
402
  sellerEmail: userEmail,
403
+ storeId: store.storeSlug,
404
+ storeName: store.storeName,
405
+ storeSlug: store.storeSlug,
353
406
  // Pass through the caller's intended status (draft vs published) instead
354
407
  // of hardcoding "draft" — this previously made every "Publish" click
355
408
  // silently save as a draft regardless of what the wizard sent.
@@ -357,6 +410,7 @@ export async function createSellerProduct(userId, userName, userEmail, input) {
357
410
  });
358
411
  serverLogger.info("createSellerProduct: product created", {
359
412
  userId,
413
+ storeId: store.storeSlug,
360
414
  status: finalizedData.status ?? "draft",
361
415
  });
362
416
  }
@@ -89,6 +89,9 @@ export function StoreAuctionsListing({ storeId, initialData }) {
89
89
  // Read by isBuyNowAvailable() on the card — without it a sold auction still
90
90
  // offers a Buyout chip.
91
91
  isSold: p.isSold,
92
+ // Renders the card's "by <seller>" line.
93
+ storeId: p.storeId,
94
+ storeName: p.storeName,
92
95
  }));
93
96
  const commitSearch = useCallback(() => {
94
97
  // table.set("q", v) already resets page to 1 — see root-cause #13.
@@ -119,6 +119,12 @@ const rawTesterChecklistItems = [
119
119
  href: "/user/settings",
120
120
  cases: [
121
121
  { key: "edit-profile", label: "Editing display name / bio saves correctly", href: "/user/profile" },
122
+ {
123
+ key: "user-pages-signed-out-redirect",
124
+ label: "Opening /user/profile in a signed-out private window shows the login redirect — never another user's data, and never a permanently-stuck spinner",
125
+ description: "Changed 2026-09-02. BEFORE: every /user/* route was server-rendered per request because of a `force-dynamic` export. AFTER: those 25 routes are prerendered to a static shell and the session hydrates client-side. The shell was verified to contain no uid, email, displayName or storeId — this case is the human confirmation of that, plus the UX question it raises. In a private window: open /user/profile, /user/orders and /user/bids. Each must show chrome plus a brief loading state and then redirect to login. Two distinct failures to watch for: (a) ANY trace of a real account (a name, an email, an order) — that would be a serious leak, fail immediately; (b) a spinner that never resolves. A short flash before the redirect is expected and is the tradeoff being evaluated — note in your comment whether it feels acceptable.",
126
+ href: "/user/profile",
127
+ },
122
128
  { key: "avatar-upload", label: "Uploading a profile avatar works" },
123
129
  { key: "notification-prefs", label: "Notification preferences save correctly" },
124
130
  { key: "public-profile-toggle", label: "Public profile visibility toggle works" },
@@ -174,6 +180,12 @@ const rawTesterChecklistItems = [
174
180
  { key: "browse-categories", label: "Browsing categories shows relevant products", href: "/categories" },
175
181
  { key: "search", label: "Search returns relevant results", href: "/search" },
176
182
  { key: "filters", label: "Search/listing filters (price, brand, condition) work correctly" },
183
+ {
184
+ key: "card-shows-seller-name",
185
+ label: "Every listing card shows a \"by <seller>\" line",
186
+ description: "BEFORE: only the standard product card rendered a seller, and it read the value off the product document — which no seller-facing create path ever populated, so it was blank on every real listing while seeded demo products looked correct. Auction, pre-order and prize-draw cards had no seller line at all. AFTER: check /products in BOTH grid and list view, then /auctions, /pre-orders and /prize-draws — every card must carry a \"by …\" line naming the store. The name must be a readable store name (e.g. \"Beyblade Arena\"), never a raw slug like \"store-beyblade-arena\".",
187
+ href: "/products",
188
+ },
177
189
  {
178
190
  key: "grid-one-card-per-row-on-phone",
179
191
  label: "On a phone, a product listing shows exactly ONE card per row — not two narrow ones",
@@ -848,6 +860,30 @@ const rawTesterChecklistItems = [
848
860
  href: "/cart",
849
861
  cases: [
850
862
  { key: "update-qty", label: "Updating item quantity in cart recalculates the total" },
863
+ // ── Clearing the cart ────────────────────────────────────────────
864
+ // These are written as explicit before → after because the bug returned
865
+ // HTTP 200 and a "Cart cleared (N items)" toast over a cart that was
866
+ // not cleared. A case reading "check Remove all works" passes against it.
867
+ {
868
+ key: "remove-all-stays-empty",
869
+ label: "\"Remove all\" empties the cart and it STAYS empty for at least a minute",
870
+ description: "BEFORE: \"Remove all\" looped a DELETE per row and never drained the local pending-op queue, so cleared rows re-appeared on the next render AND the background sync re-POSTed them to the server within 30s — the items came back permanently. AFTER: add 3 items using the quick-add button on the /products GRID (that is the path that queues pending ops — adding from a product detail page does not), open /cart, click \"Remove all\" and confirm the dialog. The cart must be empty, then WAIT 60 SECONDS without navigating and confirm nothing reappears. Reload the page and confirm it is still empty.",
871
+ },
872
+ {
873
+ key: "remove-all-confirmation",
874
+ label: "\"Remove all\" asks \"Clear your cart?\" before doing anything",
875
+ description: "BEFORE: the button fired immediately with no confirmation, even though the confirmation copy was already defined in the action registry. AFTER: clicking \"Remove all\" opens a dialog titled \"Clear your cart?\"; pressing Cancel must leave every item in place.",
876
+ },
877
+ {
878
+ key: "remove-all-badge-zero",
879
+ label: "The header cart badge reads 0 after clearing, not a leftover count",
880
+ description: "BEFORE: the badge showed the server count PLUS the undrained pending-op queue, so it sat at a non-zero number over a visibly empty cart. AFTER: after \"Remove all\", the cart icon in the header must show no badge (or 0) immediately — without a page reload.",
881
+ },
882
+ {
883
+ key: "remove-all-keeps-won-auction",
884
+ label: "\"Remove all\" keeps a won-auction line and SAYS that it did",
885
+ description: "Needs a won auction in the cart (see the Auctions section). BEFORE: the won line was included in the delete loop, the server refused it, the failure was swallowed, and the toast still claimed every item was cleared. AFTER: the won-auction line must remain in the cart and the toast must read \"… N items awaiting payment kept.\" — the count in the first half must NOT include the kept line.",
886
+ },
851
887
  // ── Grouped & bundle cart lines ──────────────────────────────────
852
888
  // Fixtures backing these: product-tester-standard-1/2/3 (three members
853
889
  // of one group, deliberately spanning two GST rates),
@@ -1918,6 +1954,18 @@ const rawTesterChecklistItems = [
1918
1954
  { key: "list-auction", label: "Listing an auction works" },
1919
1955
  { key: "list-preorder", label: "Listing a pre-order works" },
1920
1956
  { key: "edit-listing", label: "Editing an existing listing works" },
1957
+ {
1958
+ key: "new-listing-belongs-to-store",
1959
+ label: "A listing created through the wizard appears on your OWN storefront and shows your store name on its card",
1960
+ description: "BEFORE: the create path wrote sellerId/sellerName/sellerEmail and no storeId at all — the wizard's draft has no store field — so a brand-new listing belonged to no store: invisible on the seller's own storefront, showing no seller on its card, and forming no order group at checkout. Seed data set all three fields by hand, which is why demo data looked right. AFTER: publish a new product, then open your public store page (/stores/<your-slug>) and confirm it is listed there, and open /products and confirm its card reads \"by <your store name>\".",
1961
+ href: "/store/products/new",
1962
+ },
1963
+ {
1964
+ key: "store-rename-updates-cards",
1965
+ label: "Renaming your store updates the seller name on all your existing listing cards",
1966
+ description: "BEFORE: the store name is copied onto each product when it is created, and nothing re-synced it — so a rename left every existing card showing the old name forever, with no error anywhere. AFTER: note your current store name, rename the store in Storefront settings, then open /products and find one of your OLDER listings. Its card must show the NEW name. The re-sync runs as a background job, so allow up to a minute and reload before judging.",
1967
+ href: "/store/storefront",
1968
+ },
1921
1969
  { key: "media-upload", label: "Uploading product images/video during listing works" },
1922
1970
  {
1923
1971
  key: "media-upload-preview-no-white-box",
@@ -3042,6 +3090,83 @@ const rawTesterChecklistItems = [
3042
3090
  label: "The promotion/announcement banner overlays the top of the first homepage section (translucent strip) instead of pushing the page content down with extra vertical space",
3043
3091
  href: "/",
3044
3092
  },
3093
+ // --- Homepage rework, 2026-09-01 -----------------------------------
3094
+ // Every case below is written as an explicit BEFORE → AFTER. Almost
3095
+ // every defect this rework fixed returned HTTP 200 with plausible
3096
+ // content, so a case reading "check the hero works" would have passed
3097
+ // against the bug.
3098
+ {
3099
+ key: "hero-slides-have-copy",
3100
+ label: "Every hero carousel slide shows a headline, a supporting line and a working CTA button on top of its background image",
3101
+ description: "BEFORE: all five active slides rendered as bare background images with NO text and NO button — HeroCarousel only draws copy when the slide has an `overlay` (or `cards`), and no seeded slide had either. AFTER: four slides carry an overlay and the first carries a three-card grid. A slide showing only a photo is a regression. Also confirm no slide shows BOTH a big centred headline AND a row of cards — those two layers stack on the same pixels and overlap.",
3102
+ href: "/",
3103
+ },
3104
+ {
3105
+ key: "homepage-single-h1",
3106
+ label: "The homepage has exactly ONE <h1> — the welcome hero headline. Carousel slide headlines are <h2>.",
3107
+ description: "Open DevTools → Console and run: document.querySelectorAll('h1').length — it must be 1. BEFORE: the welcome hero AND every carousel slide overlay each claimed <h1>, so the page had as many h1s as it had slides with titles. Run it again after clicking through all the slides; the count must stay 1.",
3108
+ href: "/",
3109
+ },
3110
+ {
3111
+ key: "homepage-section-order",
3112
+ label: "Homepage section order reads: welcome hero → promo carousel → trust bar → Shop by Category → Featured Products → Live Auctions → Pre-Orders → Bundles → Stores → Reviews, with stats / security / FAQ near the BOTTOM",
3113
+ description: "BEFORE: the platform stats band sat third (before a visitor had seen a single product) and the FAQ sat mid-page. AFTER the order follows marketplace-homepage research: orient, then shop, then reassure.",
3114
+ href: "/",
3115
+ },
3116
+ {
3117
+ key: "homepage-no-franchise-specific-strips",
3118
+ label: "The homepage has no brand-specific product strip, and no section copy names a single franchise",
3119
+ description: "BEFORE: two 'Takara-Tomy' / 'Beyblade' product rows plus a Beyblade banner. Both rows filtered the same shared 12-item featured set client-side, so they usually rendered EMPTY as well as being off-brand. AFTER: copy is catalogue-neutral throughout. NOTE: seeding does not delete documents — if either brand strip still appears, delete `section-brand-takara-tomy` and `section-brand-beyblade` at /admin/sections.",
3120
+ href: "/",
3121
+ },
3122
+ {
3123
+ key: "section-config-actually-renders",
3124
+ label: "A section's Subtitle and 'View all link label' set in /admin/sections actually appear on the homepage",
3125
+ description: "Edit Featured Products at /admin/sections: set Subtitle to 'TEST SUBTITLE' and View all link label to 'TEST LINK', save, then reload the homepage. BEFORE: the subtitle was configurable and never rendered, and every 'View all →' label was hardcoded in code, so neither change appeared. AFTER both must show.",
3126
+ href: "/admin/sections",
3127
+ },
3128
+ {
3129
+ key: "section-save-preserves-other-fields",
3130
+ label: "Saving one field on a homepage section does NOT blank that section's other settings",
3131
+ description: "At /admin/sections open Featured Products, note Max items and Sort by, change ONLY the Title, save, reload the page and re-open the same section. BEFORE: saving wrote the whole config map, so every field the builder has no input for was silently deleted — HTTP 200, no error. AFTER: every other value must be exactly as it was.",
3132
+ href: "/admin/sections",
3133
+ },
3134
+ {
3135
+ key: "carousel-toggles-take-effect",
3136
+ label: "Turning the hero carousel's dots / arrows / pause-on-hover off in /admin/sections actually turns them off on the homepage",
3137
+ description: "BEFORE: the entire carousel config was dropped by the renderer — dots, arrows, pause-on-hover, height and autoplay delay were all hardcoded on, so every one of these switches did nothing. AFTER each must take effect. Keyboard focus still pauses the carousel even with pause-on-hover OFF — that is deliberate (WCAG 2.2.2), not a bug.",
3138
+ href: "/admin/sections",
3139
+ },
3140
+ {
3141
+ key: "banner-buttons-from-config",
3142
+ label: "The seller CTA banner shows its two configured buttons ('Start selling →' and 'Browse the marketplace') and both navigate correctly",
3143
+ description: "BEFORE: the banner read ONLY `content.title` from config — its background image, subtitle, description, height and both buttons were ignored, and the CTA labels/links were hardcoded to Shop All Products / Browse Auctions regardless of what an admin configured. AFTER the configured buttons must render, and 'Start selling →' must land on /seller-guide.",
3144
+ href: "/",
3145
+ },
3146
+ {
3147
+ key: "trust-and-security-from-config",
3148
+ label: "The trust bar and the 'Security You Can Trust' section show the four seeded items each, not a generic built-in list",
3149
+ description: "Trust bar: Authenticity checked / Escrow-backed payments / Straightforward returns / Real human support. Security: Escrow on every order / Verified sellers only / Public scam registry / Encrypted personal data. BEFORE: both sections ignored their configured items entirely and always rendered a hardcoded fallback list.",
3150
+ href: "/",
3151
+ },
3152
+ {
3153
+ key: "homepage-newsletter-enter-key",
3154
+ label: "Typing an email into the homepage newsletter box and pressing ENTER subscribes — you do not have to click the button",
3155
+ description: "BEFORE: Enter did nothing at all. The Subscribe button carried both type=submit and an onClick, while the form preventDefault'd the native submit — so Enter fired `submit` (which was swallowed) and never the click handler. Mouse worked, keyboard did not.",
3156
+ href: "/",
3157
+ },
3158
+ {
3159
+ key: "footer-newsletter-inline-error",
3160
+ label: "Typing `not-an-email` into the FOOTER newsletter box and submitting shows an inline error under the field",
3161
+ description: "BEFORE: the footer form declared a validation schema but never ran it — the only check was 'not empty' — so `not-an-email` was sent to the server and came back as a toast. AFTER the error must appear inline, on the field, with no network request.",
3162
+ href: "/",
3163
+ },
3164
+ {
3165
+ key: "homepage-faq-structured-data",
3166
+ label: "The homepage HTML contains FAQPage structured data matching the FAQs actually shown in the FAQ section",
3167
+ description: "View source on / and search for `application/ld+json`. There must be a block with \"@type\":\"FAQPage\" whose questions match the ones rendered on the page. BEFORE: the homepage rendered an FAQ strip and emitted no page-level structured data at all. There must NOT be an ItemList/Carousel block — Google restricts those to category pages.",
3168
+ href: "/",
3169
+ },
3045
3170
  ],
3046
3171
  },
3047
3172
  {
@@ -3860,6 +3985,18 @@ const rawTesterChecklistItems = [
3860
3985
  href: "/blog/spot-genuine-takara-tomy-beyblade",
3861
3986
  },
3862
3987
  { 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" },
3988
+ {
3989
+ key: "maintenance-error-lists-have-rows",
3990
+ label: "/admin/maintenance/server-errors lists actual error rows — it is NOT empty",
3991
+ description: "Found 2026-09-02. BEFORE: the three composite indexes behind these pages were declared `occurredAt ASCENDING` while the query orders `desc`, so every one of them returned `9 FAILED_PRECONDITION` and the page 500'd. The pages carried `force-dynamic`, so the build never exercised them and nobody noticed. AFTER: the index direction is corrected AND the read is wrapped in safeRead. 🛑 That combination means a still-undeployed index now looks like an EMPTY LIST rather than an error — so \"the page loads\" is NOT a pass here. Trigger a real error first (hit any admin API with a bad payload, or open a 404'd route), then reload /admin/maintenance/server-errors and confirm at least one row appears. Repeat on /client-errors and /payment-rollbacks. If all three are empty while errors are definitely being recorded, the indexes have not been deployed.",
3992
+ href: "/admin/maintenance/server-errors",
3993
+ },
3994
+ {
3995
+ key: "maintenance-cloud-logs-degrades",
3996
+ label: "/admin/maintenance/cloud-logs renders the page shell even when Google Cloud Logging is unreachable",
3997
+ description: "Found 2026-09-02: this is the only page in the app calling a Google Cloud API rather than Firestore, and the call was unguarded — a permission or outage error took the whole page down (and failed the production build). It now degrades to an empty list and records a DEGRADED_READ. Confirm the page renders its heading and empty state rather than an error boundary.",
3998
+ href: "/admin/maintenance/cloud-logs",
3999
+ },
3863
4000
  { key: "copilot-admin", label: "Admin copilot page works correctly", href: "/admin/copilot" },
3864
4001
  { key: "team-admin", label: "Admin team page works correctly", href: "/admin/team" },
3865
4002
  {
@@ -1,7 +1,23 @@
1
1
  /*
2
- * WHY: Seeds 6 carousel slides for the marketplace homepage hero carousel.
3
- * WHAT: 5 active + 1 inactive. Picsum.photos placeholder backgrounds (deterministic,
4
- * free, no API key) with dim overlay, CTAs for shop/auctions/bundles/pre-orders/graded.
2
+ * WHY: Seeds 6 carousel slides for the marketplace homepage promo carousel.
3
+ * WHAT: 5 active + 1 inactive. Deterministic Picsum backgrounds with a dim
4
+ * overlay, plus a headline / supporting line / CTA on each.
5
+ *
6
+ * 🛑 EVERY ACTIVE SLIDE MUST CARRY AN `overlay` OR `cards`, OR IT RENDERS BLANK.
7
+ * `HeroCarousel` gates all slide copy on `slide.overlay` (line ~412) and the
8
+ * card grid on `slide.cards.length` (line ~451). Before this pass every seeded
9
+ * slide had NEITHER — no overlay, `cards: []` — so all five active slides
10
+ * rendered as bare background images with no headline and no call to action.
11
+ * The `title` field is admin-list metadata; it is not rendered on the slide.
12
+ *
13
+ * 🛑 NEVER give one slide BOTH an overlay and cards. The two containers are
14
+ * sibling `POSITION_FILL` elements, so they stack on the same pixels and the
15
+ * text overlaps. `slide-hero-homepage` is the cards slide and deliberately has
16
+ * no overlay; the other four are overlay-only.
17
+ *
18
+ * Overlay titles render as <h2> — the welcome section above the carousel owns
19
+ * the page's single <h1>.
20
+ *
5
21
  * One slide (slide-beyblade-auction) uses background.type:"video" with a direct,
6
22
  * Creative-Commons-licensed MP4 URL — HeroCarousel renders video backgrounds via a
7
23
  * native <video> element (MediaVideo), not a YouTube iframe embed, so this needs a
@@ -21,25 +37,73 @@ import { seedPhoto } from "./_helpers/media";
21
37
  import { ROUTES } from "../next/routing/route-map";
22
38
  const NOW = new Date();
23
39
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
40
+ // "medium" matches the carousel section's own config: the carousel now sits
41
+ // under a full welcome hero, so a viewport-height slide would push the whole
42
+ // page below the fold.
43
+ const SLIDE_SETTINGS = { autoplayDelayMs: 6000, height: "medium" };
24
44
  const _rawSlides = [
25
45
  {
26
46
  id: "slide-hero-homepage",
27
- title: "India's Largest Collectibles Marketplace",
47
+ title: "Three ways to shop",
28
48
  order: 1,
29
49
  active: true,
30
50
  background: {
31
51
  type: "image",
32
52
  url: seedPhoto("carousel-hero-homepage-20260101", 1600, 700),
33
- dimOverlay: { enabled: true, opacity: 0.5 },
53
+ dimOverlay: { enabled: true, opacity: 0.55 },
34
54
  },
35
- settings: { autoplayDelayMs: 5000, height: "tall" },
55
+ // The ONE cards slide — and therefore the one slide with no `overlay`.
56
+ // Exists so the zone-grid render path has real data to exercise; all three
57
+ // collapse to zone 2 on mobile.
58
+ cards: [
59
+ {
60
+ id: "card-hero-shop-now",
61
+ zone: 1,
62
+ mobileZone: 2,
63
+ background: { type: "image", url: seedPhoto("carousel-card-shop-now", 600, 600), dimOverlay: { enabled: true, opacity: 0.45 } },
64
+ content: {
65
+ eyebrow: "Buy now",
66
+ title: "Ready to ship",
67
+ description: "In-stock listings from verified sellers.",
68
+ },
69
+ buttons: [{ id: "card-hero-shop-now-cta", text: "Shop products", href: String(ROUTES.PUBLIC.PRODUCTS), variant: "primary" }],
70
+ hover: { effect: "scale" },
71
+ },
72
+ {
73
+ id: "card-hero-bid",
74
+ zone: 2,
75
+ mobileZone: 2,
76
+ background: { type: "image", url: seedPhoto("carousel-card-bid", 600, 600), dimOverlay: { enabled: true, opacity: 0.45 } },
77
+ content: {
78
+ eyebrow: "Bid",
79
+ title: "Live auctions",
80
+ description: "Place a bid before the timer runs out.",
81
+ },
82
+ buttons: [{ id: "card-hero-bid-cta", text: "Browse auctions", href: String(ROUTES.PUBLIC.AUCTIONS), variant: "primary" }],
83
+ hover: { effect: "scale" },
84
+ },
85
+ {
86
+ id: "card-hero-reserve",
87
+ zone: 3,
88
+ mobileZone: 2,
89
+ background: { type: "image", url: seedPhoto("carousel-card-reserve", 600, 600), dimOverlay: { enabled: true, opacity: 0.45 } },
90
+ content: {
91
+ eyebrow: "Reserve",
92
+ title: "Pre-orders",
93
+ description: "Lock in upcoming releases with a deposit.",
94
+ },
95
+ buttons: [{ id: "card-hero-reserve-cta", text: "See pre-orders", href: String(ROUTES.PUBLIC.PRE_ORDERS), variant: "primary" }],
96
+ hover: { effect: "scale" },
97
+ },
98
+ ],
99
+ settings: SLIDE_SETTINGS,
36
100
  createdBy: "user-admin-letitrip",
37
101
  createdAt: daysAgo(60),
38
102
  updatedAt: daysAgo(3),
39
103
  },
40
104
  {
41
105
  id: "slide-beyblade-auction",
42
- title: "Rare Auctions Live Now",
106
+ title: "Live auctions",
43
107
  order: 2,
44
108
  active: true,
45
109
  background: {
@@ -50,65 +114,120 @@ const _rawSlides = [
50
114
  // video client-side instead, so the raw external URL is correct here.
51
115
  url: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", // audit-seed-external-url-ok: raw <video> src, /api/media/ext is image-only (Root Cause #27)
52
116
  thumbnail: seedPhoto("carousel-auctions-live-20260101", 1600, 700),
53
- dimOverlay: { enabled: true, opacity: 0.4 },
117
+ dimOverlay: { enabled: true, opacity: 0.5 },
118
+ },
119
+ overlay: {
120
+ enabled: true,
121
+ subtitle: "Bidding open now",
122
+ title: "Win it at your price",
123
+ description: "Live auctions close every day. Place a bid, set your maximum, and let it ride.",
124
+ button: {
125
+ id: "slide-auctions-cta",
126
+ text: "Browse live auctions",
127
+ link: String(ROUTES.PUBLIC.AUCTIONS),
128
+ variant: "primary",
129
+ openInNewTab: false,
130
+ },
54
131
  },
55
132
  link: { url: String(ROUTES.PUBLIC.AUCTIONS), openInNewTab: false },
56
- settings: { autoplayDelayMs: 5000, height: "tall" },
133
+ settings: SLIDE_SETTINGS,
57
134
  createdBy: "user-admin-letitrip",
58
135
  createdAt: daysAgo(55),
59
136
  updatedAt: daysAgo(2),
60
137
  },
61
138
  {
62
139
  id: "slide-beyblade-collection-hunt",
63
- title: "Complete Your Collection",
140
+ title: "Complete your collection",
64
141
  order: 3,
65
142
  active: true,
66
143
  background: {
67
144
  type: "image",
68
145
  url: seedPhoto("carousel-complete-collection-20260101", 1600, 700),
69
- dimOverlay: { enabled: true, opacity: 0.45 },
146
+ dimOverlay: { enabled: true, opacity: 0.5 },
147
+ },
148
+ overlay: {
149
+ enabled: true,
150
+ subtitle: "Thousands of listings",
151
+ title: "Find the piece you're missing",
152
+ description: "Filter by category, condition and price across every seller on the marketplace.",
153
+ button: {
154
+ id: "slide-collection-cta",
155
+ text: "Start browsing",
156
+ link: String(ROUTES.PUBLIC.PRODUCTS),
157
+ variant: "primary",
158
+ openInNewTab: false,
159
+ },
70
160
  },
71
161
  link: { url: String(ROUTES.PUBLIC.PRODUCTS), openInNewTab: false },
72
- settings: { autoplayDelayMs: 5000, height: "tall" },
162
+ settings: SLIDE_SETTINGS,
73
163
  createdBy: "user-admin-letitrip",
74
164
  createdAt: daysAgo(50),
75
165
  updatedAt: daysAgo(5),
76
166
  },
77
167
  {
78
168
  id: "slide-preorders-open",
79
- title: "Pre-Orders Now Open",
169
+ title: "Pre-orders open",
80
170
  order: 4,
81
171
  active: true,
82
172
  background: {
83
173
  type: "image",
84
174
  url: seedPhoto("carousel-preorders-open-20260101", 1600, 700),
85
- dimOverlay: { enabled: true, opacity: 0.4 },
175
+ dimOverlay: { enabled: true, opacity: 0.5 },
176
+ },
177
+ overlay: {
178
+ enabled: true,
179
+ subtitle: "Reserve before release",
180
+ title: "Don't miss the drop",
181
+ description: "Secure upcoming releases with a deposit and pay the balance when they ship.",
182
+ button: {
183
+ id: "slide-preorders-cta",
184
+ text: "See open pre-orders",
185
+ link: String(ROUTES.PUBLIC.PRE_ORDERS),
186
+ variant: "primary",
187
+ openInNewTab: false,
188
+ },
86
189
  },
87
190
  link: { url: String(ROUTES.PUBLIC.PRE_ORDERS), openInNewTab: false },
88
- settings: { autoplayDelayMs: 5000, height: "tall" },
191
+ settings: SLIDE_SETTINGS,
89
192
  createdBy: "user-admin-letitrip",
90
193
  createdAt: daysAgo(45),
91
- updatedAt: daysAgo(4),
194
+ updatedAt: daysAgo(3),
92
195
  },
93
196
  {
94
197
  id: "slide-authenticated-originals",
95
- title: "Authenticated Takara-Tomy Originals",
198
+ title: "Sell on the marketplace",
96
199
  order: 5,
97
200
  active: true,
98
201
  background: {
99
202
  type: "image",
100
203
  url: seedPhoto("carousel-authenticated-originals-20260101", 1600, 700),
101
- dimOverlay: { enabled: true, opacity: 0.5 },
204
+ dimOverlay: { enabled: true, opacity: 0.55 },
205
+ },
206
+ overlay: {
207
+ enabled: true,
208
+ subtitle: "For sellers",
209
+ title: "Turn your collection into income",
210
+ description: "Open a store in minutes, list for free, and get paid on a predictable schedule.",
211
+ button: {
212
+ id: "slide-sell-cta",
213
+ text: "Start selling",
214
+ link: String(ROUTES.PUBLIC.SELLER_GUIDE),
215
+ variant: "primary",
216
+ openInNewTab: false,
217
+ },
102
218
  },
103
- link: { url: String(ROUTES.PUBLIC.CATEGORY_DETAIL("category-spinning-tops")), openInNewTab: false },
104
- settings: { autoplayDelayMs: 5000, height: "tall" },
219
+ link: { url: String(ROUTES.PUBLIC.SELLER_GUIDE), openInNewTab: false },
220
+ settings: SLIDE_SETTINGS,
105
221
  createdBy: "user-admin-letitrip",
106
222
  createdAt: daysAgo(40),
107
223
  updatedAt: daysAgo(3),
108
224
  },
225
+ // Inactive on purpose — exercises the active/inactive filter in
226
+ // `getActiveSlides()`. It carries a full overlay so that enabling it in
227
+ // admin produces a finished slide rather than a blank one.
109
228
  {
110
229
  id: "slide-upcoming-promo",
111
- title: "Upcoming Tournament Season Promo",
230
+ title: "Seasonal promo (draft)",
112
231
  order: 6,
113
232
  active: false,
114
233
  background: {
@@ -116,7 +235,20 @@ const _rawSlides = [
116
235
  url: seedPhoto("carousel-tournament-promo-20260101", 1600, 700),
117
236
  dimOverlay: { enabled: true, opacity: 0.5 },
118
237
  },
119
- settings: { autoplayDelayMs: 5000, height: "tall" },
238
+ overlay: {
239
+ enabled: true,
240
+ subtitle: "Coming soon",
241
+ title: "Season sale",
242
+ description: "A draft slide kept unpublished so admins have a starting point to copy.",
243
+ button: {
244
+ id: "slide-promo-cta",
245
+ text: "See all events",
246
+ link: String(ROUTES.PUBLIC.EVENTS),
247
+ variant: "primary",
248
+ openInNewTab: false,
249
+ },
250
+ },
251
+ settings: SLIDE_SETTINGS,
120
252
  createdBy: "user-admin-letitrip",
121
253
  createdAt: daysAgo(10),
122
254
  updatedAt: daysAgo(10),
@@ -24,7 +24,10 @@ export const carouselsSeedData = [
24
24
  "slide-beyblade-auction",
25
25
  "slide-beyblade-collection-hunt",
26
26
  "slide-preorders-open",
27
- "slide-psa-graded",
27
+ // Was "slide-psa-graded", which has never existed in
28
+ // carousel-slides-seed-data.ts. `getCarouselWithSlides` filters missing
29
+ // refs away, so it failed silently rather than erroring.
30
+ "slide-authenticated-originals",
28
31
  ],
29
32
  createdBy: "user-admin-letitrip",
30
33
  createdAt: daysAgo(90),