@mohasinac/appkit 4.30.15 → 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/server/functions/https.js +17 -2
- package/dist/_internal/server/jobs/core/listingProcessor.js +5 -1
- 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/addresses/repository/addresses.repository.d.ts +12 -1
- package/dist/features/addresses/repository/addresses.repository.js +16 -7
- 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/auth/repository/user.repository.d.ts +22 -1
- package/dist/features/auth/repository/user.repository.js +31 -28
- 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/cart/repository/cart.repository.d.ts +15 -2
- package/dist/features/cart/repository/cart.repository.js +15 -13
- 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/orders/repository/orders.repository.d.ts +12 -1
- package/dist/features/orders/repository/orders.repository.js +14 -5
- package/dist/features/payments/repository/payout.repository.d.ts +19 -9
- package/dist/features/payments/repository/payout.repository.js +26 -20
- 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/ProductFilters.d.ts +19 -1
- package/dist/features/products/components/ProductFilters.js +11 -3
- package/dist/features/products/components/ProductGrid.js +2 -7
- package/dist/features/products/components/ProductsIndexListing.d.ts +4 -2
- package/dist/features/products/components/ProductsIndexListing.js +41 -28
- 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/reviews/repository/reviews.repository.d.ts +10 -8
- package/dist/features/reviews/repository/reviews.repository.js +16 -19
- 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 +215 -18
- package/dist/next/api/routeHandler.js +27 -7
- package/dist/providers/db-firebase/base.repository.d.ts +12 -0
- package/dist/providers/db-firebase/base.repository.js +48 -20
- package/dist/react/hooks/usePendingFilters.d.ts +12 -2
- package/dist/react/hooks/usePendingFilters.js +2 -2
- package/dist/react/hooks/usePendingTable.d.ts +9 -2
- package/dist/react/hooks/useRealtimeEvent.js +22 -4
- package/dist/security/pii-schemas.d.ts +19 -1
- package/dist/security/pii-schemas.js +19 -1
- 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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState, useTransition } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
5
6
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
7
|
import { isAuthError } from "../../../utils/auth-error";
|
|
7
8
|
import { Button, CountdownDisplay, Div, LoginRequiredModal, Modal, Row, Span, Stack, Text } from "../../../ui";
|
|
@@ -14,16 +15,25 @@ import { AUCTION_BUYOUT_WINDOW_MINUTES } from "../../../_internal/shared/checkou
|
|
|
14
15
|
import { BID_PRESET_MULTIPLIERS, isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config";
|
|
15
16
|
import { normalizeError } from "../../../errors/normalize";
|
|
16
17
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
18
|
+
import { toUserMessage } from "../../../errors/error-display-map";
|
|
17
19
|
const __P = {
|
|
18
20
|
p5: "p-[var(--appkit-space-5)]",
|
|
19
21
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
// The local `BID_ERROR_DISPLAY` map that used to live here is gone.
|
|
23
|
+
//
|
|
24
|
+
// It covered 5 of the ~78 codes the server can emit, and — the actual defect —
|
|
25
|
+
// fell back to `result.error`, the server's own message. `result.error` is
|
|
26
|
+
// always a string, so the safe literal after it was unreachable, and a 500
|
|
27
|
+
// therefore printed its raw internals into the bid field: this component is
|
|
28
|
+
// where `Cannot find module '../providers/db-firebase' Require stack: -
|
|
29
|
+
// /var/task/.next/server/chunks/...` was shown to users.
|
|
30
|
+
//
|
|
31
|
+
// Its five strings already existed as `errors.codes.BID_*` in messages/en.json,
|
|
32
|
+
// so nothing was lost by deleting it. `toUserMessage` resolves every code and
|
|
33
|
+
// terminates in `errors.codes.UNKNOWN` — never in server text.
|
|
34
|
+
/** Feature-specific last resort. Authored copy, never derived from an error. */
|
|
35
|
+
const BID_FALLBACK = "Failed to place bid. Please try again.";
|
|
36
|
+
const BUY_NOW_FALLBACK = "Buy Now failed. Please try again.";
|
|
27
37
|
export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags = [], onPlaceBid, onBuyNow, }) {
|
|
28
38
|
const router = useRouter();
|
|
29
39
|
// Live-updates while the bid card is open — reflects other bidders'
|
|
@@ -47,6 +57,10 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
47
57
|
const [isPending, startTransition] = useTransition();
|
|
48
58
|
const [isBuyNowPending, startBuyNowTransition] = useTransition();
|
|
49
59
|
const [success, setSuccess] = useState(false);
|
|
60
|
+
// Root translator (no namespace) — `toUserMessage` passes fully-qualified
|
|
61
|
+
// `errors.codes.*` keys, and resolves them itself, so a missing key degrades
|
|
62
|
+
// to generic copy rather than throwing MISSING_MESSAGE at render time.
|
|
63
|
+
const t = useTranslations();
|
|
50
64
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
51
65
|
/**
|
|
52
66
|
* Buy Now has no field to hang an error off — it is a standalone button, not
|
|
@@ -141,16 +155,17 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
141
155
|
setShowLoginModal(true);
|
|
142
156
|
}
|
|
143
157
|
else {
|
|
144
|
-
|
|
158
|
+
// `err.message` deliberately NOT used — a thrown error here can carry
|
|
159
|
+
// server internals just as an envelope can.
|
|
160
|
+
setFieldError("bidAmount", BID_FALLBACK);
|
|
145
161
|
}
|
|
146
162
|
}
|
|
147
163
|
}
|
|
148
164
|
function bidErrorMessage(result) {
|
|
149
165
|
if (result.ok)
|
|
150
166
|
return null;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"Failed to place bid. Please try again.");
|
|
167
|
+
// No `?? result.error` tail. That is what leaked the require stack.
|
|
168
|
+
return toUserMessage(result.code, t, { fallback: BID_FALLBACK });
|
|
154
169
|
}
|
|
155
170
|
/**
|
|
156
171
|
* Show a Buy Now failure.
|
|
@@ -164,14 +179,16 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
164
179
|
* `place_bids` soft ban. A login prompt for those tells the buyer to fix
|
|
165
180
|
* something that is not the problem.
|
|
166
181
|
*/
|
|
167
|
-
function reportBuyNowFailure(code,
|
|
182
|
+
function reportBuyNowFailure(code, _message) {
|
|
168
183
|
if (code === "UNAUTHENTICATED") {
|
|
169
184
|
setShowLoginModal(true);
|
|
170
185
|
return;
|
|
171
186
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
// `_message` is the SERVER's message and is intentionally ignored — it was
|
|
188
|
+
// the `?? message` tail here, same defect as bidErrorMessage above. Kept in
|
|
189
|
+
// the signature so callers need no change and the omission is visible.
|
|
190
|
+
void _message;
|
|
191
|
+
setBuyNowError(toUserMessage(code, t, { fallback: BUY_NOW_FALLBACK }));
|
|
175
192
|
}
|
|
176
193
|
function handleBuyNow() {
|
|
177
194
|
if (!onBuyNow)
|
|
@@ -6,7 +6,28 @@ export declare class UserRepository extends BaseRepository<UserDocument> {
|
|
|
6
6
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
7
7
|
constructor();
|
|
8
8
|
private decryptUser;
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Declared, not hand-rolled.
|
|
11
|
+
*
|
|
12
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
13
|
+
* `encryptUserData` did — encrypt the listed fields, then add the mapped blind
|
|
14
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
15
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
16
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
17
|
+
* encryption lived in a private method only three call sites remembered to
|
|
18
|
+
* call.
|
|
19
|
+
*/
|
|
20
|
+
protected piiFields: readonly ["email", "phoneNumber", "googleLinkedEmail"];
|
|
21
|
+
protected piiIndexMap: Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* The base hook, plus the nested objects it cannot see.
|
|
24
|
+
*
|
|
25
|
+
* `piiFields` is a flat list of top-level names, so a nested blob has to be
|
|
26
|
+
* encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
|
|
27
|
+
* encryption path with one extra step on top, rather than two parallel paths
|
|
28
|
+
* that have to agree — which is what the hand-rolled override was.
|
|
29
|
+
*/
|
|
30
|
+
protected applyWriteHooks<D extends object>(data: D): D;
|
|
10
31
|
protected mapDoc<D = UserDocument>(snap: DocumentSnapshot): D;
|
|
11
32
|
createWithId(id: string, data: Partial<UserDocument>): Promise<UserDocument>;
|
|
12
33
|
create(input: Omit<UserDocument, "id" | "createdAt" | "updatedAt">): Promise<UserDocument>;
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { normalizeError } from "../../../errors/normalize";
|
|
2
2
|
import { DatabaseError } from "../../../errors";
|
|
3
3
|
import { BaseRepository, getFirestoreCount, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
4
|
-
import { USER_PII_FIELDS, USER_PII_INDEX_MAP,
|
|
4
|
+
import { USER_PII_FIELDS, USER_PII_INDEX_MAP, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptShippingConfig, piiBlindIndex, } from "../../../security";
|
|
5
5
|
import { USER_COLLECTION, USER_FIELDS, } from "../schemas";
|
|
6
6
|
export class UserRepository extends BaseRepository {
|
|
7
7
|
constructor() {
|
|
8
8
|
super(USER_COLLECTION);
|
|
9
|
+
/**
|
|
10
|
+
* Declared, not hand-rolled.
|
|
11
|
+
*
|
|
12
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
13
|
+
* `encryptUserData` did — encrypt the listed fields, then add the mapped blind
|
|
14
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
15
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
16
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
17
|
+
* encryption lived in a private method only three call sites remembered to
|
|
18
|
+
* call.
|
|
19
|
+
*/
|
|
20
|
+
this.piiFields = USER_PII_FIELDS;
|
|
21
|
+
this.piiIndexMap = USER_PII_INDEX_MAP;
|
|
9
22
|
}
|
|
10
23
|
decryptUser(doc) {
|
|
11
24
|
const decrypted = decryptPiiFields(doc, [...USER_PII_FIELDS]);
|
|
@@ -17,32 +30,22 @@ export class UserRepository extends BaseRepository {
|
|
|
17
30
|
}
|
|
18
31
|
return decrypted;
|
|
19
32
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// `piiIndicesFor` applies USER_PII_INDEX_MAP's real names and, unlike
|
|
33
|
-
// `addPiiIndices`, carries no plaintext back over the ciphertext.
|
|
34
|
-
const encrypted = {
|
|
35
|
-
...encryptPiiFields(data, [...USER_PII_FIELDS]),
|
|
36
|
-
...piiIndicesFor(data, USER_PII_INDEX_MAP),
|
|
37
|
-
};
|
|
38
|
-
const access = encrypted;
|
|
39
|
-
if (access.payoutDetails) {
|
|
33
|
+
/**
|
|
34
|
+
* The base hook, plus the nested objects it cannot see.
|
|
35
|
+
*
|
|
36
|
+
* `piiFields` is a flat list of top-level names, so a nested blob has to be
|
|
37
|
+
* encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
|
|
38
|
+
* encryption path with one extra step on top, rather than two parallel paths
|
|
39
|
+
* that have to agree — which is what the hand-rolled override was.
|
|
40
|
+
*/
|
|
41
|
+
applyWriteHooks(data) {
|
|
42
|
+
const out = super.applyWriteHooks(data);
|
|
43
|
+
const access = out;
|
|
44
|
+
if (access.payoutDetails)
|
|
40
45
|
access.payoutDetails = encryptPayoutDetails(access.payoutDetails);
|
|
41
|
-
|
|
42
|
-
if (access.shippingConfig) {
|
|
46
|
+
if (access.shippingConfig)
|
|
43
47
|
access.shippingConfig = encryptShippingConfig(access.shippingConfig);
|
|
44
|
-
|
|
45
|
-
return encrypted;
|
|
48
|
+
return out;
|
|
46
49
|
}
|
|
47
50
|
mapDoc(snap) {
|
|
48
51
|
const raw = super.mapDoc(snap);
|
|
@@ -57,7 +60,7 @@ export class UserRepository extends BaseRepository {
|
|
|
57
60
|
// check with no error anywhere (found via a live RBAC bug where an
|
|
58
61
|
// admin account created this way couldn't access /admin).
|
|
59
62
|
const withUid = { uid: id, ...data };
|
|
60
|
-
const encrypted = this.
|
|
63
|
+
const encrypted = this.applyWriteHooks(withUid);
|
|
61
64
|
return super.createWithId(id, encrypted);
|
|
62
65
|
}
|
|
63
66
|
async create(input) {
|
|
@@ -78,7 +81,7 @@ export class UserRepository extends BaseRepository {
|
|
|
78
81
|
createdAt: new Date(),
|
|
79
82
|
updatedAt: new Date(),
|
|
80
83
|
};
|
|
81
|
-
const encrypted = this.
|
|
84
|
+
const encrypted = this.applyWriteHooks(userData);
|
|
82
85
|
await this.db
|
|
83
86
|
.collection(this.collection)
|
|
84
87
|
.doc(id)
|
|
@@ -155,7 +158,7 @@ export class UserRepository extends BaseRepository {
|
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
async update(uid, data) {
|
|
158
|
-
const encrypted = this.
|
|
161
|
+
const encrypted = this.applyWriteHooks(data);
|
|
159
162
|
return super.update(uid, encrypted);
|
|
160
163
|
}
|
|
161
164
|
async markEmailAsVerified(uid) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { BeforeAfterSlider } from "./BeforeAfterSlider";
|
|
3
|
-
import { Div, Stack, Text } from "../../../ui";
|
|
3
|
+
import { Div, Grid, Stack, Text } from "../../../ui";
|
|
4
4
|
export function BeforeAfterGallery({ items, className = "", }) {
|
|
5
5
|
if (items.length === 0)
|
|
6
6
|
return null;
|
|
7
|
-
return (_jsx(
|
|
7
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", className: className, children: items.map((item) => (_jsxs(Stack, { gap: "sm", children: [_jsx(BeforeAfterSlider, { item: item }), (item.title || item.description) && (_jsxs(Div, { className: "text-center", children: [item.title && (_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: item.title })), item.description && (_jsx(Text, { className: "mt-0.5", color: "muted", size: "xs", children: item.description }))] }))] }, item.id))) }));
|
|
8
8
|
}
|
|
@@ -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 { useBlogPosts } from "../hooks/useBlog";
|
|
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 { ROUTES } from "../../../next";
|
|
8
8
|
import { BlogCard } from "./BlogListView";
|
|
9
9
|
import { BlogFilters, BLOG_PUBLIC_SORT_OPTIONS } from "./BlogFilters";
|
|
@@ -19,7 +19,7 @@ const __O = {
|
|
|
19
19
|
function renderBlogGrid(props) {
|
|
20
20
|
const { isLoading, posts, view } = props;
|
|
21
21
|
if (isLoading) {
|
|
22
|
-
return (_jsx(
|
|
22
|
+
return (_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.p5}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-1/4", surface: "subtle", rounded: "default" }), _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" })] })] }, i))) }));
|
|
23
23
|
}
|
|
24
24
|
if (posts.length === 0) {
|
|
25
25
|
return _jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No posts found." });
|
|
@@ -27,7 +27,7 @@ function renderBlogGrid(props) {
|
|
|
27
27
|
if (view === "list") {
|
|
28
28
|
return (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: posts.map((post) => _jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id)) }));
|
|
29
29
|
}
|
|
30
|
-
return (_jsx(
|
|
30
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", children: posts.map((post) => _jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id)) }));
|
|
31
31
|
}
|
|
32
32
|
function renderBlogFilterDrawer(props) {
|
|
33
33
|
const { filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable } = props;
|
|
@@ -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, {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type DocumentReference } from "firebase-admin/firestore";
|
|
2
|
-
import type { DocumentSnapshot } from "../../../providers/db-firebase";
|
|
3
2
|
import { BaseRepository, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
|
|
4
3
|
import { type AddToCartInput, type CartAppliedCoupon, type CartDocument, type CartStoreAddons, type CartItemDocument, type AddGroupLineInput, type UpdateCartItemInput, type UpdateCartGroupMembersInput } from "../schemas";
|
|
5
4
|
export declare class CartRepository extends BaseRepository<CartDocument> {
|
|
@@ -7,7 +6,21 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
|
|
|
7
6
|
constructor();
|
|
8
7
|
/** Admin-facing paginated list of carts. */
|
|
9
8
|
list(model: SieveModel): Promise<FirebaseSieveResult<CartDocument>>;
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `mapDoc` is deliberately NOT overridden.
|
|
11
|
+
*
|
|
12
|
+
* It used to decrypt `items[].storeName` on every read — and nothing has
|
|
13
|
+
* ever encrypted that field. `CartRepository` declares no `piiFields`, and
|
|
14
|
+
* `storeName` appears nowhere in pii-schemas.ts. `decryptPii` passes a
|
|
15
|
+
* non-`enc:v1:` string straight through, so the call was a no-op on every
|
|
16
|
+
* document while reading as protection — the most expensive kind of dead
|
|
17
|
+
* code, because it answers the question "is this handled?" wrongly.
|
|
18
|
+
*
|
|
19
|
+
* A store name is not PII: it is the seller's public shopfront name, shown
|
|
20
|
+
* on every listing. If a genuinely private field is added to a cart line,
|
|
21
|
+
* declare it in `piiFields` so `applyWriteHooks` encrypts it on all nine
|
|
22
|
+
* write paths — do not reintroduce a decrypt with no matching encrypt.
|
|
23
|
+
*/
|
|
11
24
|
findByUserId(userId: string): Promise<CartDocument | null>;
|
|
12
25
|
getOrCreate(userId: string): Promise<CartDocument>;
|
|
13
26
|
createEmpty(userId: string): Promise<CartDocument>;
|
|
@@ -6,7 +6,6 @@ import { normalizeError } from "../../../errors/normalize";
|
|
|
6
6
|
const ERR_CART_ITEM_NOT_FOUND = "Cart item not found";
|
|
7
7
|
const ERR_CART_ITEM_LOCKED = "This item requires payment and cannot be removed or modified.";
|
|
8
8
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
9
|
-
import { decryptPii } from "../../../security";
|
|
10
9
|
import { CART_COLLECTION, } from "../schemas";
|
|
11
10
|
export class CartRepository extends BaseRepository {
|
|
12
11
|
constructor() {
|
|
@@ -16,18 +15,21 @@ export class CartRepository extends BaseRepository {
|
|
|
16
15
|
async list(model) {
|
|
17
16
|
return this.sieveQuery(model, CartRepository.SIEVE_FIELDS);
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
/**
|
|
19
|
+
* `mapDoc` is deliberately NOT overridden.
|
|
20
|
+
*
|
|
21
|
+
* It used to decrypt `items[].storeName` on every read — and nothing has
|
|
22
|
+
* ever encrypted that field. `CartRepository` declares no `piiFields`, and
|
|
23
|
+
* `storeName` appears nowhere in pii-schemas.ts. `decryptPii` passes a
|
|
24
|
+
* non-`enc:v1:` string straight through, so the call was a no-op on every
|
|
25
|
+
* document while reading as protection — the most expensive kind of dead
|
|
26
|
+
* code, because it answers the question "is this handled?" wrongly.
|
|
27
|
+
*
|
|
28
|
+
* A store name is not PII: it is the seller's public shopfront name, shown
|
|
29
|
+
* on every listing. If a genuinely private field is added to a cart line,
|
|
30
|
+
* declare it in `piiFields` so `applyWriteHooks` encrypts it on all nine
|
|
31
|
+
* write paths — do not reintroduce a decrypt with no matching encrypt.
|
|
32
|
+
*/
|
|
31
33
|
async findByUserId(userId) {
|
|
32
34
|
return this.findById(userId);
|
|
33
35
|
}
|
|
@@ -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
|
}
|