@mohasinac/appkit 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/navActive.d.ts +17 -0
- package/dist/_internal/client/features/layout/navActive.js +33 -0
- package/dist/_internal/server/features/auth/actions.d.ts +0 -1
- package/dist/_internal/server/features/auth/actions.js +0 -1
- package/dist/_internal/server/features/bundles/data.d.ts +22 -4
- package/dist/_internal/server/features/bundles/data.js +41 -4
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +7 -78
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
- package/dist/_internal/server/features/orders/adapters.js +1 -14
- package/dist/_internal/server/features/products/actions.js +8 -3
- package/dist/_internal/server/features/products/service.d.ts +15 -0
- package/dist/_internal/server/features/products/service.js +41 -0
- package/dist/_internal/server/functions/firestore.d.ts +3 -1
- package/dist/_internal/server/functions/firestore.js +17 -1
- package/dist/_internal/server/functions/scheduled.d.ts +2 -5
- package/dist/_internal/server/functions/scheduled.js +6 -30
- package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
- package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
- package/dist/_internal/server/jobs/core/index.d.ts +4 -4
- package/dist/_internal/server/jobs/core/index.js +4 -4
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +2 -2
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -4
- package/dist/_internal/server/jobs/handlers/index.js +8 -6
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
- package/dist/_internal/shared/actions/action-registry.js +40 -10
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
- package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
- package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
- package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
- package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +5 -5
- package/dist/constants/field-names.d.ts +17 -1
- package/dist/constants/field-names.js +17 -1
- package/dist/features/account/components/UserSidebar.js +21 -9
- package/dist/features/account/hooks/useNotifications.js +5 -0
- package/dist/features/admin/actions/admin-actions.js +6 -0
- package/dist/features/admin/actions/notification-actions.js +1 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
- package/dist/features/admin/components/AdminGuideHubView.js +15 -1
- package/dist/features/admin/components/AdminPaymentsGuideView.d.ts +2 -0
- package/dist/features/admin/components/AdminPaymentsGuideView.js +36 -0
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSidebar.js +18 -9
- package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
- package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
- package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +2 -0
- package/dist/features/admin/components/AdminWhatsAppGuideView.js +35 -0
- package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
- package/dist/features/admin/hooks/useAdminListing.js +4 -2
- package/dist/features/admin/schemas/firestore.d.ts +13 -5
- package/dist/features/admin/schemas/firestore.js +1 -2
- package/dist/features/auctions/actions/bid-actions.js +3 -1
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
- package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
- package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
- package/dist/features/auctions/components/CollapsibleBidHistory.js +22 -3
- package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
- package/dist/features/auctions/components/LiveBidPrice.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useBids.d.ts +33 -0
- package/dist/features/auctions/hooks/useBids.js +32 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
- package/dist/features/auth/checkout-value-otp.d.ts +7 -10
- package/dist/features/auth/checkout-value-otp.js +14 -10
- package/dist/features/auth/server.d.ts +0 -1
- package/dist/features/auth/server.js +0 -1
- package/dist/features/blog/api/[slug]/route.d.ts +5 -0
- package/dist/features/blog/api/[slug]/route.js +12 -3
- package/dist/features/blog/components/BlogPostView.d.ts +2 -0
- package/dist/features/blog/components/BlogPostView.js +5 -4
- package/dist/features/blog/hooks/useBlog.d.ts +2 -0
- package/dist/features/blog/hooks/useBlog.js +2 -0
- package/dist/features/blog/repository/blog.repository.d.ts +7 -0
- package/dist/features/blog/repository/blog.repository.js +44 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -0
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/index.d.ts +0 -2
- package/dist/features/cart/components/index.js +0 -1
- package/dist/features/cart/hooks/useCartCount.js +22 -5
- package/dist/features/cart/hooks/useGuestCart.js +11 -1
- package/dist/features/cart/utils/guest-cart.d.ts +4 -0
- package/dist/features/cart/utils/guest-cart.js +10 -0
- package/dist/features/cart/utils/pending-ops.d.ts +17 -0
- package/dist/features/cart/utils/pending-ops.js +46 -5
- package/dist/features/categories/components/BrandDetailPageView.js +6 -2
- package/dist/features/categories/components/BundleDetailView.js +4 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
- package/dist/features/categories/schemas/firestore.d.ts +7 -0
- package/dist/features/checkout/actions/index.d.ts +0 -1
- package/dist/features/checkout/actions/index.js +0 -1
- package/dist/features/checkout/schemas/firestore.d.ts +2 -2
- package/dist/features/events/actions/event-actions.d.ts +2 -0
- package/dist/features/events/actions/event-actions.js +5 -0
- package/dist/features/events/components/EventCard.js +2 -3
- package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
- package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
- package/dist/features/events/components/index.d.ts +2 -0
- package/dist/features/events/components/index.js +1 -0
- package/dist/features/events/repository/events.repository.d.ts +6 -0
- package/dist/features/events/repository/events.repository.js +21 -0
- package/dist/features/history/repository/user-history.repository.d.ts +1 -1
- package/dist/features/history/utils/guest-history.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +44 -10
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.d.ts +7 -0
- package/dist/features/media/MediaVideo.js +36 -5
- package/dist/features/orders/components/OrdersList.js +1 -5
- package/dist/features/orders/schemas/firestore.d.ts +2 -8
- package/dist/features/orders/schemas/index.d.ts +3 -32
- package/dist/features/orders/schemas/index.js +1 -4
- package/dist/features/orders/types/index.d.ts +6 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/actions/product-actions.js +13 -10
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
- package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
- package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
- package/dist/features/products/components/PrizeRevealModal.js +18 -147
- package/dist/features/products/components/ProductDetailPageView.js +72 -30
- package/dist/features/products/components/ProductForm.js +7 -14
- package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
- package/dist/features/products/components/ProductGalleryClient.js +29 -10
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +1 -1
- package/dist/features/products/components/index.d.ts +4 -2
- package/dist/features/products/components/index.js +1 -0
- package/dist/features/products/constants/action-defs.d.ts +1 -3
- package/dist/features/products/constants/action-defs.js +0 -5
- package/dist/features/products/repository/products.repository.d.ts +6 -0
- package/dist/features/products/repository/products.repository.js +20 -1
- package/dist/features/products/schemas/firestore.d.ts +28 -3
- package/dist/features/products/schemas/index.d.ts +11 -3
- package/dist/features/products/schemas/index.js +10 -2
- package/dist/features/products/types/index.d.ts +2 -1
- package/dist/features/reviews/actions/review-actions.js +1 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
- package/dist/features/reviews/components/ReviewsList.js +2 -1
- package/dist/features/reviews/schemas/firestore.d.ts +3 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -0
- package/dist/features/reviews/schemas/index.js +2 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.js +3 -2
- package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
- package/dist/features/scams/repository/scammer.repository.js +24 -0
- package/dist/features/seller/actions/seller-actions.js +8 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
- package/dist/features/seller/components/SellerSidebar.js +20 -9
- package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
- package/dist/features/stores/components/StoreGuideHubView.js +7 -1
- package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
- package/dist/features/stores/components/StoreNavTabs.js +22 -9
- package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +3 -0
- package/dist/features/stores/components/StoreWhatsAppGuideView.js +12 -0
- package/dist/features/stores/components/index.d.ts +2 -0
- package/dist/features/stores/components/index.js +1 -0
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
- package/dist/features/tester/actions/leaderboard-actions.js +5 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
- package/dist/features/tester/components/index.d.ts +2 -0
- package/dist/features/tester/components/index.js +1 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
- package/dist/features/tester/schemas/firestore.d.ts +22 -1
- package/dist/features/tester/schemas/firestore.js +8 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
- package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
- package/dist/features/tester/seed-data/products-tester-seed-data.js +154 -9
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +252 -15
- package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
- package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
- package/dist/index.d.ts +23 -22
- package/dist/index.js +31 -58
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +9 -2
- package/dist/next/routing/route-map.js +4 -1
- package/dist/schemas/registry.d.ts +6 -26
- package/dist/seed/bids-seed-data.js +14 -2
- package/dist/seed/blog-posts-seed-data.js +2 -1
- package/dist/seed/categories-seed-data.js +11 -2
- package/dist/seed/events-seed-data.js +11 -11
- package/dist/seed/faq-seed-data.js +5 -1
- package/dist/seed/products-auctions-seed-data.js +2 -2
- package/dist/seed/products-preorders-seed-data.d.ts +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +56 -10
- package/dist/seed/products-standard-seed-data.js +80 -3
- package/dist/seed/scammers-seed-data.js +75 -2
- package/dist/seed/site-settings-seed-data.js +5 -2
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +4 -20
- package/dist/server.js +10 -57
- package/dist/styles.css +1 -277
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +1 -1
- package/dist/ui/components/ImageLightbox.d.ts +9 -0
- package/dist/ui/components/ImageLightbox.js +6 -3
- package/dist/ui/components/ListingToolbar.d.ts +18 -1
- package/dist/ui/components/ListingToolbar.js +5 -2
- package/dist/ui/components/StickyToolbar.js +13 -8
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
4
5
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
5
6
|
import { isAuthError } from "../../../utils/auth-error";
|
|
6
7
|
import { Button, Div, LoginRequiredModal, Modal, Row, Span, Stack, Text } from "../../../ui";
|
|
@@ -8,6 +9,7 @@ import { Form } from "../../../ui/components/Form";
|
|
|
8
9
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
9
10
|
import { applyZodIssues } from "../../../ui/forms/FormShell";
|
|
10
11
|
import { placeBidSchema } from "../schemas/bid-input";
|
|
12
|
+
import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
|
|
11
13
|
import { normalizeError } from "../../../errors/normalize";
|
|
12
14
|
const __P = {
|
|
13
15
|
p5: "p-[var(--appkit-space-5)]",
|
|
@@ -19,7 +21,13 @@ const BID_ERROR_DISPLAY = {
|
|
|
19
21
|
BID_SELF_BID: "You cannot bid on your own listing.",
|
|
20
22
|
BID_USER_BANNED: "Your account is currently restricted from bidding. Contact support.",
|
|
21
23
|
};
|
|
22
|
-
export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted = false, bidCount, tags = [], onPlaceBid, onBuyNow, }) {
|
|
24
|
+
export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted = false, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
// Live-updates while the bid card is open — reflects other bidders'
|
|
27
|
+
// activity on this auction, not just the bidder's own submission.
|
|
28
|
+
const live = useLiveAuctionBid(productId, ssrCurrentBid, ssrBidCount, { enabled: !isEnded });
|
|
29
|
+
const currentBid = live.currentBid;
|
|
30
|
+
const bidCount = live.bidCount;
|
|
23
31
|
const minBid = currentBid + minBidIncrement;
|
|
24
32
|
const [bidAmount, setBidAmount] = useState(String(minBid));
|
|
25
33
|
const [stepMul, setStepMul] = useState(1);
|
|
@@ -44,6 +52,12 @@ export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidI
|
|
|
44
52
|
setSuccess(true);
|
|
45
53
|
setBidAmount(String(amount + minBidIncrement));
|
|
46
54
|
setStepMul(1);
|
|
55
|
+
// The bid history list and any duplicated static current-bid/bid-count
|
|
56
|
+
// text elsewhere on this SSR'd page (info panel, page-level summary
|
|
57
|
+
// cards) are frozen server props — refresh so they reflect this bid
|
|
58
|
+
// immediately instead of waiting for the page's ISR window to roll
|
|
59
|
+
// over. The live price shown above already updated via SSE regardless.
|
|
60
|
+
router.refresh();
|
|
47
61
|
}
|
|
48
62
|
catch (err) {
|
|
49
63
|
void normalizeError(err);
|
|
@@ -78,6 +92,7 @@ export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidI
|
|
|
78
92
|
return;
|
|
79
93
|
}
|
|
80
94
|
setSuccess(true);
|
|
95
|
+
router.refresh();
|
|
81
96
|
}
|
|
82
97
|
catch (err) {
|
|
83
98
|
void normalizeError(err);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BidDocument } from "../schemas/firestore";
|
|
2
|
+
export interface BidListResponse {
|
|
3
|
+
items: Omit<BidDocument, "userEmail">[];
|
|
4
|
+
total: number;
|
|
5
|
+
page: number;
|
|
6
|
+
pageSize: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
hasMore: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UseBidsParams {
|
|
11
|
+
productId: string;
|
|
12
|
+
page?: number;
|
|
13
|
+
pageSize?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface UseBidsOptions {
|
|
16
|
+
initialData?: BidListResponse;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* useBids
|
|
21
|
+
*
|
|
22
|
+
* Paginated, newest-first bid history for a single auction — backs the
|
|
23
|
+
* auction detail page's "Bid History" section. `/api/bids` already sorts
|
|
24
|
+
* `bidDate DESC` server-side (see `listBidsByProduct`), so no client sort
|
|
25
|
+
* is needed here.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useBids(params: UseBidsParams, opts?: UseBidsOptions): {
|
|
28
|
+
bids: Omit<BidDocument, "userEmail">[];
|
|
29
|
+
total: number;
|
|
30
|
+
totalPages: number;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
error: Error | null;
|
|
33
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { apiClient } from "../../../http";
|
|
3
|
+
import { BID_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
4
|
+
/**
|
|
5
|
+
* useBids
|
|
6
|
+
*
|
|
7
|
+
* Paginated, newest-first bid history for a single auction — backs the
|
|
8
|
+
* auction detail page's "Bid History" section. `/api/bids` already sorts
|
|
9
|
+
* `bidDate DESC` server-side (see `listBidsByProduct`), so no client sort
|
|
10
|
+
* is needed here.
|
|
11
|
+
*/
|
|
12
|
+
export function useBids(params, opts) {
|
|
13
|
+
const { productId, page = 1, pageSize = 5 } = params;
|
|
14
|
+
const sp = new URLSearchParams();
|
|
15
|
+
sp.set("productId", productId);
|
|
16
|
+
sp.set("page", String(page));
|
|
17
|
+
sp.set("pageSize", String(pageSize));
|
|
18
|
+
const endpoint = `${BID_ENDPOINTS.LIST}?${sp.toString()}`;
|
|
19
|
+
const query = useQuery({
|
|
20
|
+
queryKey: ["bids", productId, page, pageSize],
|
|
21
|
+
queryFn: () => apiClient.get(endpoint),
|
|
22
|
+
initialData: page === 1 ? opts?.initialData : undefined,
|
|
23
|
+
enabled: opts?.enabled !== false && !!productId,
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
bids: query.data?.items ?? [],
|
|
27
|
+
total: query.data?.total ?? 0,
|
|
28
|
+
totalPages: query.data?.totalPages ?? 1,
|
|
29
|
+
isLoading: query.isLoading,
|
|
30
|
+
error: query.error,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface UseLiveAuctionBidResult {
|
|
2
|
+
currentBid: number;
|
|
3
|
+
bidCount: number;
|
|
4
|
+
/** True once the SSE channel has delivered at least one live update. */
|
|
5
|
+
isLive: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* useLiveAuctionBid
|
|
9
|
+
*
|
|
10
|
+
* Layers the SSE-driven `useRealtimeBids` channel on top of the auction
|
|
11
|
+
* detail page's SSR-rendered `currentBid`/`bidCount` (server props, frozen
|
|
12
|
+
* at request time / the page's ISR window). Falls back to those SSR values
|
|
13
|
+
* until the live channel connects and pushes its first update — this is
|
|
14
|
+
* what lets any viewer watching an auction see a new bid the instant
|
|
15
|
+
* another user places it, mirroring how the cart/wishlist badges layer a
|
|
16
|
+
* live delta on top of the last known server count.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useLiveAuctionBid(productId: string, ssrCurrentBid: number, ssrBidCount: number, opts?: {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
}): UseLiveAuctionBidResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRealtimeBids } from "./useRealtimeBids";
|
|
3
|
+
/**
|
|
4
|
+
* useLiveAuctionBid
|
|
5
|
+
*
|
|
6
|
+
* Layers the SSE-driven `useRealtimeBids` channel on top of the auction
|
|
7
|
+
* detail page's SSR-rendered `currentBid`/`bidCount` (server props, frozen
|
|
8
|
+
* at request time / the page's ISR window). Falls back to those SSR values
|
|
9
|
+
* until the live channel connects and pushes its first update — this is
|
|
10
|
+
* what lets any viewer watching an auction see a new bid the instant
|
|
11
|
+
* another user places it, mirroring how the cart/wishlist badges layer a
|
|
12
|
+
* live delta on top of the last known server count.
|
|
13
|
+
*/
|
|
14
|
+
export function useLiveAuctionBid(productId, ssrCurrentBid, ssrBidCount, opts) {
|
|
15
|
+
const enabled = opts?.enabled !== false;
|
|
16
|
+
const live = useRealtimeBids(enabled ? productId : null);
|
|
17
|
+
const isLive = live.connected && live.currentBid !== null;
|
|
18
|
+
return {
|
|
19
|
+
currentBid: isLive ? live.currentBid : ssrCurrentBid,
|
|
20
|
+
bidCount: isLive && live.bidCount !== null ? live.bidCount : ssrBidCount,
|
|
21
|
+
isLive,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checkout Value OTP — Tier PP.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `payment.otpCheckoutThreshold` (skipped for COD — see
|
|
7
|
-
* `createCheckoutOrderAction`). Deliberately a SEPARATE Firestore namespace
|
|
8
|
-
* (`checkoutValueOtps` vs `consentOtps`) rather than reusing the shipping-
|
|
9
|
-
* consent one — different purpose, different copy, independently tunable
|
|
10
|
-
* rate limits. Reuses `generateOtpCode()`/`hashOtp()` from `consent-otp.ts`
|
|
11
|
-
* rather than duplicating the crypto.
|
|
4
|
+
* Gates placing a checkout ≥ the site's `payment.otpCheckoutThreshold`
|
|
5
|
+
* (skipped for COD — see `createCheckoutOrderAction`).
|
|
12
6
|
*/
|
|
13
|
-
|
|
14
|
-
export
|
|
7
|
+
/** HMAC-SHA256 hash of an OTP code. */
|
|
8
|
+
export declare function hashOtp(code: string): string;
|
|
9
|
+
/** Generate a cryptographically-secure random 6-digit OTP string. */
|
|
10
|
+
export declare function generateOtpCode(): string;
|
|
15
11
|
/** OTP validity window (10 minutes) — matches consent-OTP's window; independently tunable if policy diverges later. */
|
|
16
12
|
export declare const CHECKOUT_VALUE_OTP_EXPIRY_MS: number;
|
|
17
13
|
export declare const CHECKOUT_VALUE_OTP_EXPIRY_MINUTES = 10;
|
|
@@ -44,3 +40,4 @@ export interface CheckoutValueOtpDoc {
|
|
|
44
40
|
export declare function enforceCheckoutValueOtpRateLimit(db: Db, uid: string): Promise<void>;
|
|
45
41
|
/** Build the checkout-value OTP WhatsApp/SMS message body. */
|
|
46
42
|
export declare function buildCheckoutValueOtpMessage(code: string, siteName: string): string;
|
|
43
|
+
export {};
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checkout Value OTP — Tier PP.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `payment.otpCheckoutThreshold` (skipped for COD — see
|
|
7
|
-
* `createCheckoutOrderAction`). Deliberately a SEPARATE Firestore namespace
|
|
8
|
-
* (`checkoutValueOtps` vs `consentOtps`) rather than reusing the shipping-
|
|
9
|
-
* consent one — different purpose, different copy, independently tunable
|
|
10
|
-
* rate limits. Reuses `generateOtpCode()`/`hashOtp()` from `consent-otp.ts`
|
|
11
|
-
* rather than duplicating the crypto.
|
|
4
|
+
* Gates placing a checkout ≥ the site's `payment.otpCheckoutThreshold`
|
|
5
|
+
* (skipped for COD — see `createCheckoutOrderAction`).
|
|
12
6
|
*/
|
|
7
|
+
import { createHmac, randomInt } from "crypto";
|
|
13
8
|
import { AuthorizationError } from "../../errors";
|
|
14
9
|
import { resolveDate } from "../../utils";
|
|
15
10
|
import { USER_COLLECTION } from "./schemas";
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const HMAC_KEY = process.env.CONSENT_OTP_HMAC_KEY ||
|
|
12
|
+
process.env.HMAC_SECRET ||
|
|
13
|
+
"consent-otp-secret";
|
|
14
|
+
/** HMAC-SHA256 hash of an OTP code. */
|
|
15
|
+
export function hashOtp(code) {
|
|
16
|
+
return createHmac("sha256", HMAC_KEY).update(code).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
/** Generate a cryptographically-secure random 6-digit OTP string. */
|
|
19
|
+
export function generateOtpCode() {
|
|
20
|
+
return String(randomInt(100000, 1000000));
|
|
21
|
+
}
|
|
18
22
|
/** OTP validity window (10 minutes) — matches consent-OTP's window; independently tunable if policy diverges later. */
|
|
19
23
|
export const CHECKOUT_VALUE_OTP_EXPIRY_MS = 10 * 60 * 1000;
|
|
20
24
|
export const CHECKOUT_VALUE_OTP_EXPIRY_MINUTES = 10;
|
|
@@ -17,7 +17,12 @@ type RouteContext = {
|
|
|
17
17
|
};
|
|
18
18
|
export interface BlogPostDetailResponse {
|
|
19
19
|
post: BlogPost;
|
|
20
|
+
/** Same category — the original single "related" signal. */
|
|
20
21
|
related: BlogPost[];
|
|
22
|
+
/** Posts sharing at least one tag. */
|
|
23
|
+
relatedByTags: BlogPost[];
|
|
24
|
+
/** Other posts by the same author. */
|
|
25
|
+
relatedByAuthor: BlogPost[];
|
|
21
26
|
}
|
|
22
27
|
export declare function GET(_request: Request, context: RouteContext): Promise<NextResponse>;
|
|
23
28
|
export {};
|
|
@@ -30,9 +30,18 @@ export async function GET(_request, context) {
|
|
|
30
30
|
}
|
|
31
31
|
// Increment view count fire-and-forget — must not block response
|
|
32
32
|
safeFireAndForget(blogRepository.incrementViews(post.id), "blog: incrementViews");
|
|
33
|
-
// Related posts: same category
|
|
34
|
-
const related =
|
|
35
|
-
|
|
33
|
+
// Related posts: same category / tag overlap / same author — latest 3 each, excluding current
|
|
34
|
+
const [related, relatedByTags, relatedByAuthor] = await Promise.all([
|
|
35
|
+
blogRepository.findRelated(post.category, post.id, 3),
|
|
36
|
+
blogRepository.findByTagsOverlap(post.tags ?? [], post.id, 3),
|
|
37
|
+
blogRepository.findByAuthor(post.authorId, post.id, 3),
|
|
38
|
+
]);
|
|
39
|
+
const body = {
|
|
40
|
+
post: toBlogPost(post),
|
|
41
|
+
related: related.map(toBlogPost),
|
|
42
|
+
relatedByTags: relatedByTags.map(toBlogPost),
|
|
43
|
+
relatedByAuthor: relatedByAuthor.map(toBlogPost),
|
|
44
|
+
};
|
|
36
45
|
return NextResponse.json({ success: true, data: body });
|
|
37
46
|
}
|
|
38
47
|
catch (error) {
|
|
@@ -14,6 +14,8 @@ export interface BlogPostViewProps {
|
|
|
14
14
|
viewsLabel?: string;
|
|
15
15
|
featured?: string;
|
|
16
16
|
relatedTitle?: string;
|
|
17
|
+
relatedByTagsTitle?: string;
|
|
18
|
+
relatedByAuthorTitle?: string;
|
|
17
19
|
};
|
|
18
20
|
/** Render the cover image */
|
|
19
21
|
renderImage?: (post: BlogPost) => React.ReactNode;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { Div, Heading, RichText, Row, Scrim, Span, Stack, Text } from "../../../ui";
|
|
6
|
+
import { Iframe } from "../../../ui/components/Iframe";
|
|
6
7
|
import { MediaImage } from "../../media/MediaImage";
|
|
7
8
|
import { useBlogPost } from "../hooks/useBlog";
|
|
8
9
|
import { BlogCard } from "./BlogListView";
|
|
@@ -27,13 +28,13 @@ const CLS_FEATURED_BADGE = "bg-warning-surface text-warning dark:bg-warning-surf
|
|
|
27
28
|
function renderBlogPostHeader(post, date, labels) {
|
|
28
29
|
return (_jsxs(Div, { className: "mb-8", children: [_jsxs(Row, { className: "mb-4", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? ""}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && _jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels?.featured ?? "Featured" })] }), _jsx(Heading, { level: 1, className: "mb-4", size: "3xl", weight: "bold", children: post.title }), post.excerpt && _jsx(Text, { className: "mb-6", color: "muted", size: "lg", children: post.excerpt }), _jsxs(Row, { color: "muted", textSize: "sm", wrap: true, gap: "md", children: [post.authorName && _jsxs(Span, { children: [labels?.author ?? "By", " ", _jsx(Span, { weight: "medium", color: "muted", children: post.authorName })] }), post.readTimeMinutes != null && _jsxs(Span, { children: [post.readTimeMinutes, " ", labels?.readTime ?? "min read"] }), date && _jsxs(Span, { children: [labels?.publishedOn ?? "Published", " ", date] }), post.views != null && _jsxs(Span, { children: [post.views, " ", labels?.viewsLabel ?? "views"] })] })] }));
|
|
29
30
|
}
|
|
30
|
-
function renderBlogPostRelated(related,
|
|
31
|
+
function renderBlogPostRelated(related, title, renderRelatedCard) {
|
|
31
32
|
if (related.length === 0)
|
|
32
33
|
return null;
|
|
33
|
-
return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children:
|
|
34
|
+
return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children: title }), _jsx(Div, { layout: "grid", gap: "6", className: "sm:grid-cols-3", children: related.map((rel, i) => renderRelatedCard ? (_jsx(React.Fragment, { children: renderRelatedCard(rel, i) }, rel.id)) : (_jsx(BlogCard, { post: rel, href: String(ROUTES.BLOG.ARTICLE(rel.slug)) }, rel.id))) })] }));
|
|
34
35
|
}
|
|
35
36
|
export function BlogPostView({ slug, initialData, labels = {}, renderImage, renderContent, renderBackButton, renderLoading, renderError, renderRelatedCard, renderAuthorBio, className = "", }) {
|
|
36
|
-
const { post, related, isLoading, error } = useBlogPost(slug, {
|
|
37
|
+
const { post, related, relatedByTags, relatedByAuthor, isLoading, error } = useBlogPost(slug, {
|
|
37
38
|
initialData,
|
|
38
39
|
});
|
|
39
40
|
if (isLoading) {
|
|
@@ -54,5 +55,5 @@ export function BlogPostView({ slug, initialData, labels = {}, renderImage, rend
|
|
|
54
55
|
})
|
|
55
56
|
: "";
|
|
56
57
|
const coverImageUrl = getMediaUrl(post.coverImage);
|
|
57
|
-
return (_jsxs(Div, { className: `min-h-screen ${className}`, children: [coverImageUrl && (_jsxs(Div, { className: `relative h-72 md:h-96 ${__O.hidden}`, children: [renderImage ? (renderImage(post)) : (_jsx(MediaImage, { src: coverImageUrl, alt: post.title, size: "hero" })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-0" })] })), _jsxs(Div, { className: "max-w-3xl mx-auto", paddingY: "y-3xl", paddingX: "x-md", children: [renderBlogPostHeader(post, date, labels), post.tags && post.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-8", children: post.tags.map((tag) => (_jsxs(Link, { href: `${String(ROUTES.PUBLIC.BLOG)}?tags=${encodeURIComponent(tag)}`, className: "inline-block px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] hover:bg-primary/10 text-[var(--appkit-color-text-muted)] hover:text-primary text-[length:var(--appkit-text-xs)] font-medium transition-colors", children: ["#", tag] }, tag))) })), renderAuthorBio && _jsx(Div, { className: "mb-6", children: renderAuthorBio(post) }), _jsx(Div, { surface: "card", padding: "xl", className: "mb-12", children: renderContent ? renderContent(post) : (_jsx(RichText, { html: normalizeRichTextHtml(post.content ?? ""), proseClass: "prose max-w-none dark:prose-invert" })) }), renderBlogPostRelated(related, labels, renderRelatedCard), renderBackButton && _jsx(Div, { border: "default", className: "mt-10 border-t", padding: "t-xl", children: renderBackButton() })] })] }));
|
|
58
|
+
return (_jsxs(Div, { className: `min-h-screen ${className}`, children: [coverImageUrl && (_jsxs(Div, { className: `relative h-72 md:h-96 ${__O.hidden}`, children: [renderImage ? (renderImage(post)) : (_jsx(MediaImage, { src: coverImageUrl, alt: post.title, size: "hero" })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-0" })] })), _jsxs(Div, { className: "max-w-3xl mx-auto", paddingY: "y-3xl", paddingX: "x-md", children: [renderBlogPostHeader(post, date, labels), post.youtubeId && /^[A-Za-z0-9_-]{6,20}$/.test(post.youtubeId) && (_jsx(Div, { className: "mb-8", children: _jsx(Iframe, { src: `https://www.youtube-nocookie.com/embed/${post.youtubeId}`, title: `${post.title} — video`, aspect: "video", rounded: "lg" }) })), post.tags && post.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-8", children: post.tags.map((tag) => (_jsxs(Link, { href: `${String(ROUTES.PUBLIC.BLOG)}?tags=${encodeURIComponent(tag)}`, className: "inline-block px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] hover:bg-primary/10 text-[var(--appkit-color-text-muted)] hover:text-primary text-[length:var(--appkit-text-xs)] font-medium transition-colors", children: ["#", tag] }, tag))) })), renderAuthorBio && _jsx(Div, { className: "mb-6", children: renderAuthorBio(post) }), _jsx(Div, { surface: "card", padding: "xl", className: "mb-12", children: renderContent ? renderContent(post) : (_jsx(RichText, { html: normalizeRichTextHtml(post.content ?? ""), proseClass: "prose max-w-none dark:prose-invert" })) }), _jsxs(Stack, { gap: "xl", children: [renderBlogPostRelated(related, labels?.relatedTitle ?? "Related Posts", renderRelatedCard), renderBlogPostRelated(relatedByTags, labels?.relatedByTagsTitle ?? "You might also like", renderRelatedCard), renderBlogPostRelated(relatedByAuthor, labels?.relatedByAuthorTitle ?? `More from ${post.authorName}`, renderRelatedCard)] }), renderBackButton && _jsx(Div, { border: "default", className: "mt-10 border-t", padding: "t-xl", children: renderBackButton() })] })] }));
|
|
58
59
|
}
|
|
@@ -24,6 +24,8 @@ interface UseBlogPostOptions {
|
|
|
24
24
|
export declare function useBlogPost(slug: string, opts?: UseBlogPostOptions): {
|
|
25
25
|
post: import("..").BlogPost | null;
|
|
26
26
|
related: import("..").BlogPost[];
|
|
27
|
+
relatedByTags: import("..").BlogPost[];
|
|
28
|
+
relatedByAuthor: import("..").BlogPost[];
|
|
27
29
|
isLoading: boolean;
|
|
28
30
|
error: Error | null;
|
|
29
31
|
};
|
|
@@ -46,6 +46,8 @@ export function useBlogPost(slug, opts) {
|
|
|
46
46
|
return {
|
|
47
47
|
post: query.data?.post ?? null,
|
|
48
48
|
related: query.data?.related ?? [],
|
|
49
|
+
relatedByTags: query.data?.relatedByTags ?? [],
|
|
50
|
+
relatedByAuthor: query.data?.relatedByAuthor ?? [],
|
|
49
51
|
isLoading: query.isLoading,
|
|
50
52
|
error: query.error,
|
|
51
53
|
};
|
|
@@ -10,6 +10,13 @@ declare class BlogRepository extends BaseRepository<BlogPostDocument> {
|
|
|
10
10
|
delete(id: string): Promise<void>;
|
|
11
11
|
incrementViews(id: string): Promise<void>;
|
|
12
12
|
findRelated(category: BlogPostCategory, excludeId: string, limit?: number): Promise<BlogPostDocument[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Published posts sharing at least one tag with the given list.
|
|
15
|
+
* `array-contains-any` caps at 10 values, so only the first 10 tags are used.
|
|
16
|
+
*/
|
|
17
|
+
findByTagsOverlap(tags: string[], excludeId: string, limit?: number): Promise<BlogPostDocument[]>;
|
|
18
|
+
/** Other published posts by the same author. */
|
|
19
|
+
findByAuthor(authorId: string, excludeId: string, limit?: number): Promise<BlogPostDocument[]>;
|
|
13
20
|
static readonly SIEVE_FIELDS: {
|
|
14
21
|
id: {
|
|
15
22
|
canFilter: boolean;
|
|
@@ -95,6 +95,50 @@ class BlogRepository extends BaseRepository {
|
|
|
95
95
|
throw new DatabaseError(`Failed to find related posts: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Published posts sharing at least one tag with the given list.
|
|
100
|
+
* `array-contains-any` caps at 10 values, so only the first 10 tags are used.
|
|
101
|
+
*/
|
|
102
|
+
async findByTagsOverlap(tags, excludeId, limit = 3) {
|
|
103
|
+
if (tags.length === 0)
|
|
104
|
+
return [];
|
|
105
|
+
try {
|
|
106
|
+
const snapshot = await this.db
|
|
107
|
+
.collection(this.collection)
|
|
108
|
+
.where(BLOG_POST_FIELDS.STATUS, "==", "published")
|
|
109
|
+
.where(BLOG_POST_FIELDS.TAGS, "array-contains-any", tags.slice(0, 10))
|
|
110
|
+
.limit(limit + 1)
|
|
111
|
+
.get();
|
|
112
|
+
return snapshot.docs
|
|
113
|
+
.map((doc) => this.mapDoc(doc))
|
|
114
|
+
.filter((post) => post.id !== excludeId)
|
|
115
|
+
.slice(0, limit);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
void normalizeError(error);
|
|
119
|
+
throw new DatabaseError(`Failed to find posts by tag overlap: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Other published posts by the same author. */
|
|
123
|
+
async findByAuthor(authorId, excludeId, limit = 3) {
|
|
124
|
+
try {
|
|
125
|
+
const snapshot = await this.db
|
|
126
|
+
.collection(this.collection)
|
|
127
|
+
.where(BLOG_POST_FIELDS.STATUS, "==", "published")
|
|
128
|
+
.where(BLOG_POST_FIELDS.AUTHOR_ID, "==", authorId)
|
|
129
|
+
.orderBy(BLOG_POST_FIELDS.PUBLISHED_AT, "desc")
|
|
130
|
+
.limit(limit + 1)
|
|
131
|
+
.get();
|
|
132
|
+
return snapshot.docs
|
|
133
|
+
.map((doc) => this.mapDoc(doc))
|
|
134
|
+
.filter((post) => post.id !== excludeId)
|
|
135
|
+
.slice(0, limit);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
void normalizeError(error);
|
|
139
|
+
throw new DatabaseError(`Failed to find posts by author: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
98
142
|
async listPublished(opts, model) {
|
|
99
143
|
let baseQuery = this.getCollection().where(BLOG_POST_FIELDS.STATUS, "==", "published");
|
|
100
144
|
if (opts?.category) {
|
|
@@ -22,6 +22,8 @@ export interface BlogPostDocument extends BaseDocument {
|
|
|
22
22
|
coverImage?: MediaFieldInput;
|
|
23
23
|
contentImages?: MediaField[];
|
|
24
24
|
additionalImages?: MediaField[];
|
|
25
|
+
/** YouTube video ID (11 chars, e.g. "dQw4w9WgXcQ") embedded above the article content. */
|
|
26
|
+
youtubeId?: string;
|
|
25
27
|
category: BlogPostCategory;
|
|
26
28
|
tags: string[];
|
|
27
29
|
isFeatured: boolean;
|
|
@@ -46,6 +48,7 @@ export declare const BLOG_POST_FIELDS: {
|
|
|
46
48
|
readonly COVER_IMAGE: "coverImage";
|
|
47
49
|
readonly CONTENT_IMAGES: "contentImages";
|
|
48
50
|
readonly ADDITIONAL_IMAGES: "additionalImages";
|
|
51
|
+
readonly YOUTUBE_ID: "youtubeId";
|
|
49
52
|
readonly CATEGORY: "category";
|
|
50
53
|
readonly TAGS: "tags";
|
|
51
54
|
readonly IS_FEATURED: "isFeatured";
|
|
@@ -10,6 +10,8 @@ export interface BlogPost {
|
|
|
10
10
|
coverImage?: MediaFieldInput;
|
|
11
11
|
contentImages?: MediaField[];
|
|
12
12
|
additionalImages?: MediaField[];
|
|
13
|
+
/** YouTube video ID (11 chars, e.g. "dQw4w9WgXcQ") embedded above the article content. */
|
|
14
|
+
youtubeId?: string;
|
|
13
15
|
category: BlogPostCategory;
|
|
14
16
|
tags?: string[];
|
|
15
17
|
isFeatured?: boolean;
|
|
@@ -7,8 +7,6 @@ export { CheckoutView } from "./CheckoutView";
|
|
|
7
7
|
export type { CheckoutViewProps } from "./CheckoutView";
|
|
8
8
|
export { CheckoutAddressStep } from "./CheckoutAddressStep";
|
|
9
9
|
export type { CheckoutAddressStepProps, CheckoutAddressStepLabels, } from "./CheckoutAddressStep";
|
|
10
|
-
export { CheckoutOtpModal } from "./CheckoutOtpModal";
|
|
11
|
-
export type { CheckoutOtpModalProps } from "./CheckoutOtpModal";
|
|
12
10
|
export { CheckoutSuccessView } from "./CheckoutSuccessView";
|
|
13
11
|
export type { CheckoutSuccessViewProps } from "./CheckoutSuccessView";
|
|
14
12
|
export { ShippingPicker } from "./ShippingPicker";
|
|
@@ -3,6 +3,5 @@ export { CartView } from "./CartView";
|
|
|
3
3
|
export { CartSummary } from "./CartSummary";
|
|
4
4
|
export { CheckoutView } from "./CheckoutView";
|
|
5
5
|
export { CheckoutAddressStep } from "./CheckoutAddressStep";
|
|
6
|
-
export { CheckoutOtpModal } from "./CheckoutOtpModal";
|
|
7
6
|
export { CheckoutSuccessView } from "./CheckoutSuccessView";
|
|
8
7
|
export { ShippingPicker } from "./ShippingPicker";
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
1
3
|
import { useQuery } from "@tanstack/react-query";
|
|
2
4
|
import { apiClient } from "../../../http";
|
|
3
5
|
import { useGuestCart } from "./useGuestCart";
|
|
4
6
|
import { CART_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
+
import { CART_OPS_CHANGE_EVENT, getCartOpsDelta } from "../utils/pending-ops";
|
|
5
8
|
/**
|
|
6
9
|
* useCartCount
|
|
7
10
|
*
|
|
@@ -16,11 +19,25 @@ export function useCartCount(enabled = false) {
|
|
|
16
19
|
queryFn: () => apiClient.get(CART_ENDPOINTS.GET),
|
|
17
20
|
enabled,
|
|
18
21
|
});
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
22
|
+
// "Add to cart" quick actions (listing grid, product cards) write local-
|
|
23
|
+
// first — see useSyncManager — and queue an unsynced op that only reaches
|
|
24
|
+
// the server on the next ~30s background sync. Layer that pending delta on
|
|
25
|
+
// top of the server count so the badge updates the instant the op is
|
|
26
|
+
// queued instead of lagging up to 30s behind the toast.
|
|
27
|
+
const [pendingDelta, setPendingDelta] = useState(0);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!enabled)
|
|
30
|
+
return;
|
|
31
|
+
const recompute = () => setPendingDelta(getCartOpsDelta());
|
|
32
|
+
recompute();
|
|
33
|
+
window.addEventListener(CART_OPS_CHANGE_EVENT, recompute);
|
|
34
|
+
return () => window.removeEventListener(CART_OPS_CHANGE_EVENT, recompute);
|
|
35
|
+
}, [enabled]);
|
|
36
|
+
// When authenticated, the server + pending-ops delta is authoritative —
|
|
37
|
+
// never mix in the guest count, otherwise stale localStorage entries leak
|
|
38
|
+
// into the badge after login (the cart-merge clears the local store but
|
|
39
|
+
// `useGuestCart`'s React state holds the pre-clear snapshot until remount).
|
|
23
40
|
if (enabled)
|
|
24
|
-
return data?.itemCount ?? 0;
|
|
41
|
+
return (data?.itemCount ?? 0) + pendingDelta;
|
|
25
42
|
return guestCount;
|
|
26
43
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useState, useEffect, useCallback } from "react";
|
|
3
|
-
import { getGuestCartItems, addToGuestCart, clearGuestCart, removeFromGuestCart, updateGuestCartQuantity, } from "../utils/guest-cart";
|
|
3
|
+
import { getGuestCartItems, addToGuestCart, clearGuestCart, removeFromGuestCart, updateGuestCartQuantity, GUEST_CART_CHANGE_EVENT, } from "../utils/guest-cart";
|
|
4
4
|
/**
|
|
5
5
|
* useGuestCart
|
|
6
6
|
*
|
|
@@ -10,6 +10,16 @@ export function useGuestCart() {
|
|
|
10
10
|
const [items, setItems] = useState([]);
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
setItems(getGuestCartItems());
|
|
13
|
+
// Cross-instance sync within the same tab (e.g. a listing-page "Add to
|
|
14
|
+
// cart" quick action writing storage while the header badge's own
|
|
15
|
+
// useGuestCart() instance is mounted elsewhere) + cross-tab via "storage".
|
|
16
|
+
const onChange = () => setItems(getGuestCartItems());
|
|
17
|
+
window.addEventListener(GUEST_CART_CHANGE_EVENT, onChange);
|
|
18
|
+
window.addEventListener("storage", onChange);
|
|
19
|
+
return () => {
|
|
20
|
+
window.removeEventListener(GUEST_CART_CHANGE_EVENT, onChange);
|
|
21
|
+
window.removeEventListener("storage", onChange);
|
|
22
|
+
};
|
|
13
23
|
}, []);
|
|
14
24
|
const add = useCallback((productId, quantity, snapshot) => {
|
|
15
25
|
const updated = addToGuestCart(productId, quantity, snapshot);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Fired whenever guest cart storage is written, so every `useGuestCart()`
|
|
2
|
+
* instance in the tab (header badge, listing quick-add, cart page) can
|
|
3
|
+
* re-read storage and stay in sync without waiting for a remount. */
|
|
4
|
+
export declare const GUEST_CART_CHANGE_EVENT = "appkit/cart/guest-changed";
|
|
1
5
|
export declare class CartFullError extends Error {
|
|
2
6
|
readonly code: "CART_FULL";
|
|
3
7
|
readonly limit = 50;
|
|
@@ -6,6 +6,15 @@ const DEFAULT_GUEST_CART_KEY = process.env.NEXT_PUBLIC_APP_ID
|
|
|
6
6
|
const DEFAULT_GUEST_RETURN_TO_KEY = process.env.NEXT_PUBLIC_APP_ID
|
|
7
7
|
? `${process.env.NEXT_PUBLIC_APP_ID}_guest_return_to`
|
|
8
8
|
: "guest_return_to";
|
|
9
|
+
/** Fired whenever guest cart storage is written, so every `useGuestCart()`
|
|
10
|
+
* instance in the tab (header badge, listing quick-add, cart page) can
|
|
11
|
+
* re-read storage and stay in sync without waiting for a remount. */
|
|
12
|
+
export const GUEST_CART_CHANGE_EVENT = "appkit/cart/guest-changed";
|
|
13
|
+
function dispatchGuestCartChange() {
|
|
14
|
+
if (typeof window === "undefined")
|
|
15
|
+
return;
|
|
16
|
+
window.dispatchEvent(new Event(GUEST_CART_CHANGE_EVENT));
|
|
17
|
+
}
|
|
9
18
|
export class CartFullError extends Error {
|
|
10
19
|
constructor(current) {
|
|
11
20
|
super(`Cart full (${current}/${CART_MAX_ITEMS})`);
|
|
@@ -39,6 +48,7 @@ function writeItems(storage, key, items) {
|
|
|
39
48
|
if (!storage)
|
|
40
49
|
return;
|
|
41
50
|
storage.setItem(key, JSON.stringify(items));
|
|
51
|
+
dispatchGuestCartChange();
|
|
42
52
|
}
|
|
43
53
|
export function getGuestCartItems(storage = getDefaultStorage(), key = DEFAULT_GUEST_CART_KEY) {
|
|
44
54
|
return readItems(storage, key);
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* Written to localStorage so they survive page refreshes.
|
|
6
6
|
* The sync manager replays these and clears them after a successful push.
|
|
7
7
|
*/
|
|
8
|
+
/** Fired whenever the pending cart/wishlist op queue changes, so a badge
|
|
9
|
+
* reading server-authoritative counts (authenticated users) can layer the
|
|
10
|
+
* not-yet-synced local delta on top instead of waiting for the next
|
|
11
|
+
* useSyncManager interval (up to 30s) to reflect a just-added item. */
|
|
12
|
+
export declare const CART_OPS_CHANGE_EVENT = "appkit/cart/ops-changed";
|
|
13
|
+
export declare const WISHLIST_OPS_CHANGE_EVENT = "appkit/wishlist/ops-changed";
|
|
8
14
|
export type CartOpKind = "add" | "remove";
|
|
9
15
|
export type WishlistOpKind = "add" | "remove";
|
|
10
16
|
export interface CartOp {
|
|
@@ -34,6 +40,17 @@ export interface WishlistOp {
|
|
|
34
40
|
export declare function getCartOps(): CartOp[];
|
|
35
41
|
export declare function pushCartOp(op: Omit<CartOp, "ts">): void;
|
|
36
42
|
export declare function clearCartOps(): void;
|
|
43
|
+
/** Net quantity of unsynced "add" ops — layered on top of the server's
|
|
44
|
+
* authoritative itemCount so the header badge updates immediately instead
|
|
45
|
+
* of waiting for the next useSyncManager interval. Cart never queues
|
|
46
|
+
* "remove" ops (removal from an authenticated cart is always a synchronous
|
|
47
|
+
* API call), so summing "add" quantities is exact, not an approximation. */
|
|
48
|
+
export declare function getCartOpsDelta(): number;
|
|
37
49
|
export declare function getWishlistOps(): WishlistOp[];
|
|
38
50
|
export declare function pushWishlistOp(op: Omit<WishlistOp, "ts">): void;
|
|
51
|
+
/** Net count of unsynced wishlist ops — add/remove pairs for the same item
|
|
52
|
+
* are already collapsed by `pushWishlistOp`, so this is exact: +1 per
|
|
53
|
+
* queued "add", -1 per queued "remove" (an item already on the server
|
|
54
|
+
* being removed before the next sync). */
|
|
55
|
+
export declare function getWishlistOpsDelta(): number;
|
|
39
56
|
export declare function clearWishlistOps(): void;
|