@mohasinac/appkit 4.64.0 → 4.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.js +18 -1
- package/dist/_internal/server/features/search/index-build/sources.js +27 -12
- package/dist/_internal/server/features/seo/sitemap.js +78 -12
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +16 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/features/stores/tabs.d.ts +19 -15
- package/dist/_internal/server/features/stores/tabs.js +20 -21
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +9 -4
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +22 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +0 -1
- package/dist/_internal/server/jobs/handlers/onCatalogueSubmittedForApproval.js +1 -1
- package/dist/_internal/shared/features/site-settings/types.d.ts +10 -2
- package/dist/_internal/shared/features/sticker-trace/trace/segment.js +13 -2
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +54 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +72 -0
- package/dist/client.d.ts +5 -2
- package/dist/client.js +8 -1
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +14 -0
- package/dist/features/account/api/[userId]/addresses/route.js +1 -5
- package/dist/features/addresses/repository/addresses.repository.js +1 -1
- package/dist/features/admin/api/bids/route.js +1 -5
- package/dist/features/admin/api/coupons/route.js +1 -5
- package/dist/features/admin/api/products/route.js +1 -5
- package/dist/features/admin/api/reviews/route.js +1 -5
- package/dist/features/admin/components/AdminCarouselEditorView.js +0 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -2
- package/dist/features/admin/components/AdminGuideHubView.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +4 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +19 -3
- package/dist/features/admin/components/OwnerQuickActions.d.ts +7 -1
- package/dist/features/admin/components/OwnerQuickActions.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +0 -1
- package/dist/features/admin/schemas/firestore.d.ts +52 -0
- package/dist/features/admin/schemas/firestore.js +20 -0
- package/dist/features/auctions/api/route.js +1 -8
- package/dist/features/auctions/components/AuctionDetailPageView.js +1 -7
- package/dist/features/auth/hooks/useAuth.js +1 -1
- package/dist/features/before-after/api/route.js +1 -5
- package/dist/features/blog/api/route.js +1 -8
- package/dist/features/blog/components/BlogIndexPageView.d.ts +2 -3
- package/dist/features/blog/components/BlogIndexPageView.js +6 -9
- package/dist/features/cart/actions/cart-actions.js +1 -1
- package/dist/features/cart/api/route.js +1 -5
- package/dist/features/cart/utils/guest-cart.js +1 -5
- package/dist/features/catalogue/repository/catalogue.repository.js +1 -1
- package/dist/features/categories/api/route.js +5 -23
- package/dist/features/categories/components/BundlesListView.d.ts +2 -3
- package/dist/features/categories/components/BundlesListView.js +2 -5
- package/dist/features/categories/components/CategoriesIndexListing.js +47 -5
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +2 -3
- package/dist/features/categories/components/CategoriesIndexPageView.js +0 -32
- package/dist/features/categories/constants/category-tabs.d.ts +1 -1
- package/dist/features/categories/constants/category-tabs.js +7 -0
- package/dist/features/categories/hooks/useCategories.d.ts +6 -0
- package/dist/features/categories/hooks/useCategories.js +73 -2
- package/dist/features/categories/types/index.d.ts +1 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -8
- package/dist/features/collections/api/route.js +1 -5
- package/dist/features/consultation/api/route.js +1 -5
- package/dist/features/contact/email.js +2 -5
- package/dist/features/corporate/api/route.js +1 -5
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +1 -7
- package/dist/features/events/components/AdminEventEditorView.js +1 -1
- package/dist/features/events/components/EventsListPageView.d.ts +2 -3
- package/dist/features/events/components/EventsListPageView.js +8 -11
- package/dist/features/faq/constants/help-guides.d.ts +8 -5
- package/dist/features/faq/constants/help-guides.js +9 -5
- package/dist/features/filters/AsyncFacetSection.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +5 -5
- package/dist/features/filters/RangeFilter.js +2 -2
- package/dist/features/filters/SwitchFilter.js +2 -2
- package/dist/features/filters/filterUtils.d.ts +0 -2
- package/dist/features/filters/filterUtils.js +0 -4
- package/dist/features/filters/index.d.ts +1 -1
- package/dist/features/filters/index.js +1 -1
- package/dist/features/forms/Checkbox.js +4 -3
- package/dist/features/forms/Input.js +5 -4
- package/dist/features/forms/Radio.js +6 -5
- package/dist/features/forms/Select.js +3 -2
- package/dist/features/forms/Slider.js +3 -2
- package/dist/features/forms/Textarea.js +3 -2
- package/dist/features/forms/Toggle.js +3 -3
- package/dist/features/forms/index.d.ts +1 -1
- package/dist/features/forms/index.js +1 -1
- package/dist/features/forms/utils.d.ts +0 -2
- package/dist/features/forms/utils.js +0 -4
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +6 -6
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- package/dist/features/history/utils/guest-history.js +1 -5
- package/dist/features/homepage/components/CustomCardsSection.js +8 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +6 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +23 -1
- package/dist/features/layout/AppLayoutShell.js +13 -12
- package/dist/features/layout/BottomNavItem.d.ts +9 -1
- package/dist/features/layout/BottomNavItem.js +18 -4
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +1 -7
- package/dist/features/media/MediaLightbox.js +3 -5
- package/dist/features/orders/components/OrderCompletionPanel.d.ts +1 -1
- package/dist/features/orders/components/OrderCompletionPanel.js +1 -1
- package/dist/features/orders/components/RaiseDisputeForm.js +1 -1
- package/dist/features/orders/components/RefundRequestView.js +1 -1
- package/dist/features/parts/schemas/firestore.d.ts +274 -0
- package/dist/features/parts/schemas/firestore.js +19 -0
- package/dist/features/payments/repository/saved-payment-methods.repository.js +1 -1
- package/dist/features/pre-orders/api/admin/route.js +1 -5
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -7
- package/dist/features/products/api/route.js +1 -3
- package/dist/features/products/components/CustomSectionTabContent.js +0 -5
- package/dist/features/products/components/GroupMemberPicker.js +2 -2
- package/dist/features/products/components/GroupSettingsPanel.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -7
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +1 -7
- package/dist/features/products/repository/products.repository.js +0 -1
- package/dist/features/promotions/api/route.js +1 -8
- package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +2 -3
- package/dist/features/reviews/components/ReviewsIndexPageView.js +9 -12
- package/dist/features/seller/api/coupons/route.js +1 -5
- package/dist/features/seller/api/products/route.js +1 -5
- package/dist/features/seller/components/SellerProductShell.js +1 -2
- package/dist/features/seller/hooks/useSellerListingData.js +0 -30
- package/dist/features/stores/api/[storeSlug]/auctions/route.js +1 -10
- package/dist/features/stores/api/[storeSlug]/products/route.js +1 -10
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -1
- package/dist/features/stores/components/StoreGuideHubView.js +1 -1
- package/dist/features/stores/components/StoresIndexPageView.d.ts +2 -3
- package/dist/features/stores/components/StoresIndexPageView.js +6 -9
- package/dist/features/stores/schemas/firestore.d.ts +27 -0
- package/dist/features/stores/schemas/firestore.js +2 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +1 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.js +28 -1
- package/dist/providers/db-firebase/admin-app-lite.js +14 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/react/hooks/useBrowsePageFlags.d.ts +26 -0
- package/dist/react/hooks/useBrowsePageFlags.js +18 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/security/csp.d.ts +32 -2
- package/dist/security/csp.js +32 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.js +1 -1
- package/dist/ui/components/Chart.js +0 -1
- package/dist/ui/rich-text/RichText.js +1 -3
- package/dist/ui/rich-text/RichTextRenderer.js +1 -3
- package/dist/utils/browser-storage.d.ts +14 -0
- package/dist/utils/browser-storage.js +18 -0
- package/dist/utils/classnames.d.ts +19 -0
- package/dist/utils/classnames.js +21 -0
- package/dist/utils/search-params.d.ts +44 -0
- package/dist/utils/search-params.js +46 -0
- package/dist/utils/sieve-builder.d.ts +13 -0
- package/dist/utils/sieve-builder.js +22 -0
- package/dist/utils/string.formatter.d.ts +12 -0
- package/dist/utils/string.formatter.js +17 -0
- package/package.json +2 -2
|
@@ -548,7 +548,252 @@ export interface PartGlossaryDocument extends BaseDocument, AdminEditableRecord
|
|
|
548
548
|
imageSourceUrl?: string;
|
|
549
549
|
/** "Read more on the Beyblade Wiki". Facts taken, prose theirs, link out. */
|
|
550
550
|
sourceUrl?: string;
|
|
551
|
+
/**
|
|
552
|
+
* Hand-written analysis, read from the sources one page at a time.
|
|
553
|
+
*
|
|
554
|
+
* 🛑 ONE NESTED OBJECT, not twenty top-level fields, and the reason is the
|
|
555
|
+
* merge rather than tidiness. Everything else on this document is EXTRACTED
|
|
556
|
+
* and is regenerated wholesale by `generate-parts-seed.mjs`; this is
|
|
557
|
+
* AUTHORED and must survive that regeneration. Keeping it under one key
|
|
558
|
+
* makes the overlay merge a single assignment the generator cannot
|
|
559
|
+
* accidentally clobber field-by-field, and makes "is this ours or the
|
|
560
|
+
* wiki's?" answerable by position rather than by memory.
|
|
561
|
+
*
|
|
562
|
+
* 🛑 It also keeps `adminEditedFields` honest: an admin editing
|
|
563
|
+
* `authored.verdict` records one path, where twenty flattened fields would
|
|
564
|
+
* need twenty entries and the first missed one would be silently
|
|
565
|
+
* re-generated over.
|
|
566
|
+
*/
|
|
567
|
+
authored?: PartAuthoredAnalysis;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* What a human wrote about one part after reading its sources.
|
|
571
|
+
*
|
|
572
|
+
* ── 🛑 WHY THESE FIELDS AND NOT A FREE-TEXT BLOB ───────────────────────────
|
|
573
|
+
*
|
|
574
|
+
* Each one is a question a buyer or a game implementation actually asks, and
|
|
575
|
+
* each was being answered in passing inside prose where nothing could filter,
|
|
576
|
+
* sort or require it. `availability` is the worked example: 93 records carried
|
|
577
|
+
* "one bey, Takara Tomy only" and "widely shipped across six products" inside
|
|
578
|
+
* their competitive reasoning, so the single most commercially useful fact on
|
|
579
|
+
* the record was unqueryable until it became a field.
|
|
580
|
+
*/
|
|
581
|
+
export interface PartAuthoredAnalysis {
|
|
582
|
+
/** My own account of the shape, in my words, from the sources' facts. */
|
|
583
|
+
geometryNote: string;
|
|
584
|
+
/** 2-3 sentences. The thing a buyer reads first. */
|
|
585
|
+
verdict: string;
|
|
586
|
+
competitive: {
|
|
587
|
+
/** essential | useful | collector | outclassed. */
|
|
588
|
+
tier: PartCompetitiveTier;
|
|
589
|
+
/** 0-100 WITHIN its slot, so parts are comparable where they compete. */
|
|
590
|
+
score: number;
|
|
591
|
+
/** Where the judgement came from, so a reader can discount it. */
|
|
592
|
+
basis: "source-overall" | "ratings" | "mass" | "legality" | "combo-list" | "inference";
|
|
593
|
+
/**
|
|
594
|
+
* Per format or per role, because one part is often two parts.
|
|
595
|
+
*
|
|
596
|
+
* 🛑 Not decoration: `attack-ring--circle-upper` produces monstrous Smash
|
|
597
|
+
* ONLY in left spin and mingles all three attack types in right spin, from
|
|
598
|
+
* one fixed shape. A single tier for that part is a lie in one direction.
|
|
599
|
+
*/
|
|
600
|
+
byFormat?: Record<string, string> | null;
|
|
601
|
+
reasoning: string;
|
|
602
|
+
};
|
|
603
|
+
designDifficulty: {
|
|
604
|
+
/** 0-100. How hard this part is to reproduce faithfully in a game. */
|
|
605
|
+
score: number;
|
|
606
|
+
/** From the CLOSED driver set in `terms.json`. */
|
|
607
|
+
drivers: string[];
|
|
608
|
+
reasoning: string;
|
|
609
|
+
/** The detail a naive implementation gets wrong. */
|
|
610
|
+
note?: string;
|
|
611
|
+
};
|
|
612
|
+
/**
|
|
613
|
+
* Which products it came in, and in what colour.
|
|
614
|
+
*
|
|
615
|
+
* 🛑 `beys` is what a RELEASE LIST says shipped it — a purchasing fact with
|
|
616
|
+
* codes and colourways attached. `beysFromOtherSources` is "a document
|
|
617
|
+
* describes this part inside this bey", LABELLED BY ORIGIN
|
|
618
|
+
* (`corpus:gen1-plastic-dranzer-v2`). Merging the two would make a corpus
|
|
619
|
+
* mention indistinguishable from a box you can buy, which is the one
|
|
620
|
+
* distinction a shopper needs. 306 parts have the second and not the first.
|
|
621
|
+
*/
|
|
622
|
+
availability: {
|
|
623
|
+
beys: string[];
|
|
624
|
+
beysFromOtherSources?: string[];
|
|
625
|
+
usedByIds?: string[];
|
|
626
|
+
colourways: Array<{
|
|
627
|
+
colourway: string;
|
|
628
|
+
vendor?: string;
|
|
629
|
+
code?: string;
|
|
630
|
+
distribution?: string;
|
|
631
|
+
inBey?: string;
|
|
632
|
+
}>;
|
|
633
|
+
codes?: string[];
|
|
634
|
+
hasbroCode?: string;
|
|
635
|
+
moulds?: boolean;
|
|
636
|
+
/** Why `beys` is empty, when it is. An empty list is a finding, not a blank. */
|
|
637
|
+
note?: string | null;
|
|
638
|
+
};
|
|
639
|
+
/**
|
|
640
|
+
* The character who used it in the story.
|
|
641
|
+
*
|
|
642
|
+
* 🛑 The single biggest collector hook, and it does NOT live on the part
|
|
643
|
+
* pages: measured, the `Owner` infobox key is filled on 3 of 2,248 part
|
|
644
|
+
* pages. It comes from the bey and character pages. Nobody searches "Attack
|
|
645
|
+
* Ring MA-04"; they search "Tyson's Dragoon".
|
|
646
|
+
*/
|
|
647
|
+
owner?: string;
|
|
648
|
+
/** "Bladebreakers", "Blitz Team" — a collecting set in its own right. */
|
|
649
|
+
team?: string;
|
|
650
|
+
/** Series plus episode of first appearance, as the source words it. */
|
|
651
|
+
animeDebut?: string;
|
|
652
|
+
/** How hard it is to get. A closed enum so it can be faceted. */
|
|
653
|
+
rarity?: PartRarity;
|
|
654
|
+
/** The vendor or region that gated it — "SH-03/04 Chinese exclusive". */
|
|
655
|
+
exclusiveTo?: string;
|
|
656
|
+
/**
|
|
657
|
+
* How to tell which copy you are buying.
|
|
658
|
+
*
|
|
659
|
+
* 🛑 The most actionable buying guidance in the whole corpus and it was
|
|
660
|
+
* buried in prose. Circle Upper's first mould has a 2.9 mm metal frame and
|
|
661
|
+
* weighs mid-19 g; its second has 3.6 mm and weighs upper-20 g, and they
|
|
662
|
+
* perform differently. Cross Griffon's Hasbro mould moves the figurine
|
|
663
|
+
* mounts to the top where they only add recoil, so the wiki says a Takara
|
|
664
|
+
* copy is "greatly preferred". That is a purchase decision.
|
|
665
|
+
*/
|
|
666
|
+
mouldIdentification?: string;
|
|
667
|
+
/**
|
|
668
|
+
* What wears or breaks, and whether it matters — which is exactly what a
|
|
669
|
+
* USED-marketplace listing needs and what the sources keep volunteering:
|
|
670
|
+
* "spikes wear with use but do NOT affect usability", "very unlikely to
|
|
671
|
+
* break, even on Grip Base", "sharp wing tips can cause damage to Stadium
|
|
672
|
+
* walls".
|
|
673
|
+
*/
|
|
674
|
+
conditionWatch?: string;
|
|
675
|
+
/** What else you must own for it to work — "needs a taller RC to clear the floor". */
|
|
676
|
+
needsToUse?: string;
|
|
677
|
+
/**
|
|
678
|
+
* Named finishing moves, with the character who performs them.
|
|
679
|
+
*
|
|
680
|
+
* Source: the bey corpus's own tables, e.g.
|
|
681
|
+
* `| Dark Wings | Phi | Layer-level impact at low spin | Burst Turbo |`.
|
|
682
|
+
* Nothing had ever joined them to a part or a bey.
|
|
683
|
+
*/
|
|
684
|
+
specialMoves?: Array<{
|
|
685
|
+
name: string;
|
|
686
|
+
character?: string;
|
|
687
|
+
effect?: string;
|
|
688
|
+
system?: string;
|
|
689
|
+
}>;
|
|
690
|
+
/** The spirit bound to the bey — a game entity in its own right. */
|
|
691
|
+
bitBeast?: string;
|
|
692
|
+
/**
|
|
693
|
+
* How it wins.
|
|
694
|
+
*
|
|
695
|
+
* 🛑 FOUR classes are already evidenced and most models carry two. Beyond
|
|
696
|
+
* knock-out and destabilise-then-outspin: Cross Griffon has "some of the
|
|
697
|
+
* most effective stopping power of any Attack Ring", interrupting rotation
|
|
698
|
+
* without moving the opponent, and Cross Spiker delivers "spike attack" in
|
|
699
|
+
* left spin. An implementation with only KO and topple has nowhere to put
|
|
700
|
+
* either, and would render the generation's best stopping-power ring as a
|
|
701
|
+
* mid-tier smash part.
|
|
702
|
+
*/
|
|
703
|
+
winCondition?: string[];
|
|
704
|
+
/**
|
|
705
|
+
* Every photograph of this part that a source carries, with its caption.
|
|
706
|
+
*
|
|
707
|
+
* 🛑 THE CAPTION IS THE DATA. "Advance Attacker with Metal Frame detached" is
|
|
708
|
+
* not a label for a picture — it is the statement that a photograph exists of
|
|
709
|
+
* the one feature the part is built around, and looking at it corrected the
|
|
710
|
+
* prose: every source calls the metal frame a "Metal Frame" that "separates
|
|
711
|
+
* from the ABS Caul", and the photograph shows it is a single die-cast
|
|
712
|
+
* triskelion — a central annulus, three radial arms, three tangential claws,
|
|
713
|
+
* with three registration pegs at 120° that are what permit the flip. No
|
|
714
|
+
* source says any of that.
|
|
715
|
+
*
|
|
716
|
+
* 🛑 `image` above is ONE `/media/<slug>` for the card. This is the gallery,
|
|
717
|
+
* and it holds SOURCE urls deliberately: routing them through `/media`
|
|
718
|
+
* requires uploading ~1,600 objects to live Storage, which is a separate,
|
|
719
|
+
* explicitly-authorised step. Recording what exists is free and reversible;
|
|
720
|
+
* publishing it is neither. Nothing renders these directly — Root Cause #27
|
|
721
|
+
* and the media rules still apply at the point of use.
|
|
722
|
+
*
|
|
723
|
+
* 🛑 Fetching needs `Referer: https://beyblade.fandom.com/`.
|
|
724
|
+
* `static.wikia.nocookie.net` hotlink-protects on the REFERER, not the
|
|
725
|
+
* User-Agent: a browser UA alone returns 403 `text/html`, which reads as "the
|
|
726
|
+
* image is gone" and sends you hunting for a replacement that does not exist.
|
|
727
|
+
*
|
|
728
|
+
* 🛑 fandom and WBO are ONE lineage here, not two sources. Both serve
|
|
729
|
+
* `1AdvanceStriker.JPG` under the same name. Dedupe on `name`, never on
|
|
730
|
+
* `url`, or one photograph is counted as corroboration of itself.
|
|
731
|
+
*/
|
|
732
|
+
photos?: Array<{
|
|
733
|
+
/** The source's own caption. Empty on WBO, which usually gives none. */
|
|
734
|
+
caption?: string;
|
|
735
|
+
/** Original upload filename — the dedupe key across hosts. */
|
|
736
|
+
name?: string;
|
|
737
|
+
/** Full-size URL, with the host's thumbnail scaling undone. */
|
|
738
|
+
url: string;
|
|
739
|
+
/** `fandom` | `wbo` — which crawl it came from, for attribution. */
|
|
740
|
+
host?: string;
|
|
741
|
+
/** What this photo PROVES that the prose does not say. */
|
|
742
|
+
shows?: string;
|
|
743
|
+
}>;
|
|
744
|
+
/**
|
|
745
|
+
* What wear does to it.
|
|
746
|
+
*
|
|
747
|
+
* 🛑 WEAR CAN BE A POSITIVE, which a durability model that only degrades
|
|
748
|
+
* cannot express. hmsdb's best combo for Circle Upper wants "a Bearing Core
|
|
749
|
+
* 2 that is WORN ENOUGH to consistently flower pattern". Treating wear as
|
|
750
|
+
* monotonic damage contradicts the source.
|
|
751
|
+
*/
|
|
752
|
+
wearBehaviour?: string;
|
|
753
|
+
/** Launcher or winder needed, and whether spin direction is chosen at launch. */
|
|
754
|
+
launcherRequirement?: string;
|
|
755
|
+
/** Where behaviour changes by stadium — range matters more in a wide one. */
|
|
756
|
+
stadiumDependence?: string;
|
|
757
|
+
/** Counters and countered-by, as the sources name them. */
|
|
758
|
+
matchups?: {
|
|
759
|
+
strongAgainst?: string[];
|
|
760
|
+
weakAgainst?: string[];
|
|
761
|
+
note?: string;
|
|
762
|
+
};
|
|
763
|
+
/** Parts the sources pair it with, verbatim from their combo lists. */
|
|
764
|
+
comboPartners?: string[];
|
|
765
|
+
/** Terms from `terms.json`, so the vocabulary stays closed. */
|
|
766
|
+
termsUsed?: string[];
|
|
767
|
+
/** Where a source was wrong or where two sources disagree. */
|
|
768
|
+
corrections?: Array<{
|
|
769
|
+
field: string;
|
|
770
|
+
was: string;
|
|
771
|
+
now: string;
|
|
772
|
+
basis: string;
|
|
773
|
+
}>;
|
|
774
|
+
/** confirmed | probable | weak — and the gate checks it against the record. */
|
|
775
|
+
confidence: "confirmed" | "probable" | "weak";
|
|
776
|
+
confidenceNote?: string;
|
|
777
|
+
/** Which sources the record was written from. */
|
|
778
|
+
sourceUsed: string[];
|
|
779
|
+
sourceUrl?: string;
|
|
780
|
+
authoredAt: string;
|
|
781
|
+
/** Set when a record is re-read after new sources became reachable. */
|
|
782
|
+
reReadAt?: string;
|
|
551
783
|
}
|
|
784
|
+
/** How hard a part is to obtain. Faceted, so it is a closed set. */
|
|
785
|
+
export declare const PartRarityValues: readonly ["mass-retail", "booster", "random-booster", "set-only", "prize", "vendor-exclusive", "regional-exclusive"];
|
|
786
|
+
export type PartRarity = (typeof PartRarityValues)[number];
|
|
787
|
+
/**
|
|
788
|
+
* How much a part matters competitively.
|
|
789
|
+
*
|
|
790
|
+
* 🛑 `collector` is not a weak `useful`. It means "worth owning rather than
|
|
791
|
+
* playing" and it is the only tier assignable with NO performance evidence —
|
|
792
|
+
* which is what a part like `attack-ring--dark-spike`, whose every source is
|
|
793
|
+
* silent, honestly deserves.
|
|
794
|
+
*/
|
|
795
|
+
export declare const PartCompetitiveTierValues: readonly ["essential", "useful", "collector", "outclassed"];
|
|
796
|
+
export type PartCompetitiveTier = (typeof PartCompetitiveTierValues)[number];
|
|
552
797
|
/** One slot of one model's composition. */
|
|
553
798
|
export interface ModelSlot {
|
|
554
799
|
slotKey: PartSlot;
|
|
@@ -624,6 +869,35 @@ export interface ModelCompositionDocument extends BaseDocument {
|
|
|
624
869
|
source?: string;
|
|
625
870
|
/** The named-model category this composition describes, when one exists. */
|
|
626
871
|
categoryId?: string;
|
|
872
|
+
/**
|
|
873
|
+
* The character who used this bey, as a `character-*` category id.
|
|
874
|
+
*
|
|
875
|
+
* 🛑 An ID, not a name. Character names are re-localised between the
|
|
876
|
+
* Japanese and English releases (Takao Kinomiya / Tyson Granger), so a name
|
|
877
|
+
* is the one key guaranteed to drift — the same trap `brand-rows.ts` records
|
|
878
|
+
* for brands matching on display name.
|
|
879
|
+
*/
|
|
880
|
+
owner?: string;
|
|
881
|
+
/**
|
|
882
|
+
* ── 🛑 THE ANIME SUCCESSION CHAIN, NOT RELEASE ORDER ────────────────────
|
|
883
|
+
*
|
|
884
|
+
* `previousBey` is the bey this one replaced in the STORY; `nextBey` is the
|
|
885
|
+
* one that replaced it. Both are composition ids.
|
|
886
|
+
*
|
|
887
|
+
* 🛑 IT CANNOT BE DERIVED FROM PRODUCT CODES, and a generator must not try.
|
|
888
|
+
* Tyson's chain runs Dragoon -> Dragoon S -> Dragoon V -> Dragoon V2 ->
|
|
889
|
+
* Dragoon G -> Dragoon MS, crossing four part systems and three toy lines,
|
|
890
|
+
* and the codes do not order it. The sequence is a narrative fact stated in
|
|
891
|
+
* prose on the bey and character pages, so it is READ.
|
|
892
|
+
*
|
|
893
|
+
* 🛑 BOTH DIRECTIONS ARE STORED AND MUST AGREE. A one-way link makes "what
|
|
894
|
+
* came before this" unanswerable from the earlier bey's page, and a
|
|
895
|
+
* `character.lineup` that disagrees with these links is the denormalised-
|
|
896
|
+
* mirror trap (Root Cause #42) — here it would silently break a progression.
|
|
897
|
+
* `audit-lineup-symmetry` checks both.
|
|
898
|
+
*/
|
|
899
|
+
previousBey?: string;
|
|
900
|
+
nextBey?: string;
|
|
627
901
|
}
|
|
628
902
|
/** `(slot, code|name)` → document id. The ONE place this is composed. */
|
|
629
903
|
export declare function partGlossaryId(slot: PartSlot, codeOrName: string): string;
|
|
@@ -305,6 +305,25 @@ export function partSlotFromSegment(segment) {
|
|
|
305
305
|
const hit = ALL_PART_SLOTS.find((s) => s === segment);
|
|
306
306
|
return hit ?? null;
|
|
307
307
|
}
|
|
308
|
+
/** How hard a part is to obtain. Faceted, so it is a closed set. */
|
|
309
|
+
export const PartRarityValues = [
|
|
310
|
+
"mass-retail",
|
|
311
|
+
"booster",
|
|
312
|
+
"random-booster",
|
|
313
|
+
"set-only",
|
|
314
|
+
"prize",
|
|
315
|
+
"vendor-exclusive",
|
|
316
|
+
"regional-exclusive",
|
|
317
|
+
];
|
|
318
|
+
/**
|
|
319
|
+
* How much a part matters competitively.
|
|
320
|
+
*
|
|
321
|
+
* 🛑 `collector` is not a weak `useful`. It means "worth owning rather than
|
|
322
|
+
* playing" and it is the only tier assignable with NO performance evidence —
|
|
323
|
+
* which is what a part like `attack-ring--dark-spike`, whose every source is
|
|
324
|
+
* silent, honestly deserves.
|
|
325
|
+
*/
|
|
326
|
+
export const PartCompetitiveTierValues = ["essential", "useful", "collector", "outclassed"];
|
|
308
327
|
/** `(slot, code|name)` → document id. The ONE place this is composed. */
|
|
309
328
|
export function partGlossaryId(slot, codeOrName) {
|
|
310
329
|
const tail = codeOrName
|
|
@@ -12,7 +12,7 @@ import { createHash } from "crypto";
|
|
|
12
12
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
13
13
|
import { DatabaseError } from "../../../errors/index.js";
|
|
14
14
|
import { serverLogger } from "../../../monitoring/index.js";
|
|
15
|
-
import { BaseRepository, prepareForFirestore
|
|
15
|
+
import { BaseRepository, prepareForFirestore } from "../../../providers/db-firebase/index.js";
|
|
16
16
|
import { PAYMENT_METHOD_PII_FIELDS, encryptPiiFields, maskPii, revealPii, } from "../../../security/index.js";
|
|
17
17
|
import { asEncrypted } from "../../../security/encrypted.js";
|
|
18
18
|
import { SAVED_PAYMENT_METHOD_FIELDS, SAVED_PAYMENT_METHODS_COLLECTION, } from "../schemas/saved-methods-firestore.js";
|
|
@@ -11,11 +11,7 @@
|
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { getProviders } from "../../../../contracts/index.js";
|
|
13
13
|
import { normalizeError } from "../../../../errors/normalize.js";
|
|
14
|
-
|
|
15
|
-
const v = url.searchParams.get(key);
|
|
16
|
-
const n = v !== null ? Number(v) : NaN;
|
|
17
|
-
return Number.isFinite(n) ? n : fallback;
|
|
18
|
-
}
|
|
14
|
+
import { numParam } from "../../../../utils/search-params.js";
|
|
19
15
|
export async function GET(request) {
|
|
20
16
|
try {
|
|
21
17
|
const url = new URL(request.url);
|
|
@@ -14,7 +14,7 @@ const CLS_STATUS_BADGE = "inline-block rounded-full bg-warning-surface dark:bg-w
|
|
|
14
14
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
15
15
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
16
16
|
import { formatCurrency, roundRupees } from "../../../utils/number.formatter.js";
|
|
17
|
-
import {
|
|
17
|
+
import { toDescriptionHtml } from "../../../utils/string.formatter.js";
|
|
18
18
|
import { safeDisplayName } from "../../../security/index.js";
|
|
19
19
|
import { ProgressBarFill } from "../../../ui/components/ProgressBarFill.js";
|
|
20
20
|
import { Button, Container, Div, GatedPrice, PricesOnly, Heading, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, SpecTable, } from "../../../ui/index.js";
|
|
@@ -38,12 +38,6 @@ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListing
|
|
|
38
38
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data.js";
|
|
39
39
|
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
40
40
|
import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
|
|
41
|
-
function toDescriptionHtml(raw) {
|
|
42
|
-
if (!raw)
|
|
43
|
-
return "";
|
|
44
|
-
const s = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
45
|
-
return normalizeRichTextHtml(s);
|
|
46
|
-
}
|
|
47
41
|
const PRODUCTION_STATUS_LABELS = {
|
|
48
42
|
upcoming: "Coming Soon",
|
|
49
43
|
in_production: "In Production",
|
|
@@ -22,6 +22,7 @@ import { printMetaSchema } from "../schemas/index.js";
|
|
|
22
22
|
import { sanitizeProductsForPublic } from "../utils/sanitize.js";
|
|
23
23
|
import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public.js";
|
|
24
24
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
25
|
+
import { param } from "../../../utils/search-params.js";
|
|
25
26
|
// --- Mutation schemas ---------------------------------------------------------
|
|
26
27
|
// Minimal schemas for secured mutations — consumer apps can extend as needed.
|
|
27
28
|
const productMutateSchema = z
|
|
@@ -72,9 +73,6 @@ const productMutateSchema = z
|
|
|
72
73
|
})
|
|
73
74
|
.passthrough();
|
|
74
75
|
// --- Helpers ------------------------------------------------------------------
|
|
75
|
-
function param(url, key) {
|
|
76
|
-
return url.searchParams.get(key);
|
|
77
|
-
}
|
|
78
76
|
// --- GET /api/products --------------------------------------------------------
|
|
79
77
|
/**
|
|
80
78
|
* Delegates to `listPublicProducts` — the single implementation shared with
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { RichText, SpecTable, Stack, Text } from "../../../ui/index.js";
|
|
3
3
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
4
|
-
function renderFieldValue(f) {
|
|
5
|
-
if (f.type === "boolean")
|
|
6
|
-
return f.value === "true" ? "Yes" : "No";
|
|
7
|
-
return f.value + (f.unit ? ` ${f.unit}` : "");
|
|
8
|
-
}
|
|
9
4
|
/**
|
|
10
5
|
* `customSections` → `ProductTabsShell`'s `customTabs` prop, in one place.
|
|
11
6
|
*
|
|
@@ -18,7 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
* consumer pages untouched.
|
|
19
19
|
*/
|
|
20
20
|
import { useMemo, useState } from "react";
|
|
21
|
-
import { Alert, Button, Div, GatedPrice, PricesOnly, Row, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr, } from "../../../ui/index.js";
|
|
21
|
+
import { Alert, Button, Div, GatedPrice, PricesOnly, Row, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr, TextLink, } from "../../../ui/index.js";
|
|
22
22
|
import { QuantityStepper } from "../../../ui/components/QuantityStepper.js";
|
|
23
23
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
24
24
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
@@ -140,7 +140,7 @@ export function GroupMemberPicker({ groupId, groupSource, members, rules = EMPTY
|
|
|
140
140
|
* A made-to-order member has no ceiling; the line's
|
|
141
141
|
* own 99-per-line cap still applies at the cart.
|
|
142
142
|
*/
|
|
143
|
-
max: m.madeToOrder === true ? undefined : m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx(
|
|
143
|
+
max: m.madeToOrder === true ? undefined : m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx(TextLink, { href: memberHref(m), size: "xs", className: "text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }, m.id));
|
|
144
144
|
}) })] }) }), !isCrossStore && (_jsxs(_Fragment, { children: [ruleNotes.length > 0 && (_jsx(Text, { size: "xs", color: "muted", children: ruleNotes.join(" · ") })), error && _jsx(Alert, { variant: "error", children: error }), selectedUnits > 0 && violations.length > 0 && (_jsx(Alert, { variant: "warning", children: _jsx(Stack, { gap: "dense", children: violations.map((v) => (_jsx(Text, { size: "sm", children: v.message }, v.code + v.message))) }) })), _jsxs(Row, { justify: "between", align: "center", gap: "sm", className: "min-w-0", children: [_jsxs(Text, { size: "sm", color: "muted", children: [selectedUnits > 0
|
|
145
145
|
? `${selectedUnits} item${selectedUnits === 1 ? "" : "s"}`
|
|
146
146
|
: "Nothing selected yet", selectedUnits > 0 && (_jsx(PricesOnly, { children: discountPercent > 0
|
|
@@ -359,7 +359,7 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
|
|
|
359
359
|
return (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: close, title: "Add child listing", children: content })) : (_jsx(Modal, { open: showAddModal, onClose: close, title: "Add child listing", size: "lg", children: content }));
|
|
360
360
|
})(), confirmAction && (_jsx(ConfirmDeleteModal, { isOpen: true, title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onClose: () => setConfirmAction(null), isDeleting: loading }))] }));
|
|
361
361
|
}
|
|
362
|
-
function AddChildContent({ addTab, setAddTab, createForm, setCreateForm,
|
|
362
|
+
function AddChildContent({ addTab, setAddTab, createForm, setCreateForm, storeProductsEndpoint, linkTargets, setLinkTargets, onAddCreate, onAddLink, loading, }) {
|
|
363
363
|
const sections = React.useMemo(() => buildSectionsFromSchema(createChildSchema, {
|
|
364
364
|
options: { condition: CONDITION_OPTIONS },
|
|
365
365
|
}), []);
|
|
@@ -9,7 +9,7 @@ const __P = {
|
|
|
9
9
|
const CLS_PRIZE_BADGE = "inline-block rounded-full bg-fuchsia-100 dark:bg-fuchsia-900/30 px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-fuchsia-700 dark:text-fuchsia-300";
|
|
10
10
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
11
11
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
12
|
-
import {
|
|
12
|
+
import { toDescriptionHtml } from "../../../utils/string.formatter.js";
|
|
13
13
|
import { safeDisplayName } from "../../../security/index.js";
|
|
14
14
|
import { Container, Disclaimer, Div, Heading, Main, RichText, Row, Section, Span, Stack, Text, } from "../../../ui/index.js";
|
|
15
15
|
import { PreOrderDetailView } from "./PreOrderDetailView.js";
|
|
@@ -24,12 +24,6 @@ import { computeRelatedItems } from "../../../_internal/server/features/products
|
|
|
24
24
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel.js";
|
|
25
25
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data.js";
|
|
26
26
|
import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
|
|
27
|
-
function toDescriptionHtml(raw) {
|
|
28
|
-
if (!raw)
|
|
29
|
-
return "";
|
|
30
|
-
const s = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
31
|
-
return normalizeRichTextHtml(s);
|
|
32
|
-
}
|
|
33
27
|
function statusLabel(s) {
|
|
34
28
|
switch (s) {
|
|
35
29
|
case "open":
|
|
@@ -29,4 +29,4 @@ export interface PrizeDrawEntryActionsProps {
|
|
|
29
29
|
* the rule's preflight already checks `prizeMaxEntries`. The whole server half
|
|
30
30
|
* was written and unreachable because the client never sent a quantity.
|
|
31
31
|
*/
|
|
32
|
-
export declare function PrizeDrawEntryActions({ productId,
|
|
32
|
+
export declare function PrizeDrawEntryActions({ productId, title, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }: PrizeDrawEntryActionsProps): import("react").JSX.Element;
|
|
@@ -34,7 +34,7 @@ import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
|
34
34
|
* the rule's preflight already checks `prizeMaxEntries`. The whole server half
|
|
35
35
|
* was written and unreachable because the client never sent a quantity.
|
|
36
36
|
*/
|
|
37
|
-
export function PrizeDrawEntryActions({ productId,
|
|
37
|
+
export function PrizeDrawEntryActions({ productId, title, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }) {
|
|
38
38
|
const router = useRouter();
|
|
39
39
|
const { requireAuth, isAuthResolving, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
40
40
|
const [consentOpen, setConsentOpen] = useState(false);
|
|
@@ -34,7 +34,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
|
34
34
|
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
35
35
|
import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
|
|
36
36
|
import { Button, Container, Div, Heading, Main, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, SpecTable, Icon, } from "../../../ui/index.js";
|
|
37
|
-
import {
|
|
37
|
+
import { toDescriptionHtml } from "../../../utils/string.formatter.js";
|
|
38
38
|
import { safeDisplayName } from "../../../security/index.js";
|
|
39
39
|
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel.js";
|
|
40
40
|
import { ProductDetailView } from "./ProductDetailView.js";
|
|
@@ -61,12 +61,6 @@ import { DEFAULT_MIN_OFFER_PERCENT, resolveOfferBounds, } from "../../../_intern
|
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
// Helpers
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
|
-
function toDescriptionHtml(raw) {
|
|
65
|
-
if (!raw)
|
|
66
|
-
return "";
|
|
67
|
-
const s = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
68
|
-
return normalizeRichTextHtml(s);
|
|
69
|
-
}
|
|
70
64
|
function StarRating({ value }) {
|
|
71
65
|
const full = Math.floor(value);
|
|
72
66
|
const half = value - full >= 0.5;
|
|
@@ -343,7 +343,6 @@ export class ProductRepository extends BaseRepository {
|
|
|
343
343
|
* derived one is the one that goes stale.
|
|
344
344
|
*/
|
|
345
345
|
const assignments = readAssignments(input);
|
|
346
|
-
const leaf = assignments[0] ?? null;
|
|
347
346
|
/*
|
|
348
347
|
* The brand selector's option value is the brand ROW's id (`brand-beyblade`),
|
|
349
348
|
* but `ProductDocument.brand` is matched by DISPLAY NAME —
|
|
@@ -20,14 +20,7 @@
|
|
|
20
20
|
import { NextResponse } from "next/server.js";
|
|
21
21
|
import { getProviders } from "../../../contracts/index.js";
|
|
22
22
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
23
|
-
|
|
24
|
-
return url.searchParams.get(key);
|
|
25
|
-
}
|
|
26
|
-
function numParam(url, key, fallback) {
|
|
27
|
-
const v = url.searchParams.get(key);
|
|
28
|
-
const n = v !== null ? Number(v) : NaN;
|
|
29
|
-
return Number.isFinite(n) ? n : fallback;
|
|
30
|
-
}
|
|
23
|
+
import { numParam, param } from "../../../utils/search-params.js";
|
|
31
24
|
// --- GET /api/promotions ------------------------------------------------------
|
|
32
25
|
// Query params:
|
|
33
26
|
// isActive — "true" | "false" (default: "true")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
type
|
|
2
|
+
import type { SearchParamsRecord } from "../../../utils/search-params";
|
|
3
3
|
export interface ReviewsIndexPageViewProps {
|
|
4
|
-
searchParams?:
|
|
4
|
+
searchParams?: SearchParamsRecord;
|
|
5
5
|
}
|
|
6
6
|
export declare function ReviewsIndexPageView({ searchParams }: ReviewsIndexPageViewProps): Promise<React.JSX.Element>;
|
|
7
|
-
export {};
|
|
@@ -5,13 +5,10 @@ import { Container, Heading, Main, Section } from "../../../ui/index.js";
|
|
|
5
5
|
import { AdSlot } from "../../homepage/components/AdSlot.js";
|
|
6
6
|
import { ReviewsIndexListing } from "./ReviewsIndexListing.js";
|
|
7
7
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
8
|
-
|
|
9
|
-
const v = params[key];
|
|
10
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
11
|
-
}
|
|
8
|
+
import { firstParam } from "../../../utils/search-params.js";
|
|
12
9
|
function buildReviewFilters(params) {
|
|
13
10
|
const parts = ["status==approved"];
|
|
14
|
-
const rating =
|
|
11
|
+
const rating = firstParam(params, "rating");
|
|
15
12
|
if (rating) {
|
|
16
13
|
const values = rating.split("|").filter(Boolean);
|
|
17
14
|
if (values.length === 1)
|
|
@@ -19,14 +16,14 @@ function buildReviewFilters(params) {
|
|
|
19
16
|
else if (values.length > 1)
|
|
20
17
|
parts.push(sieveFilter("rating", SIEVE_OP.EQ, values.join("|")));
|
|
21
18
|
}
|
|
22
|
-
const minVotes =
|
|
19
|
+
const minVotes = firstParam(params, "minVotes");
|
|
23
20
|
if (minVotes)
|
|
24
21
|
parts.push(sieveFilter("helpfulCount", SIEVE_OP.GTE, minVotes));
|
|
25
|
-
const maxVotes =
|
|
22
|
+
const maxVotes = firstParam(params, "maxVotes");
|
|
26
23
|
if (maxVotes)
|
|
27
24
|
parts.push(sieveFilter("helpfulCount", SIEVE_OP.LTE, maxVotes));
|
|
28
|
-
const dateFrom =
|
|
29
|
-
const dateTo =
|
|
25
|
+
const dateFrom = firstParam(params, "dateFrom");
|
|
26
|
+
const dateTo = firstParam(params, "dateTo");
|
|
30
27
|
if (dateFrom)
|
|
31
28
|
parts.push(sieveFilter("createdAt", SIEVE_OP.GTE, dateFrom));
|
|
32
29
|
if (dateTo)
|
|
@@ -53,10 +50,10 @@ function buildReviewFilters(params) {
|
|
|
53
50
|
return parts.join(",");
|
|
54
51
|
}
|
|
55
52
|
export async function ReviewsIndexPageView({ searchParams = {} }) {
|
|
56
|
-
const sort =
|
|
57
|
-
const page = Number(
|
|
53
|
+
const sort = firstParam(searchParams, "sort") || "-createdAt";
|
|
54
|
+
const page = Number(firstParam(searchParams, "page")) || 1;
|
|
58
55
|
const filters = buildReviewFilters(searchParams);
|
|
59
|
-
const q =
|
|
56
|
+
const q = firstParam(searchParams, "q");
|
|
60
57
|
// audit-public-test-data-ok: `ReviewDocument` carries no `isTestData` field —
|
|
61
58
|
// tsc rejects the filter outright, which is the honest signal that reviews
|
|
62
59
|
// have no sandbox concept to strip. A review OF a sandbox product is not
|
|
@@ -15,11 +15,7 @@
|
|
|
15
15
|
import { NextResponse } from "next/server.js";
|
|
16
16
|
import { getProviders } from "../../../../contracts/index.js";
|
|
17
17
|
import { createRouteHandler } from "../../../../next/api/routeHandler.js";
|
|
18
|
-
|
|
19
|
-
const v = url.searchParams.get(key);
|
|
20
|
-
const n = v !== null ? Number(v) : NaN;
|
|
21
|
-
return Number.isFinite(n) ? n : fallback;
|
|
22
|
-
}
|
|
18
|
+
import { numParam } from "../../../../utils/search-params.js";
|
|
23
19
|
export const GET = createRouteHandler({
|
|
24
20
|
roles: ["seller", "admin", "moderator"],
|
|
25
21
|
handler: async ({ request, user }) => {
|
|
@@ -16,11 +16,7 @@ import { NextResponse } from "next/server.js";
|
|
|
16
16
|
import { getProviders } from "../../../../contracts/index.js";
|
|
17
17
|
import { createRouteHandler } from "../../../../next/api/routeHandler.js";
|
|
18
18
|
import { storeRepository } from "../../../stores/repository/store.repository.js";
|
|
19
|
-
|
|
20
|
-
const v = url.searchParams.get(key);
|
|
21
|
-
const n = v !== null ? Number(v) : NaN;
|
|
22
|
-
return Number.isFinite(n) ? n : fallback;
|
|
23
|
-
}
|
|
19
|
+
import { numParam } from "../../../../utils/search-params.js";
|
|
24
20
|
export const GET = createRouteHandler({
|
|
25
21
|
roles: ["seller", "admin", "moderator"],
|
|
26
22
|
handler: async ({ request, user }) => {
|
|
@@ -544,7 +544,6 @@ productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCat
|
|
|
544
544
|
useFormShellState(sellerProductSchema, undefined, {
|
|
545
545
|
allowAddAnother: mode === "create",
|
|
546
546
|
});
|
|
547
|
-
const [stepError, setStepError] = useState(null);
|
|
548
547
|
const { isDirty, markDirty, markClean } = useFormShell();
|
|
549
548
|
const router = useRouter();
|
|
550
549
|
const { showToast } = useToast();
|
|
@@ -859,7 +858,7 @@ productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCat
|
|
|
859
858
|
}) }) }) }));
|
|
860
859
|
}
|
|
861
860
|
if (mode === "create") {
|
|
862
|
-
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, splitPreview: !!previewSlot, renderBottomBar: () => (_jsxs(Div, { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(Div, { paddingX: "x-5", padding: "y-sm", children: _jsx(FormErrorSummary, {}) }) }), _jsxs(Div, { paddingX: "x-5", padding: "b-sm", children: [_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(AddAnotherToggle, { noun: listingTypeLabel.toLowerCase(), isCreate: mode === "create" }) }), _jsxs(Row, { gap: "sm", justify: "between", wrap: true, children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => void handleSave(), disabled: isLoading || !draft.title?.trim(), children: "Save as draft & finish later" }), _jsx(Button, { variant: "primary", onClick: () => void handlePublish(), isLoading: isLoading, disabled: isLoading, children: addAnotherSubmitLabel(`Publish ${listingTypeLabel}`, addAnother) })] })] })
|
|
861
|
+
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, splitPreview: !!previewSlot, renderBottomBar: () => (_jsxs(Div, { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(Div, { paddingX: "x-5", padding: "y-sm", children: _jsx(FormErrorSummary, {}) }) }), _jsxs(Div, { paddingX: "x-5", padding: "b-sm", children: [_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(AddAnotherToggle, { noun: listingTypeLabel.toLowerCase(), isCreate: mode === "create" }) }), _jsxs(Row, { gap: "sm", justify: "between", wrap: true, children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => void handleSave(), disabled: isLoading || !draft.title?.trim(), children: "Save as draft & finish later" }), _jsx(Button, { variant: "primary", onClick: () => void handlePublish(), isLoading: isLoading, disabled: isLoading, children: addAnotherSubmitLabel(`Publish ${listingTypeLabel}`, addAnother) })] })] })] })), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(SectionForm, { sections: sections, values: draft, onChange: update, onSubmit: handlePublish, schema: sellerProductSchema, openIds: openIds, onOpenChange: setOpenIds, isLoading: isLoading, hideActions: true }) }) }));
|
|
863
862
|
}
|
|
864
863
|
// Edit mode — FormShell with section nav + full form
|
|
865
864
|
const editSections = [
|