@mohasinac/appkit 4.30.16 → 4.30.17
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/shared/types/action-error-reporter.d.ts +40 -0
- package/dist/_internal/shared/types/action-error-reporter.js +27 -0
- package/dist/_internal/shared/types/action-result.d.ts +10 -0
- package/dist/_internal/shared/types/action-result.js +26 -0
- package/dist/client/api/surface-error.d.ts +6 -4
- package/dist/client/api/surface-error.js +19 -8
- package/dist/contracts/realtime-session.d.ts +27 -0
- package/dist/contracts/realtime-session.js +118 -0
- package/dist/core/unit-of-work.js +21 -13
- package/dist/errors/base-error.js +14 -1
- package/dist/errors/database-error.js +11 -1
- package/dist/errors/describe-cause.d.ts +5 -0
- package/dist/errors/describe-cause.js +76 -0
- package/dist/errors/error-display-map.d.ts +32 -1
- package/dist/errors/error-display-map.js +46 -0
- package/dist/errors/error-mapping.d.ts +47 -7
- package/dist/errors/error-mapping.js +46 -5
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +4 -0
- package/dist/features/about/components/PublicProfileView.js +4 -4
- package/dist/features/account/components/AddressBook.js +2 -2
- package/dist/features/account/components/AddressesIndexListing.js +2 -2
- package/dist/features/admin/components/AdminAnalyticsView.js +2 -2
- package/dist/features/admin/components/AdminCouponsView.js +2 -2
- package/dist/features/admin/components/AdminGuideHubView.js +2 -2
- package/dist/features/admin/components/AdminMediaView.js +2 -2
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionGrid.d.ts +1 -2
- package/dist/features/auctions/components/MarketplaceAuctionGrid.js +8 -7
- package/dist/features/auctions/components/PlaceBidFormClient.js +32 -15
- package/dist/features/before-after/components/BeforeAfterGallery.js +2 -2
- package/dist/features/blog/components/BlogIndexListing.js +3 -3
- package/dist/features/blog/components/BlogListView.js +3 -3
- package/dist/features/blog/components/BlogPostView.js +2 -2
- package/dist/features/categories/components/BundleCollage.js +2 -2
- package/dist/features/categories/components/CategoriesIndexListing.js +3 -3
- package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
- package/dist/features/categories/components/CategoryGrid.js +2 -2
- package/dist/features/categories/components/CategoryProductsListing.js +2 -2
- package/dist/features/categories/components/CategoryStoresListing.js +2 -2
- package/dist/features/collections/components/CollectionCard.js +2 -2
- package/dist/features/events/components/EventRafflesSection.js +2 -2
- package/dist/features/events/components/EventsIndexListing.js +2 -2
- package/dist/features/events/components/EventsListView.js +3 -3
- package/dist/features/homepage/components/FeaturedBundlesSection.js +2 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
- package/dist/features/homepage/components/HomepageSkeleton.js +1 -1
- package/dist/features/homepage/components/SecurityHighlightsSection.js +1 -1
- package/dist/features/homepage/components/SocialFeedSection.js +2 -2
- package/dist/features/homepage/components/StatsCounterSection.js +1 -1
- package/dist/features/homepage/components/TrustBadges.js +2 -2
- package/dist/features/homepage/components/TrustFeaturesSection.js +1 -1
- package/dist/features/messages/hooks/useConversation.js +39 -15
- package/dist/features/messages/hooks/useConversations.js +36 -14
- package/dist/features/messages/repository/conversations.repository.d.ts +23 -0
- package/dist/features/messages/repository/conversations.repository.js +53 -0
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -3
- package/dist/features/products/components/AuctionsIndexListing.js +2 -3
- package/dist/features/products/components/PrizeDrawCollage.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -3
- package/dist/features/products/components/PrizeDrawsSection.js +2 -2
- package/dist/features/products/components/ProductGrid.js +2 -7
- package/dist/features/products/components/ProductsIndexListing.js +3 -3
- package/dist/features/products/components/RelatedProducts.js +2 -2
- package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -2
- package/dist/features/reviews/components/ReviewsListingPanel.js +2 -2
- package/dist/features/seller/components/SellerCouponsView.js +2 -2
- package/dist/features/seller/components/SellerDashboardView.js +3 -3
- package/dist/features/server-errors/schemas/firestore.d.ts +9 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -3
- package/dist/features/stores/components/StoreClassifiedsListing.js +2 -3
- package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -3
- package/dist/features/stores/components/StoreGuideHubView.js +2 -2
- package/dist/features/stores/components/StoreLiveItemsListing.js +2 -3
- package/dist/features/stores/components/StorePreOrdersListing.js +2 -3
- package/dist/features/stores/components/StoreProductsListing.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +3 -3
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +54 -1
- package/dist/next/api/routeHandler.js +27 -7
- package/dist/providers/db-firebase/base.repository.js +34 -18
- package/dist/react/hooks/useRealtimeEvent.js +22 -4
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +84 -20
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/tokens.css +7 -2
- package/dist/ui/components/Layout.d.ts +31 -18
- package/dist/ui/components/Layout.js +30 -19
- package/dist/ui/components/Layout.style.css +76 -17
- package/dist/ui/components/ListingLayout.d.ts +2 -0
- package/dist/ui/components/ListingLayout.js +16 -2
- package/dist/ui/components/StickyToolbar.js +3 -48
- package/dist/ui/components/toolbar-collapse.d.ts +44 -0
- package/dist/ui/components/toolbar-collapse.js +93 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { Article, Button, Div, Heading, Pagination, Row, Span, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Article, Button, Div, Grid, Heading, Pagination, Row, Span, Stack, Text } from "../../../ui";
|
|
5
5
|
import { MediaImage } from "../../media/MediaImage";
|
|
6
6
|
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
7
7
|
import { getMediaUrl } from "../../media/types/index";
|
|
@@ -37,7 +37,7 @@ export function BlogCategoryTabs({ categories, active, onSelect, labels = {}, })
|
|
|
37
37
|
}
|
|
38
38
|
export function BlogListView({ posts, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onPostClick, emptyLabel = "No posts found", slots, }) {
|
|
39
39
|
if (isLoading) {
|
|
40
|
-
return (_jsx(
|
|
40
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: "animate-pulse overflow-hidden border border-neutral-200 bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsx(Div, { className: "aspect-video bg-neutral-200" }), _jsxs(Stack, { className: `${__P.p5}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-16 bg-neutral-200", rounded: "default" }), _jsx(Div, { className: "h-5 w-full bg-neutral-200", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4 bg-neutral-200", rounded: "default" })] })] }, i))) }));
|
|
41
41
|
}
|
|
42
42
|
if (posts.length === 0) {
|
|
43
43
|
if (slots?.renderEmptyState) {
|
|
@@ -47,7 +47,7 @@ export function BlogListView({ posts, isLoading, totalPages = 1, currentPage = 1
|
|
|
47
47
|
}
|
|
48
48
|
return (_jsxs(Stack, { gap: "xl", children: [slots?.renderHeader
|
|
49
49
|
? slots.renderHeader({ total })
|
|
50
|
-
: null, _jsx(
|
|
50
|
+
: null, _jsx(Grid, { cols: "cards", gap: "lg", children: posts.map((post, i) => slots?.renderCard ? (_jsx(React.Fragment, { children: slots.renderCard(post, i) }, post.id)) : (_jsx(BlogCard, { post: post, onClick: onPostClick }, post.id))) }), slots?.renderFooter ? slots.renderFooter({
|
|
51
51
|
page: currentPage,
|
|
52
52
|
totalPages,
|
|
53
53
|
}) : totalPages > 1 && onPageChange ? (_jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) })) : null] }));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { Div, Heading, RichText, Row, Scrim, Span, Stack, Text } from "../../../ui";
|
|
5
|
+
import { Div, Grid, Heading, RichText, Row, Scrim, Span, Stack, Text } from "../../../ui";
|
|
6
6
|
import { Iframe } from "../../../ui/components/Iframe";
|
|
7
7
|
import { MediaImage } from "../../media/MediaImage";
|
|
8
8
|
import { useBlogPost } from "../hooks/useBlog";
|
|
@@ -31,7 +31,7 @@ function renderBlogPostHeader(post, date, labels) {
|
|
|
31
31
|
function renderBlogPostRelated(related, title, renderRelatedCard) {
|
|
32
32
|
if (related.length === 0)
|
|
33
33
|
return null;
|
|
34
|
-
return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children: title }), _jsx(
|
|
34
|
+
return (_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "mb-6", size: "xl", weight: "semibold", children: title }), _jsx(Grid, { cols: "cards", gap: "lg", 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))) })] }));
|
|
35
35
|
}
|
|
36
36
|
export function BlogPostView({ slug, initialData, labels = {}, renderImage, renderContent, renderBackButton, renderLoading, renderError, renderRelatedCard, renderAuthorBio, className = "", }) {
|
|
37
37
|
const { post, related, relatedByTags, relatedByAuthor, isLoading, error } = useBlogPost(slug, {
|
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
*/
|
|
11
11
|
import { useState } from "react";
|
|
12
12
|
import Link from "next/link";
|
|
13
|
-
import { Button, Div, Row, Text } from "../../../ui";
|
|
13
|
+
import { Button, Div, Grid, Row, Text } from "../../../ui";
|
|
14
14
|
import { MediaImage } from "../../media/MediaImage";
|
|
15
15
|
import { ImageLightbox } from "../../../ui/components/ImageLightbox";
|
|
16
16
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
@@ -40,7 +40,7 @@ export function BundleCollage({ members, onItemClick }) {
|
|
|
40
40
|
if (!members.length)
|
|
41
41
|
return null;
|
|
42
42
|
const galleryImages = toGalleryImages(members);
|
|
43
|
-
return (_jsxs(_Fragment, { children: [_jsx(
|
|
43
|
+
return (_jsxs(_Fragment, { children: [_jsx(Grid, { cols: "categoryCards", gap: "3", children: members.map((p, idx) => {
|
|
44
44
|
const cover = p.mainImage ?? p.images?.[0];
|
|
45
45
|
const href = String(ROUTES.PUBLIC.PRODUCT_DETAIL?.(p.slug ?? p.id) ?? "#");
|
|
46
46
|
return (_jsxs(Div, { layout: "flex-col", className: "group relative overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] transition-transform hover:scale-[1.01]", rounded: "lg", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: makeBundleItemClickHandler(p, idx, onItemClick, setLightboxIndex), paddingX: "none", paddingY: "none", rounded: "none", className: "relative aspect-square w-full overflow-hidden focus-visible:ring-2 focus-visible:ring-[var(--appkit-color-primary)]", "aria-label": `View ${p.title} in lightbox`, children: _jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (_jsx(MediaImage, { src: cover, alt: p.title, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", surface: "muted", className: "absolute inset-0", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), _jsxs(Div, { textWeight: "semibold", textSize: "xs", surface: "overlay-xl", padding: "chip-2xs", className: "absolute left-2 top-2 text-white", rounded: "default", children: ["#", idx + 1] })] }) }), _jsxs(Div, { padding: "xs", children: [_jsx(Link, { href: href, className: "line-clamp-2 text-[length:var(--appkit-text-sm)] font-medium hover:underline", children: p.title }), _jsx(Text, { className: "mt-0.5 text-[var(--appkit-color-text-muted)]", size: "xs", children: formatCurrency(p.price ?? 0, p.currency ?? "INR") })] })] }, p.id));
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useCallback, useMemo } from "react";
|
|
4
4
|
import { useCategoriesFiltered } from "../hooks/useCategories";
|
|
5
5
|
import { ROUTES } from "../../../next";
|
|
6
|
-
import { Button, Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
|
|
6
|
+
import { Button, Div, Grid, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
|
|
7
7
|
import { CategoryCard } from "./CategoryGrid";
|
|
8
8
|
import { CategoryFilters } from "./CategoryFilters";
|
|
9
9
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
@@ -21,7 +21,7 @@ function renderCategoryGrid(props) {
|
|
|
21
21
|
const { isLoading, categories, view, activeSearch, activeTab, brandsOnly } = props;
|
|
22
22
|
const isBrandView = activeTab === "brands" || brandsOnly;
|
|
23
23
|
if (isLoading) {
|
|
24
|
-
return (_jsx(
|
|
24
|
+
return (_jsx(Grid, { cols: "categoryCards", gap: "md", children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-[4/3]", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}.5`, gap: "sm", children: [_jsx(Div, { className: "h-3.5 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" })] })] }, i))) }));
|
|
25
25
|
}
|
|
26
26
|
if (categories.length === 0) {
|
|
27
27
|
return (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: activeSearch ? `No ${isBrandView ? "brands" : "categories"} matching "${activeSearch}"` : isBrandView ? "No brands found" : "No categories found" }));
|
|
@@ -29,7 +29,7 @@ function renderCategoryGrid(props) {
|
|
|
29
29
|
if (view === "list") {
|
|
30
30
|
return (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: categories.map((category) => _jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id)) }));
|
|
31
31
|
}
|
|
32
|
-
return (_jsx(
|
|
32
|
+
return (_jsx(Grid, { cols: "categoryCards", gap: "md", children: categories.map((category) => _jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id)) }));
|
|
33
33
|
}
|
|
34
34
|
function renderCategoryFilterDrawer(props) {
|
|
35
35
|
const { filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable } = props;
|
|
@@ -10,7 +10,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
*/
|
|
11
11
|
import { Row, Stack, sortBy } from "@mohasinac/appkit/client";
|
|
12
12
|
import { useMemo, useCallback, useState } from "react";
|
|
13
|
-
import { Div, Label, Span, Text, Toggle, StickyToolbar } from "../../../ui";
|
|
13
|
+
import { Div, Grid, Label, Span, Text, Toggle, StickyToolbar } from "../../../ui";
|
|
14
14
|
import { ListingToolbar, Pagination, FilterDrawer } from "../../../ui";
|
|
15
15
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
16
16
|
import { BundleBuyNowCta } from "./BundleBuyNowCta";
|
|
@@ -93,5 +93,5 @@ export function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, })
|
|
|
93
93
|
// Apply → close. <FilterDrawer> below stays available for parity
|
|
94
94
|
// with every other listing page's filter affordance.
|
|
95
95
|
{ label: "Show out-of-stock", active: showOutOfStock, onChange: (next) => table.setMany({ showOutOfStock: next ? "true" : "", page: "1" }) },
|
|
96
|
-
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: pageItems.length === 0 ? (_jsxs(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx(
|
|
96
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: pageItems.length === 0 ? (_jsxs(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx(Grid, { cols: "cards", gap: "lg", children: pageItems.map((bundle) => (_jsxs(Stack, { children: [_jsx(MarketplaceBundleCard, { bundle: bundle }), onBuyNow && (_jsx(Div, { border: "default", surface: "default", className: "-mt-px border-t border-t-0 border-[var(--appkit-color-border)] pt-[0.5rem] pb-[0.75rem]", padding: "x-sm", roundedBottom: "lg", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: bundle.bundleStockStatus === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }, bundle.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: () => setPendingShowOutOfStock(false), activeCount: filterActiveCount, hideTrigger: true, children: _jsx(Stack, { className: `${__P.p4}`, gap: "md", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "justify-[space-between] cursor-pointer", children: [_jsx(Span, { size: "sm", weight: "medium", color: "muted", children: "Show out-of-stock bundles" }), _jsx(Toggle, { checked: pendingShowOutOfStock, onChange: setPendingShowOutOfStock, size: "sm" })] }) }) })] }));
|
|
97
97
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { ArrowRight } from "lucide-react";
|
|
4
|
-
import { Div, Row, Span, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Div, Grid, Row, Span, Stack, Text } from "../../../ui";
|
|
5
5
|
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv";
|
|
6
6
|
import { MediaImage } from "../../media/MediaImage";
|
|
7
7
|
const __P = {
|
|
@@ -30,5 +30,5 @@ export function CategoryGrid({ categories, getHref, onCategoryClick, emptyLabel
|
|
|
30
30
|
if (categories.length === 0) {
|
|
31
31
|
return (_jsx(Text, { paddingY: "3xl", className: "text-[var(--appkit-color-text-muted)]", size: "sm", align: "start", children: emptyLabel }));
|
|
32
32
|
}
|
|
33
|
-
return (_jsx(
|
|
33
|
+
return (_jsx(Grid, { cols: "categoryCards", gap: "md", className: className, children: categories.map((cat) => (_jsx(CategoryCard, { category: cat, href: getHref ? getHref(cat) : undefined, onClick: !getHref ? onCategoryClick : undefined }, cat.id))) }));
|
|
34
34
|
}
|
|
@@ -4,7 +4,7 @@ import { useState, useCallback } from "react";
|
|
|
4
4
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
5
|
import { usePendingTable } from "../../../react/hooks/usePendingTable";
|
|
6
6
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
7
|
-
import { Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast, StickyToolbar } from "../../../ui";
|
|
7
|
+
import { Div, Grid, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast, StickyToolbar } from "../../../ui";
|
|
8
8
|
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
9
9
|
import { ACTION_ID } from "../../products/constants/action-defs";
|
|
10
10
|
import { ROUTES } from "../../../next";
|
|
@@ -26,7 +26,7 @@ const FILTER_KEYS = ["condition", "brand", "minPrice", "maxPrice"];
|
|
|
26
26
|
function renderCategoryProductGrid(props) {
|
|
27
27
|
const { isLoading, products, view, getProductHref, onWishlistToggle, wishlistedIds, onAddToCart } = props;
|
|
28
28
|
if (isLoading) {
|
|
29
|
-
return (_jsx(
|
|
29
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 8 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) }));
|
|
30
30
|
}
|
|
31
31
|
return (_jsx(ProductGrid, { products: products, getProductHref: getProductHref, view: view, emptyLabel: "No products found in this category.", onWishlistToggle: onWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: onAddToCart }));
|
|
32
32
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { InteractiveStoreCard } from "../../stores/components/InteractiveStoreCard";
|
|
4
4
|
import { ROUTES } from "../../../next";
|
|
5
|
-
import {
|
|
5
|
+
import { Grid, Stack, Text } from "../../../ui";
|
|
6
6
|
export function CategoryStoresListing({ stores }) {
|
|
7
7
|
if (stores.length === 0) {
|
|
8
8
|
return (_jsx(Stack, { justify: "center", className: "text-left", padding: "y-4xl", align: "start", children: _jsx(Text, { size: "sm", color: "muted", children: "No stores found in this category." }) }));
|
|
9
9
|
}
|
|
10
|
-
return (_jsx(
|
|
10
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", children: stores.map((store) => (_jsx(InteractiveStoreCard, { store: store, href: String(ROUTES.PUBLIC.STORE_DETAIL(store.storeSlug)) }, store.id))) }));
|
|
11
11
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Div, Heading, Text, TextLink } from "../../../ui";
|
|
2
|
+
import { Div, Grid, Heading, Text, TextLink } from "../../../ui";
|
|
3
3
|
import { MediaImage } from "../../media/MediaImage";
|
|
4
4
|
import { LAYOUT } from "../../../tokens";
|
|
5
5
|
const CLS_PLACEHOLDER = "w-full";
|
|
@@ -7,5 +7,5 @@ export function CollectionCard({ collection, href }) {
|
|
|
7
7
|
return (_jsxs(TextLink, { rounded: "xl", href: href, shadow: "hover-lg", className: "group relative block overflow-hidden bg-[var(--appkit-color-surface)] dark:bg-[var(--appkit-color-surface-elevated)] transition", children: [collection.image ? (_jsx(Div, { className: `relative ${LAYOUT.cardHeight.md} w-full`, overflow: "hidden", children: _jsx(MediaImage, { src: collection.image, alt: collection.title, size: "card", className: "transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: `${LAYOUT.cardHeight.md} ${CLS_PLACEHOLDER}` })), _jsxs(Div, { surface: "default", padding: "sm", children: [_jsx(Heading, { level: 3, className: "text-[var(--appkit-color-text)]", weight: "semibold", children: collection.title }), collection.subtitle && (_jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: collection.subtitle })), collection.productCount !== undefined && (_jsxs(Text, { className: "mt-2 text-gray-400", size: "xs", children: [collection.productCount, " ", collection.productCount === 1 ? "item" : "items"] }))] })] }));
|
|
8
8
|
}
|
|
9
9
|
export function CollectionGrid({ collections, getHref }) {
|
|
10
|
-
return (_jsx(
|
|
10
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", children: collections.map((c) => (_jsx(CollectionCard, { collection: c, href: getHref(c.slug) }, c.slug))) }));
|
|
11
11
|
}
|
|
@@ -8,7 +8,7 @@ import { serverLogger } from "../../../monitoring/server-logger";
|
|
|
8
8
|
// this section silently rendered nothing instead of failing loudly.
|
|
9
9
|
import { sieveFilter, sieveAnd, SIEVE_OP } from "../../../utils/sieve-builder";
|
|
10
10
|
import { sortBy } from "../../../constants/sort";
|
|
11
|
-
import { Container,
|
|
11
|
+
import { Container, Grid, Heading, Section, Stack, Text } from "../../../ui";
|
|
12
12
|
import { eventRepository } from "../repository/events.repository";
|
|
13
13
|
import { EventCard } from "./EventCard";
|
|
14
14
|
/**
|
|
@@ -40,5 +40,5 @@ export async function EventRafflesSection({ config, }) {
|
|
|
40
40
|
}
|
|
41
41
|
if (events.length === 0)
|
|
42
42
|
return null;
|
|
43
|
-
return (_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), subtitle ? (_jsx(Text, { size: "sm", color: "muted", children: subtitle })) : null] }), _jsx(
|
|
43
|
+
return (_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), subtitle ? (_jsx(Text, { size: "sm", color: "muted", children: subtitle })) : null] }), _jsx(Grid, { cols: "cardsWide", gap: "3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })] }) }) }));
|
|
44
44
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useCallback, useMemo } from "react";
|
|
4
4
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
5
|
import { useEvents } from "../hooks/useEvents";
|
|
6
|
-
import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
|
|
6
|
+
import { Div, Grid, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
|
|
7
7
|
import { EventCard } from "./EventCard";
|
|
8
8
|
import { EventFilters, EVENT_PUBLIC_SORT_OPTIONS } from "./EventFilters";
|
|
9
9
|
import { EVENT_FIELDS } from "../../../constants/field-names";
|
|
@@ -123,5 +123,5 @@ export function EventsIndexListing({ initialData, facetCounts }) {
|
|
|
123
123
|
}, [searchInput, table]);
|
|
124
124
|
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), sortOptions: EVENT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
|
|
125
125
|
{ label: "Show expired", active: showExpired, onChange: (next) => table.set(TABLE_KEYS.SHOW_EXPIRED, next ? "true" : "") },
|
|
126
|
-
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(
|
|
126
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8 mt-2", surface: "subtle", rounded: "default" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No events found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx(Grid, { cols: "cards", gap: "lg", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(EventFilters, { table: pendingTable, variant: "public", counts: facetCounts, hideEmpty: Boolean(facetCounts) }) })] }));
|
|
127
127
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Div, Pagination, Row, Stack, Text } from "../../../ui";
|
|
3
|
+
import { Div, Grid, Pagination, Row, Stack, Text } from "../../../ui";
|
|
4
4
|
import { EventCard } from "./EventCard";
|
|
5
5
|
const __P = {
|
|
6
6
|
p4: "p-[var(--appkit-space-4)]",
|
|
7
7
|
};
|
|
8
8
|
export function EventsListView({ events, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onParticipate, emptyLabel = "No events found", slots, }) {
|
|
9
9
|
if (isLoading) {
|
|
10
|
-
return (_jsx(
|
|
10
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: "animate-pulse overflow-hidden", surface: "subtle", rounded: "xl", border: "default", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-16", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-5 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" })] })] }, i))) }));
|
|
11
11
|
}
|
|
12
12
|
if (events.length === 0) {
|
|
13
13
|
if (slots?.renderEmptyState) {
|
|
@@ -17,7 +17,7 @@ export function EventsListView({ events, isLoading, totalPages = 1, currentPage
|
|
|
17
17
|
}
|
|
18
18
|
return (_jsxs(Stack, { gap: "xl", children: [slots?.renderHeader
|
|
19
19
|
? slots.renderHeader({ total })
|
|
20
|
-
: null, _jsx(
|
|
20
|
+
: null, _jsx(Grid, { cols: "cards", gap: "lg", children: events.map((event, i) => slots?.renderCard ? (_jsx(React.Fragment, { children: slots.renderCard(event, i) }, event.id)) : (_jsx(EventCard, { event: event, onParticipate: onParticipate ? (e) => onParticipate(e) : undefined }, event.id))) }), slots?.renderFooter ? slots.renderFooter({
|
|
21
21
|
page: currentPage,
|
|
22
22
|
totalPages,
|
|
23
23
|
}) : totalPages > 1 && onPageChange ? (_jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) })) : null] }));
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"use client";
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import Link from "next/link";
|
|
12
|
-
import { Badge, Div, Heading, Row, Section, Stack, Text } from "../../../ui";
|
|
12
|
+
import { Badge, Div, Grid, Heading, Row, Section, Stack, Text } from "../../../ui";
|
|
13
13
|
import { MediaImage } from "../../media/MediaImage";
|
|
14
14
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
15
15
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -29,7 +29,7 @@ export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitl
|
|
|
29
29
|
const items = initialItems.filter((c) => c.categoryType === "bundle");
|
|
30
30
|
if (items.length === 0)
|
|
31
31
|
return null;
|
|
32
|
-
return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(
|
|
32
|
+
return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
|
|
33
33
|
}
|
|
34
34
|
function FeaturedBundleCard({ bundle, onBuyNow }) {
|
|
35
35
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
|
-
import { Anchor, Div, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
|
|
3
|
+
import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
|
|
4
4
|
import { MediaImage } from "../../media/MediaImage";
|
|
5
5
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
6
6
|
import { fetchGoogleReviews } from "../lib/google-reviews-fetcher";
|
|
@@ -65,7 +65,7 @@ export async function GoogleReviewsSection(config) {
|
|
|
65
65
|
}
|
|
66
66
|
const gridClass = layout === "carousel"
|
|
67
67
|
? "flex gap-[var(--appkit-space-4)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"
|
|
68
|
-
:
|
|
68
|
+
: `${GRID_MAP.cards} gap-[var(--appkit-space-4)]`;
|
|
69
69
|
const cardClass = layout === "carousel" ? "flex-shrink-0 w-[85%] sm:w-72 snap-start" : "";
|
|
70
70
|
const mapsHref = googleMapsUrl || (placeId ? `https://search.google.com/local/reviews?placeid=${placeId}` : "");
|
|
71
71
|
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto", padding: "x-md", children: [_jsxs(HandModeRow, { className: "mb-8", align: "end", justify: "between", gap: "md", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, children: "What Our Customers Say" }), aggregateRating > 0 && (_jsxs(Row, { className: "mt-1", align: "center", gap: "sm", children: [_jsx(StarRating, { rating: Math.round(aggregateRating) }), _jsxs(Text, { size: "sm", variant: "muted", children: [aggregateRating.toFixed(1), " \u00B7 ", totalRatings.toLocaleString(), " reviews"] })] }))] }), linkToGoogleMaps && mapsHref && (_jsx(Anchor, { href: mapsHref, tone: "none", underline: "hover", className: CLS_GMAPS_LINK, children: "View on Google \u2192" }))] }), reviews.length === 0 ? (_jsx(EmptyState, {})) : (_jsx(Div, { className: gridClass, children: reviews.map((review, i) => (_jsx(Div, { className: cardClass, children: _jsx(ReviewCard, { review: review, showRating: showRating, showDate: showDate }) }, `${review.authorName}-${i}`))) }))] }) }));
|
|
@@ -17,5 +17,5 @@ export function HomepageSkeleton() {
|
|
|
17
17
|
const trustCardH = "h-[clamp(120px,18vh,170px)]";
|
|
18
18
|
const categoryTileH = "h-[clamp(112px,16vh,160px)]";
|
|
19
19
|
const newsletterH = "h-[clamp(220px,28vh,320px)]";
|
|
20
|
-
return (_jsxs(Div, { className: `w-full ${__O.hidden}`, children: [_jsx(Div, { className: `${skeleton.card} w-full ${heroSkeletonH}`, "aria-hidden": "true" }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Grid, {
|
|
20
|
+
return (_jsxs(Div, { className: `w-full ${__O.hidden}`, children: [_jsx(Div, { className: `${skeleton.card} w-full ${heroSkeletonH}`, "aria-hidden": "true" }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Grid, { cols: "statTiles", children: Array.from({ length: 4 }).map((_, i) => (_jsx(Stack, { className: `${skeleton.card} ${trustCardH}`, align: "center", gap: "3", padding: "lg" }, i))) }) }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsx(Div, { className: `${skeleton.heading} w-48 mx-auto mb-6` }), _jsx(Grid, { cols: "statTiles", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: `${skeleton.card} ${categoryTileH}` }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "muted", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-52` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { cols: "cards", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-56` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { cols: "cards", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Div, { className: `${skeleton.card} max-w-2xl mx-auto ${newsletterH}`, rounded: "2xl" }) })] }));
|
|
21
21
|
}
|
|
@@ -35,5 +35,5 @@ export function SecurityHighlightsSection({ title, subtitle, pillLabel, items, l
|
|
|
35
35
|
observer.observe(el);
|
|
36
36
|
return () => observer.disconnect();
|
|
37
37
|
}, []);
|
|
38
|
-
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, surface: "muted", paddingX: "x-md", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [pillLabel && (_jsx(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: pillLabel })), _jsx(Heading, { level: 2, className: `mt-4 ${themed.textPrimary}`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-2 max-w-xl", children: subtitle }))] }), _jsx(Grid, {
|
|
38
|
+
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, surface: "muted", paddingX: "x-md", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [pillLabel && (_jsx(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: pillLabel })), _jsx(Heading, { level: 2, className: `mt-4 ${themed.textPrimary}`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-2 max-w-xl", children: subtitle }))] }), _jsx(Grid, { cols: "cards", gap: "xl", children: items.map((item, index) => (_jsx(SecurityCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.key))) }), learnMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel }) }))] }) }));
|
|
39
39
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
|
-
import { Anchor, Div, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
|
|
3
|
+
import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
|
|
4
4
|
import { SocialPostCard } from "./SocialPostCard";
|
|
5
5
|
import { fetchInstagramPosts, fetchFacebookPosts, fetchTikTokPosts, fetchDeviantArtPosts, } from "../lib/social-feed-fetcher";
|
|
6
6
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
@@ -93,7 +93,7 @@ export async function SocialFeedSection(config) {
|
|
|
93
93
|
? "flex gap-[var(--appkit-space-3)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"
|
|
94
94
|
: layout === "masonry"
|
|
95
95
|
? "columns-2 sm:columns-3 lg:columns-4 gap-[var(--appkit-space-3)] space-y-3"
|
|
96
|
-
:
|
|
96
|
+
: `${GRID_MAP.cards} gap-[var(--appkit-space-3)]`;
|
|
97
97
|
const cardClass = layout === "carousel"
|
|
98
98
|
? "flex-shrink-0 w-[85%] sm:w-56 snap-start"
|
|
99
99
|
: layout === "masonry"
|
|
@@ -31,5 +31,5 @@ export function StatsCounterSection({ stats, className = "", }) {
|
|
|
31
31
|
observer.observe(el);
|
|
32
32
|
return () => observer.disconnect();
|
|
33
33
|
}, []);
|
|
34
|
-
return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsx(Div, { className: "mx-auto max-w-5xl", children: _jsx(Grid, {
|
|
34
|
+
return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsx(Div, { className: "mx-auto max-w-5xl", children: _jsx(Grid, { cols: "statTiles", gap: "none", children: stats.map((stat, i) => (_jsx(StatCard, { stat: stat, visible: visible, delayClass: STAT_DELAYS[Math.min(i, STAT_DELAYS.length - 1)], isLast: i === stats.length - 1 }, stat.key))) }) }) }));
|
|
35
35
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Div, Row, Section, Stack, Text } from "../../../ui";
|
|
2
|
+
import { Div, Grid, Row, Section, Stack, Text } from "../../../ui";
|
|
3
3
|
const BADGE_ICONS = {
|
|
4
4
|
shipping: (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-7 w-7", "aria-hidden": "true", children: [_jsx("path", { d: "M5 12H19" }), _jsx("path", { d: "M15 7l5 5-5 5" }), _jsx("path", { d: "M1 17V7a4 4 0 0 1 4-4h10" }), _jsx("path", { d: "M22 17a4 4 0 0 1-4 4H5" })] })),
|
|
5
5
|
support: (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-7 w-7", "aria-hidden": "true", children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) })),
|
|
@@ -42,5 +42,5 @@ const FALLBACK_BADGES = [
|
|
|
42
42
|
];
|
|
43
43
|
export function TrustBadges({ badges }) {
|
|
44
44
|
const items = badges && badges.length > 0 ? badges : FALLBACK_BADGES;
|
|
45
|
-
return (_jsx(Section, { className: "bg-[var(--section-bg)] [border-top:var(--section-border)] [border-bottom:var(--section-border)]", children: _jsx(Div, { className: "mx-auto max-w-7xl sm:py-[2.5rem]", paddingY: "y-lg", paddingX: "x-md", children: _jsx(
|
|
45
|
+
return (_jsx(Section, { className: "bg-[var(--section-bg)] [border-top:var(--section-border)] [border-bottom:var(--section-border)]", children: _jsx(Div, { className: "mx-auto max-w-7xl sm:py-[2.5rem]", paddingY: "y-lg", paddingX: "x-md", children: _jsx(Grid, { cols: "statTiles", gap: "lg", children: items.map((badge) => (_jsxs(Stack, { className: "text-left [flex-direction:row] items-[center] gap-[1rem]", align: "center", gap: "sm", children: [_jsx(Row, { className: "h-12 w-12 shrink-0 bg-[rgba(240,196,23,0.12)] text-[var(--color-red)] [border:2px_solid_var(--border-ink)]", align: "center", justify: "center", rounded: "full", children: BADGE_ICONS[badge.iconKey] }), _jsxs(Div, { children: [_jsx(Text, { className: "font-black uppercase tracking-wide text-[var(--section-title-color)]", size: "sm", children: badge.title }), _jsx(Text, { className: "text-[var(--color-muted)]", size: "xs", weight: "medium", children: badge.sub })] })] }, badge.id))) }) }) }));
|
|
46
46
|
}
|
|
@@ -38,5 +38,5 @@ export function TrustFeaturesSection({ items, title, variant = "cards", classNam
|
|
|
38
38
|
const doubled = [...items, ...items];
|
|
39
39
|
return (_jsx(Section, { border: "subtle", ref: sectionRef, className: `overflow-hidden border-y ${className}`, padding: "y-md", children: _jsx(Div, { layout: "flex", className: "animate-marquee", children: doubled.map((item, i) => (_jsx(TrustStripItem, { item: item }, `${item.iconName}-${i}`))) }) }));
|
|
40
40
|
}
|
|
41
|
-
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, paddingX: "x-md", surface: "muted", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [title && (_jsx(Div, { className: "text-left mb-8", children: _jsx(Heading, { level: 2, color: "primary", size: "3xl", weight: "bold", mdSize: "4xl", children: title }) })), _jsx(Grid, {
|
|
41
|
+
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, paddingX: "x-md", surface: "muted", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [title && (_jsx(Div, { className: "text-left mb-8", children: _jsx(Heading, { level: 2, color: "primary", size: "3xl", weight: "bold", mdSize: "4xl", children: title }) })), _jsx(Grid, { cols: "statTiles", gap: "lg", children: items.map((item, index) => (_jsx(TrustFeatureCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.iconName))) })] }) }));
|
|
42
42
|
}
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback, useEffect, useState } from "react";
|
|
13
13
|
import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
14
|
+
import { acquireRealtimeSession } from "../../../contracts/realtime-session";
|
|
14
15
|
import { conversationPingPath, userConversationsPingPath, } from "../realtime";
|
|
15
16
|
import { normalizeError } from "../../../errors/normalize";
|
|
16
17
|
import { apiClient, ApiClientError } from "../../../http/ApiClient";
|
|
17
|
-
import { CONVERSATION_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
18
|
+
import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS, } from "../../../constants/api-endpoints";
|
|
18
19
|
/**
|
|
19
20
|
* @deprecated Re-exported for backwards compat — import `conversationPingPath`
|
|
20
21
|
* from `@mohasinac/appkit` instead.
|
|
@@ -69,22 +70,45 @@ export function useConversation(conversationId) {
|
|
|
69
70
|
setIsConnected(false);
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
let unsubscribe;
|
|
74
|
+
let release;
|
|
75
|
+
let cancelled = false;
|
|
76
|
+
// 🛑 Sign in BEFORE subscribing. Without a session `auth == null`, and the
|
|
77
|
+
// `chats/$conversationId` rule requires
|
|
78
|
+
// `auth.token.conversationIds[$conversationId]` — a claim that, until this
|
|
79
|
+
// change, NOTHING issued (the token route only ever sent `chatIds`, from
|
|
80
|
+
// the unrelated `chatRooms` collection). Two stacked faults on one channel:
|
|
81
|
+
// no sign-in, and a claim with no issuer. The recipient of a message never
|
|
82
|
+
// saw it until they manually refreshed; the sender only saw their own
|
|
83
|
+
// because `send()` calls `refetch()` directly.
|
|
84
|
+
void (async () => {
|
|
85
|
+
try {
|
|
86
|
+
release = await acquireRealtimeSession("messages", () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
|
|
87
|
+
if (cancelled) {
|
|
88
|
+
release?.();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
unsubscribe = getClientRealtimeProvider().subscribe(conversationPingPath(conversationId), () => {
|
|
92
|
+
setIsConnected(true);
|
|
93
|
+
void refetch();
|
|
94
|
+
}, (err) => {
|
|
95
|
+
void normalizeError(err);
|
|
96
|
+
setIsConnected(false);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (_err) {
|
|
100
|
+
void normalizeError(_err);
|
|
101
|
+
// Provider unregistered or token fetch failed — falls back to the
|
|
102
|
+
// one-shot fetch above.
|
|
78
103
|
setIsConnected(false);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
void normalizeError(_err);
|
|
84
|
-
// Realtime provider not registered — falls back to one-shot fetch above.
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
85
108
|
setIsConnected(false);
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
unsubscribe?.();
|
|
110
|
+
release?.();
|
|
111
|
+
};
|
|
88
112
|
}, [conversationId, refetch]);
|
|
89
113
|
const sendMessage = useCallback(async (body) => {
|
|
90
114
|
if (!conversationId)
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { useCallback, useEffect, useState } from "react";
|
|
8
8
|
import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
9
|
+
import { acquireRealtimeSession } from "../../../contracts/realtime-session";
|
|
9
10
|
import { userConversationsPingPath } from "../realtime";
|
|
10
11
|
import { normalizeError } from "../../../errors/normalize";
|
|
11
12
|
import { apiClient } from "../../../http/ApiClient";
|
|
12
|
-
import { CONVERSATION_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
|
+
import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
14
|
const LIST_ENDPOINT = CONVERSATION_ENDPOINTS.LIST;
|
|
14
15
|
async function fetchList(endpoint) {
|
|
15
16
|
const data = await apiClient.get(endpoint);
|
|
@@ -46,19 +47,40 @@ export function useConversations(userId, opts = {}) {
|
|
|
46
47
|
useEffect(() => {
|
|
47
48
|
if (!userId)
|
|
48
49
|
return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
let unsubscribe;
|
|
51
|
+
let release;
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
// 🛑 Sign in BEFORE subscribing. This hook used to call subscribe() with no
|
|
54
|
+
// authenticated session at all, so `auth == null` and the
|
|
55
|
+
// `chats/user/$userId` rule (`auth.uid == $userId`) could never pass. Every
|
|
56
|
+
// update was permission-denied and the error handler below was a literal
|
|
57
|
+
// `// ignore`, so the whole live-list channel was inert while looking wired.
|
|
58
|
+
void (async () => {
|
|
59
|
+
try {
|
|
60
|
+
release = await acquireRealtimeSession("messages", () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
|
|
61
|
+
if (cancelled) {
|
|
62
|
+
release?.();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
unsubscribe = getClientRealtimeProvider().subscribe(userConversationsPingPath(userId), () => {
|
|
66
|
+
void refetch();
|
|
67
|
+
}, (err) => {
|
|
68
|
+
// Surfaced, not ignored. A denied subscription is a real defect and
|
|
69
|
+
// silence here is what hid it for months.
|
|
70
|
+
void normalizeError(err);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (_err) {
|
|
74
|
+
void normalizeError(_err);
|
|
75
|
+
// Provider unregistered or token fetch failed — non-fatal; the list
|
|
76
|
+
// still works via the one-shot fetch above, just without live updates.
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
return () => {
|
|
80
|
+
cancelled = true;
|
|
81
|
+
unsubscribe?.();
|
|
82
|
+
release?.();
|
|
83
|
+
};
|
|
62
84
|
}, [userId, refetch]);
|
|
63
85
|
const totalUnread = conversations.reduce((sum, c) => sum + (c[unreadField] ?? 0), 0);
|
|
64
86
|
return { conversations, isLoading, error, totalUnread, refetch };
|
|
@@ -30,6 +30,29 @@ export declare class ConversationsRepository {
|
|
|
30
30
|
findById(id: string): Promise<ConversationDocument | null>;
|
|
31
31
|
listByBuyer(buyerId: string): Promise<ConversationDocument[]>;
|
|
32
32
|
listByStore(storeId: string): Promise<ConversationDocument[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Conversation ids this user may subscribe to, for the RTDB `conversationIds`
|
|
35
|
+
* custom-token claim.
|
|
36
|
+
*
|
|
37
|
+
* 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
|
|
38
|
+
* rule requires `auth.token.conversationIds[$conversationId] == true`, but
|
|
39
|
+
* `/api/realtime/token` only ever issued `chatIds`, built by
|
|
40
|
+
* `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
|
|
41
|
+
* a different collection with different document ids. So the buyer↔seller
|
|
42
|
+
* live-message channel was denied for every user, and both subscribing hooks
|
|
43
|
+
* swallowed the permission error (one into `setIsConnected(false)`, one into
|
|
44
|
+
* a literal `// ignore`). Messages only ever appeared for the sender, via an
|
|
45
|
+
* unrelated `refetch()` after send.
|
|
46
|
+
*
|
|
47
|
+
* `.select()` with no fields fetches ids only — the document bodies are not
|
|
48
|
+
* needed and this runs on every token issue.
|
|
49
|
+
*
|
|
50
|
+
* @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
|
|
51
|
+
* total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
|
|
52
|
+
* user in hundreds of conversations would make `createCustomToken` throw
|
|
53
|
+
* and take the whole route down (see the route's own comment).
|
|
54
|
+
*/
|
|
55
|
+
getConversationIdsForUser(userId: string, storeIds?: readonly string[], limit?: number): Promise<string[]>;
|
|
33
56
|
/**
|
|
34
57
|
* Append a message to a conversation in a transaction. Bumps the counterparty
|
|
35
58
|
* unread counter; updates lastMessage / lastMessageAt / updatedAt.
|