@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
|
@@ -31,18 +31,58 @@ export type HttpErrorCode = (typeof HTTP_ERROR_CODES)[keyof typeof HTTP_ERROR_CO
|
|
|
31
31
|
export interface MappedError {
|
|
32
32
|
status: number;
|
|
33
33
|
code: string;
|
|
34
|
+
/**
|
|
35
|
+
* The CLIENT-FACING message. For any `status >= 500` in production this is a
|
|
36
|
+
* generic string — never the thrown error's own text.
|
|
37
|
+
*/
|
|
34
38
|
message: string;
|
|
39
|
+
/**
|
|
40
|
+
* The unscrubbed message, present only when `message` was replaced.
|
|
41
|
+
*
|
|
42
|
+
* 🛑 Recorders (serverErrors, logs) MUST use `internalMessage ?? message`.
|
|
43
|
+
* Responses MUST use `message`. The whole point of the split is that
|
|
44
|
+
* scrubbing the UI must not blind the log store — before this existed, the
|
|
45
|
+
* raw text was the only copy of the fault and it was being shipped to the
|
|
46
|
+
* browser instead (a `/var/task/...` path leaked to end users), while
|
|
47
|
+
* scrubbing it naively would have left nothing anywhere.
|
|
48
|
+
*/
|
|
49
|
+
internalMessage?: string;
|
|
35
50
|
issues?: unknown[];
|
|
51
|
+
/**
|
|
52
|
+
* True when the status/code came from the FIRESTORE error map — i.e. the
|
|
53
|
+
* datastore itself refused, rather than our own business logic deciding.
|
|
54
|
+
*
|
|
55
|
+
* Needed because the two are indistinguishable by code alone. Firestore
|
|
56
|
+
* `permission-denied` (an expired service-account key) and
|
|
57
|
+
* `resource-exhausted` (quota blown) map to 403/429 — exactly the codes our
|
|
58
|
+
* RBAC guard and rate limiter emit in normal operation. Before this flag,
|
|
59
|
+
* those infrastructure failures fell below the `status >= 500` persist
|
|
60
|
+
* threshold and were recorded nowhere; adding their CODES to
|
|
61
|
+
* `LOG_AS_INCIDENT_CODES` instead would have persisted every ordinary 404
|
|
62
|
+
* and every rate-limited request along with them.
|
|
63
|
+
*/
|
|
64
|
+
infrastructure?: boolean;
|
|
36
65
|
}
|
|
37
66
|
/**
|
|
38
|
-
*
|
|
67
|
+
* Classify a thrown value AND make the result safe to send to a browser.
|
|
39
68
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
69
|
+
* 🛑 The scrub is keyed on **status**, not on which branch matched.
|
|
70
|
+
*
|
|
71
|
+
* It used to be the other way round: `isProduction` was consulted in exactly
|
|
72
|
+
* one place — the unknown-error tail at the bottom of `classifyError` — while
|
|
73
|
+
* the `DatabaseError`, `AppError` and `ApiError` branches above it returned
|
|
74
|
+
* `err.message` verbatim, in production. Since almost every real 500 in this
|
|
75
|
+
* codebase is a `DatabaseError`, the scrub essentially never ran. That is how
|
|
76
|
+
* a Node `MODULE_NOT_FOUND` — including the absolute server path
|
|
77
|
+
* `/var/task/.next/server/chunks/...` and its full require stack — was rendered
|
|
78
|
+
* as user-facing copy inside the "Place your bid" modal.
|
|
79
|
+
*
|
|
80
|
+
* Keying on status means a branch added later cannot reintroduce the leak by
|
|
81
|
+
* forgetting to check a flag.
|
|
82
|
+
*
|
|
83
|
+
* 4xx messages are NOT scrubbed: they are deliberate, user-actionable copy
|
|
84
|
+
* ("Bid too low", "Coupon expired") produced by our own validation, and
|
|
85
|
+
* replacing them with a generic string would make the product worse.
|
|
46
86
|
*/
|
|
47
87
|
export declare function mapToHttpError(err: unknown, opts?: {
|
|
48
88
|
isProduction?: boolean;
|
|
@@ -34,6 +34,8 @@ export const HTTP_ERROR_CODES = {
|
|
|
34
34
|
CLIENT_WINDOW_ERROR: "CLIENT_WINDOW_ERROR",
|
|
35
35
|
CLIENT_UNHANDLED: "CLIENT_UNHANDLED",
|
|
36
36
|
};
|
|
37
|
+
/** Shown to users for any 5xx in production. */
|
|
38
|
+
const GENERIC_5XX_MESSAGE = "An internal error occurred";
|
|
37
39
|
function isZodLikeError(err) {
|
|
38
40
|
return (typeof err === "object" &&
|
|
39
41
|
err !== null &&
|
|
@@ -84,6 +86,7 @@ function mapFirestore(err) {
|
|
|
84
86
|
status: mapped.status,
|
|
85
87
|
code: mapped.code,
|
|
86
88
|
message: err.message ?? mapped.code,
|
|
89
|
+
infrastructure: true,
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
/**
|
|
@@ -94,9 +97,12 @@ function mapFirestore(err) {
|
|
|
94
97
|
* - DatabaseError unwraps its `data` to look for a wrapped FirestoreError
|
|
95
98
|
* - FirestoreError (numeric .code or string .code) maps via the table above
|
|
96
99
|
* - ZodError shape maps to 400 VALIDATION_FAILED + issues
|
|
97
|
-
* - Unknown Error → 500 INTERNAL
|
|
100
|
+
* - Unknown Error → 500 INTERNAL
|
|
101
|
+
*
|
|
102
|
+
* Production scrubbing of 5xx messages is applied ONCE, by `mapToHttpError`
|
|
103
|
+
* below — deliberately not inside these branches. See the note there.
|
|
98
104
|
*/
|
|
99
|
-
|
|
105
|
+
function classifyError(err, opts) {
|
|
100
106
|
const isProduction = opts?.isProduction ?? false;
|
|
101
107
|
// AppError subclasses first — they carry status + code authoritatively
|
|
102
108
|
if (err instanceof ValidationError) {
|
|
@@ -194,15 +200,50 @@ export function mapToHttpError(err, opts) {
|
|
|
194
200
|
};
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
|
-
// Unknown
|
|
203
|
+
// Unknown
|
|
204
|
+
void isProduction; // scrubbing happens once, in mapToHttpError
|
|
198
205
|
const rawMessage = err instanceof Error
|
|
199
206
|
? err.message
|
|
200
207
|
: typeof err === "string"
|
|
201
208
|
? err
|
|
202
|
-
:
|
|
209
|
+
: GENERIC_5XX_MESSAGE;
|
|
203
210
|
return {
|
|
204
211
|
status: 500,
|
|
205
212
|
code: HTTP_ERROR_CODES.INTERNAL,
|
|
206
|
-
message:
|
|
213
|
+
message: rawMessage,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Classify a thrown value AND make the result safe to send to a browser.
|
|
218
|
+
*
|
|
219
|
+
* 🛑 The scrub is keyed on **status**, not on which branch matched.
|
|
220
|
+
*
|
|
221
|
+
* It used to be the other way round: `isProduction` was consulted in exactly
|
|
222
|
+
* one place — the unknown-error tail at the bottom of `classifyError` — while
|
|
223
|
+
* the `DatabaseError`, `AppError` and `ApiError` branches above it returned
|
|
224
|
+
* `err.message` verbatim, in production. Since almost every real 500 in this
|
|
225
|
+
* codebase is a `DatabaseError`, the scrub essentially never ran. That is how
|
|
226
|
+
* a Node `MODULE_NOT_FOUND` — including the absolute server path
|
|
227
|
+
* `/var/task/.next/server/chunks/...` and its full require stack — was rendered
|
|
228
|
+
* as user-facing copy inside the "Place your bid" modal.
|
|
229
|
+
*
|
|
230
|
+
* Keying on status means a branch added later cannot reintroduce the leak by
|
|
231
|
+
* forgetting to check a flag.
|
|
232
|
+
*
|
|
233
|
+
* 4xx messages are NOT scrubbed: they are deliberate, user-actionable copy
|
|
234
|
+
* ("Bid too low", "Coupon expired") produced by our own validation, and
|
|
235
|
+
* replacing them with a generic string would make the product worse.
|
|
236
|
+
*/
|
|
237
|
+
export function mapToHttpError(err, opts) {
|
|
238
|
+
const isProduction = opts?.isProduction ?? false;
|
|
239
|
+
const mapped = classifyError(err, opts);
|
|
240
|
+
if (!isProduction || mapped.status < 500)
|
|
241
|
+
return mapped;
|
|
242
|
+
if (mapped.message === GENERIC_5XX_MESSAGE)
|
|
243
|
+
return mapped;
|
|
244
|
+
return {
|
|
245
|
+
...mapped,
|
|
246
|
+
message: GENERIC_5XX_MESSAGE,
|
|
247
|
+
internalMessage: mapped.message,
|
|
207
248
|
};
|
|
208
249
|
}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -17,3 +17,4 @@ export { safeRead, installDegradedReadReporter, type DegradedReadReport, } from
|
|
|
17
17
|
export { normalizeError, getErrorMessage, isAuthError, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./normalize";
|
|
18
18
|
export type { NormalizedError, NormalizedApiError, NormalizedAppError, NormalizedFirebaseAuthError, NormalizedFirebaseFirestoreError, NormalizedFirebaseStorageError, NormalizedNativeError, NormalizedNetworkError, NormalizedUnknownThrownValue, NormalizedZodError, } from "./normalize";
|
|
19
19
|
export * from "./degraded";
|
|
20
|
+
export { describeCauseChain } from "./describe-cause";
|
package/dist/errors/index.js
CHANGED
|
@@ -26,3 +26,7 @@ export { safeRead, installDegradedReadReporter, } from "./safe-read";
|
|
|
26
26
|
// `appkit/src/utils/auth-error.ts` (now a thin re-export shim).
|
|
27
27
|
export { normalizeError, getErrorMessage, isAuthError, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./normalize";
|
|
28
28
|
export * from "./degraded";
|
|
29
|
+
// Flattens Error.cause into a persistable string. Required by every recorder:
|
|
30
|
+
// AppError rebases its own stack onto the wrap site, so the frames naming the
|
|
31
|
+
// real fault exist only on the cause.
|
|
32
|
+
export { describeCauseChain } from "./describe-cause";
|
|
@@ -119,7 +119,7 @@ function renderProfileHero(t, ctx) {
|
|
|
119
119
|
return (_jsx(Section, { color: "inverse", tone: "accent-banner", paddingY: "y-2-5xl", children: _jsx(Div, { className: `${page.container.md}`, children: _jsxs(Stack, { direction: "sm-row", wrap: true, align: "center", smAlign: "end", gap: "md", children: [_jsx(Row, { align: "center", justify: "center", className: `w-20 h-20 flex-shrink-0 ${__O.hidden}`, surface: "default", rounded: "full", children: photoURL ? _jsx(MediaImage, { src: photoURL, alt: displayName, size: "avatar", fallback: "\uD83D\uDC64" }) : _jsx(User, { className: "w-10 h-10 text-white/60" }) }), _jsxs(Stack, { gap: "xs", className: "text-center sm:text-left", children: [_jsxs(Row, { justify: "start", gap: "xs", wrap: true, children: [_jsx(Heading, { color: "inverse", level: 1, variant: "none", className: "mb-0", children: displayName }), isSeller && _jsx(Span, { color: "inverse", size: "xs", weight: "semibold", className: "bg-[white]/20", padding: "pill-sm", rounded: "full", children: t("roleSeller") })] }), _jsx(Text, { color: "inverse", variant: "none", size: "sm", children: memberSince })] }), isSeller && storeSlug && (_jsx(Div, { className: "sm:ml-auto", children: _jsxs(Link, { href: String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)), className: "inline-flex items-center gap-[var(--appkit-space-1-5)] rounded-full border border-white/30 bg-white/10 hover:bg-white/20 px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-white transition-colors", children: [_jsx(ExternalLink, { className: "w-3.5 h-3.5" }), t("visitStore")] }) }))] }) }) }));
|
|
120
120
|
}
|
|
121
121
|
function renderProfileStatsRow(flex, statItems) {
|
|
122
|
-
return (_jsx(Grid, {
|
|
122
|
+
return (_jsx(Grid, { cols: "statTiles", gap: "md", children: statItems.map(({ icon: Icon, label, value }) => (_jsxs(Div, { rounded: "xl", className: `${__P.p4} text-center`, border: "default", surface: "muted", children: [_jsx(Row, { centered: true, className: `${flex.center} mb-1`, children: _jsx(Icon, { className: "w-4 h-4 text-neutral-400" }) }), _jsx(Text, { size: "lg", weight: "bold", children: value }), _jsx(Text, { variant: "secondary", size: "xs", children: label })] }, label))) }));
|
|
123
123
|
}
|
|
124
124
|
function renderProfileBioSection(pub) {
|
|
125
125
|
if (!pub?.bio && !pub?.location && !pub?.website)
|
|
@@ -132,11 +132,11 @@ function renderStoreDescriptionSection(isSeller, storeSlug, storeDescription, st
|
|
|
132
132
|
return (_jsx(Section, { children: _jsxs(Div, { rounded: "2xl", className: `${__P.p6}`, border: "default", surface: "subtle", children: [_jsx(Heading, { level: 3, className: "mb-2", children: storeName }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: storeDescription }), _jsxs(Link, { href: String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)), className: "mt-3 inline-flex items-center gap-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-medium text-primary hover:underline", children: [t("visitStore"), " \u2192"] })] }) }));
|
|
133
133
|
}
|
|
134
134
|
function renderProfileListingsSection(t, products, storeSlug) {
|
|
135
|
-
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("listingsTitle") }), products.length === 0 ? (_jsxs(Div, { rounded: "2xl", padding: "3xl", className: "text-center", border: "default", surface: "subtle", children: [_jsx(ShoppingBag, { className: CLS_EMPTY_ICON }), _jsx(Text, { variant: "secondary", size: "sm", children: t("noListings") })] })) : (_jsx(Grid, {
|
|
135
|
+
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("listingsTitle") }), products.length === 0 ? (_jsxs(Div, { rounded: "2xl", padding: "3xl", className: "text-center", border: "default", surface: "subtle", children: [_jsx(ShoppingBag, { className: CLS_EMPTY_ICON }), _jsx(Text, { variant: "secondary", size: "sm", children: t("noListings") })] })) : (_jsx(Grid, { cols: "cards", gap: "md", children: products.slice(0, 8).map((p) => _jsx(ProductCard, { product: toProductItem(p), href: getProductHref(p) }, p.id)) })), products.length > 8 && storeSlug && (_jsx(Div, { className: "mt-4 text-center", children: _jsx(Link, { href: String(ROUTES.PUBLIC.STORE_PRODUCTS(storeSlug)), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary hover:underline", children: t("viewAllListings", { count: products.length }) }) }))] }));
|
|
136
136
|
}
|
|
137
137
|
function renderAuthoredReviewsSection(t, reviews, displayName) {
|
|
138
|
-
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsAuthoredTitle", { name: displayName }) }), reviews.length === 0 ? (_jsxs(Div, { rounded: "2xl", padding: "3xl", className: "text-center", border: "default", surface: "subtle", children: [_jsx(Star, { className: CLS_EMPTY_ICON }), _jsx(Text, { variant: "secondary", size: "sm", children: t("noReviewsAuthored", { name: displayName }) })] })) : (_jsx(Grid, {
|
|
138
|
+
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsAuthoredTitle", { name: displayName }) }), reviews.length === 0 ? (_jsxs(Div, { rounded: "2xl", padding: "3xl", className: "text-center", border: "default", surface: "subtle", children: [_jsx(Star, { className: CLS_EMPTY_ICON }), _jsx(Text, { variant: "secondary", size: "sm", children: t("noReviewsAuthored", { name: displayName }) })] })) : (_jsx(Grid, { cols: "cardsWide", gap: "md", children: reviews.slice(0, 6).map((review) => _jsx(ReviewCard, { review: review }, review.id)) }))] }));
|
|
139
139
|
}
|
|
140
140
|
function renderBuyerReceivedReviewsSection(t, reviews) {
|
|
141
|
-
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsReceivedAsBuyerTitle") || "Reviews Received" }), _jsx(Grid, {
|
|
141
|
+
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsReceivedAsBuyerTitle") || "Reviews Received" }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: reviews.slice(0, 6).map((review) => _jsx(ReviewCard, { review: review }, review.id)) })] }));
|
|
142
142
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Button, Div, Span, Stack, Text } from "../../../ui";
|
|
2
|
+
import { Button, Div, Grid, Span, Stack, Text } from "../../../ui";
|
|
3
3
|
export function AddressCard({ address, onEdit, onDelete, onSetDefault, labels = {}, }) {
|
|
4
4
|
const line1 = "line1" in address && address.line1
|
|
5
5
|
? address.line1
|
|
@@ -10,5 +10,5 @@ export function AddressCard({ address, onEdit, onDelete, onSetDefault, labels =
|
|
|
10
10
|
return (_jsxs(Div, { surface: "card", padding: "sm", className: "relative", children: [address.isDefault && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "absolute right-3 top-3", rounded: "full", padding: "pill-xs", children: labels.defaultBadge ?? "Default" })), address.label && (_jsx(Text, { className: "mb-1 tracking-wide text-[var(--appkit-color-text-muted)]", size: "xs", weight: "semibold", transform: "uppercase", children: address.label })), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", children: line1 }), line2 && _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", children: line2 }), _jsxs(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", children: [address.city, ", ", address.state, " ", address.postalCode] }), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", children: address.country }), address.phone && (_jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: address.phone })), _jsxs(Div, { layout: "flex", gap: "1", className: "mt-3", children: [onEdit && (_jsx(Button, { onClick: () => onEdit(address), variant: "ghost", size: "sm", className: "text-[length:var(--appkit-text-xs)] font-medium text-primary hover:underline", children: labels.edit ?? "Edit" })), onDelete && (_jsx(Button, { onClick: () => onDelete(address.id), variant: "ghost", size: "sm", className: "text-[length:var(--appkit-text-xs)] font-medium text-error hover:underline", children: labels.delete ?? "Delete" })), !address.isDefault && onSetDefault && (_jsx(Button, { onClick: () => onSetDefault(address.id), variant: "outline", size: "sm", className: "ml-auto text-[length:var(--appkit-text-xs)] font-medium", children: labels.setDefault ?? "Set default" }))] })] }));
|
|
11
11
|
}
|
|
12
12
|
export function AddressBook({ addresses, onEdit, onDelete, onSetDefault, onAdd, emptyLabel = "No saved addresses", addLabel = "Add Address", }) {
|
|
13
|
-
return (_jsxs(Stack, { gap: "md", children: [addresses.length === 0 && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: emptyLabel })), _jsx(
|
|
13
|
+
return (_jsxs(Stack, { gap: "md", children: [addresses.length === 0 && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: emptyLabel })), _jsx(Grid, { cols: "cardsWide", gap: "md", children: addresses.map((addr) => (_jsx(AddressCard, { address: addr, onEdit: onEdit, onDelete: onDelete, onSetDefault: onSetDefault }, addr.id))) }), onAdd && (_jsxs(Button, { rounded: "lg", onClick: onAdd, variant: "outline", paddingX: "md", paddingY: "md", textSize: "sm", weight: "medium", className: "mt-2 border border-dashed border-neutral-300 dark:border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] dark:text-[var(--appkit-color-text-muted)] transition hover:border-neutral-400 dark:hover:border-[var(--appkit-color-border-subtle)] hover:text-neutral-700 dark:hover:text-[var(--appkit-color-text)]", children: ["+ ", addLabel] }))] }));
|
|
14
14
|
}
|
|
@@ -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 { useAddresses } from "../hooks/useAddresses";
|
|
6
|
-
import { Div, ListingFilterDrawer, ListingToolbar, Stack, Text } from "../../../ui";
|
|
6
|
+
import { Div, Grid, ListingFilterDrawer, ListingToolbar, Stack, Text } from "../../../ui";
|
|
7
7
|
import { AddressBook } from "./AddressBook";
|
|
8
8
|
import { AddressFilters } from "./AddressFilters";
|
|
9
9
|
import { TABLE_KEYS, VIEW_MODE } from "../../../constants/table-keys";
|
|
@@ -74,5 +74,5 @@ export function AddressesIndexListing({ onAdd, onEdit, onDelete, }) {
|
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
76
|
return (_jsxs(Stack, { gap: "md", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search by address, postcode or label...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, view: view, onViewChange: (v) => { if (v === "table")
|
|
77
|
-
return; setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(Div, { className: "sm:px-[1rem]", padding: "x-sm", children: isLoading ? (_jsx(
|
|
77
|
+
return; setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(Div, { className: "sm:px-[1rem]", padding: "x-sm", children: isLoading ? (_jsx(Grid, { cols: "cardsWide", gap: "md", children: Array.from({ length: 4 }).map((_, i) => (_jsxs(Stack, { gap: "sm", rounded: "xl", border: "default", padding: "md", className: "animate-pulse", children: [_jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" })] }, i))) })) : addresses.length === 0 ? (_jsx(Text, { paddingY: "xl", color: "muted", size: "sm", align: "start", children: table.get(TABLE_KEYS.QUERY) ? `No addresses matching "${table.get(TABLE_KEYS.QUERY)}"` : "No saved addresses." })) : (_jsx(AddressBook, { addresses: addresses, onEdit: onEdit, onDelete: onDelete, onAdd: onAdd })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(AddressFilters, { table: pendingTable }) })] }));
|
|
78
78
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import { Div, Heading, Stack } from "../../../ui";
|
|
4
|
+
import { Div, Grid, Heading, Stack } from "../../../ui";
|
|
5
5
|
import { Alert } from "../../../ui";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
@@ -21,7 +21,7 @@ export function AdminAnalyticsView({ data, labels = {}, formatRevenue = (n) => S
|
|
|
21
21
|
const summary = resolvedData?.summary;
|
|
22
22
|
const ordersByMonth = resolvedData?.ordersByMonth ?? [];
|
|
23
23
|
const topProducts = resolvedData?.topProducts ?? [];
|
|
24
|
-
return (_jsxs(Div, { className: className, children: [renderHeader ? (renderHeader()) : labels.title ? (_jsx(Heading, { level: 1, className: "mb-6", size: "2xl", weight: "bold", children: labels.title })) : null, query.error ? (_jsx(Alert, { variant: "error", title: "Could not load analytics", children: query.error instanceof Error ? query.error.message : "Unknown error" })) : null, renderDateRange?.(), renderSummaryCards ? (renderSummaryCards()) : busy ? (_jsx(
|
|
24
|
+
return (_jsxs(Div, { className: className, children: [renderHeader ? (renderHeader()) : labels.title ? (_jsx(Heading, { level: 1, className: "mb-6", size: "2xl", weight: "bold", children: labels.title })) : null, query.error ? (_jsx(Alert, { variant: "error", title: "Could not load analytics", children: query.error instanceof Error ? query.error.message : "Unknown error" })) : null, renderDateRange?.(), renderSummaryCards ? (renderSummaryCards()) : busy ? (_jsx(Grid, { cols: "statTiles", gap: "md", children: Array.from({ length: 4 }).map((_, index) => (_jsx(Div, { className: "h-28 border border-[var(--appkit-color-border-subtle)] animate-pulse", surface: "default", rounded: "xl" }, index))) })) : summary ? (_jsxs(Grid, { cols: "statTiles", gap: "md", children: [_jsx(AdminStatCard, { label: labels.totalRevenue ?? "Total Revenue", value: formatRevenue(summary.totalRevenue) }), _jsx(AdminStatCard, { label: labels.totalOrders ?? "Total Orders", value: String(summary.totalOrders) }), _jsx(AdminStatCard, { label: labels.revenueThisMonth ?? "Revenue This Month", value: formatRevenue(summary.revenueThisMonth) }), _jsx(AdminStatCard, { label: labels.ordersThisMonth ?? "Orders This Month", value: String(summary.newOrdersThisMonth) })] })) : null, renderCharts ? (renderCharts()) : ordersByMonth.length > 0 ? (_jsxs(Stack, { gap: "lg", children: [_jsx(AdminRevenueChart, { data: ordersByMonth, labels: { title: labels.revenueByMonth } }), _jsx(AdminOrdersChart, { data: ordersByMonth, labels: { title: labels.ordersByMonth } })] })) : null, renderTable ? (renderTable()) : topProducts.length > 0 ? (_jsx(AdminTopProductsTable, { products: topProducts, labels: {
|
|
25
25
|
title: labels.topProducts,
|
|
26
26
|
orders: labels.orders,
|
|
27
27
|
view: labels.view,
|
|
@@ -5,7 +5,7 @@ import { SIEVE_OP, Stack, sieveFilter } from "@mohasinac/appkit/client";
|
|
|
5
5
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
6
6
|
import React, { useCallback } from "react";
|
|
7
7
|
import { useQueryClient } from "@tanstack/react-query";
|
|
8
|
-
import { Div, FilterChipGroup, ListingLayout, Text, useToast } from "../../../ui";
|
|
8
|
+
import { Div, Grid, FilterChipGroup, ListingLayout, Text, useToast } from "../../../ui";
|
|
9
9
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
10
|
import { ROW_ACTION_META, ROW_ACTION_ID, ADMIN_BULK_ACTIONS } from "../../products/constants/action-defs";
|
|
11
11
|
import { ADMIN_COUPON_TYPE_TABS } from "../constants/filter-tabs";
|
|
@@ -116,7 +116,7 @@ export function AdminCouponsView({ children, onBulkArchive, onBulkDelete, ...pro
|
|
|
116
116
|
], value: pendingFilters.showExpired ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, showExpired: id })), allId: "" })] })),
|
|
117
117
|
renderCards: (rows, _view, selectionCtx, isLoading) => {
|
|
118
118
|
rowsRef.current = rows;
|
|
119
|
-
return isLoading ? (_jsx(
|
|
119
|
+
return isLoading ? (_jsx(Grid, { cols: "cardsWide", gap: "3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Stack, { border: "skeleton", className: "animate-pulse", gap: "3", rounded: "xl", padding: "md", children: [_jsx(Div, { className: "h-6 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" })] }, i))) })) : rows.length === 0 ? (_jsx(Div, { className: "text-center", padding: "y-4xl", children: _jsx(Text, { color: "faint", children: "No coupons found" }) })) : (_jsx(Grid, { cols: "cardsWide", gap: "3", children: rows.map((row) => (_jsx(CouponCard, { coupon: row.raw, onEdit: selectionCtx.openEditPanel, onToggleActive: handleToggle, onDelete: handleDelete }, row.id))) }));
|
|
120
120
|
},
|
|
121
121
|
renderEditor: ({ editId, closePanel }) => (_jsx(AdminCouponEditorView, { couponId: editId ?? undefined, initialData: editId ? rowsRef.current.find((r) => r.id === editId)?.raw : undefined, onSaved: closePanel, onDeleted: closePanel, embedded: true })),
|
|
122
122
|
resolveEditorTitle: ({ isCreate }) => (isCreate ? "Add Coupon" : "Edit Coupon"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Users, Package, Store, ShoppingBag, FileText, Settings, Shield, BarChart2, AlertTriangle, BookOpen, MessageSquare, CreditCard, } from "lucide-react";
|
|
3
|
-
import { Alert, Div, Heading, Row, Section, Stack, Text, TextLink } from "../../../ui";
|
|
3
|
+
import { Alert, Div, Grid, Heading, Row, Section, Stack, Text, TextLink } from "../../../ui";
|
|
4
4
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
5
5
|
const CLS_WARN_PANEL = "rounded-2xl border border-amber-200 bg-warning-surface dark:border-amber-800 p-[var(--appkit-space-6)]";
|
|
6
6
|
const CLS_WARN_HEAD = "font-semibold text-amber-800 dark:text-amber-300 mb-1";
|
|
@@ -88,5 +88,5 @@ export function AdminGuideHubView({ permissions = [], isFullAdmin = false }) {
|
|
|
88
88
|
const hasPermission = (perm) => isFullAdmin || permissions.includes(perm);
|
|
89
89
|
const visibleCards = GUIDE_CARDS.filter((c) => hasPermission(c.permission));
|
|
90
90
|
const hasAny = visibleCards.length > 0;
|
|
91
|
-
return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse", className: "/80 tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Admin & Employee Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: "LetItRip Internal Guide" }), _jsx(Text, { color: "inverse", className: "/80", size: "base", children: "Everything you need to operate and manage LetItRip \u2014 India's largest collectibles marketplace." })] }) }), _jsx(Alert, { variant: "info", compact: true, children: "These guides describe expected platform behaviour. Real access to each section is enforced independently by the permission system \u2014 this page is reference only." }), !hasAny && (_jsx(Alert, { variant: "info", title: "Limited access", children: "Your permission set doesn't include read access to any admin sections yet. Contact your admin to request the permissions you need." })), hasAny && (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)] mb-4", size: "lg", weight: "semibold", children: "Guides" }), _jsx(
|
|
91
|
+
return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse", className: "/80 tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Admin & Employee Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: "LetItRip Internal Guide" }), _jsx(Text, { color: "inverse", className: "/80", size: "base", children: "Everything you need to operate and manage LetItRip \u2014 India's largest collectibles marketplace." })] }) }), _jsx(Alert, { variant: "info", compact: true, children: "These guides describe expected platform behaviour. Real access to each section is enforced independently by the permission system \u2014 this page is reference only." }), !hasAny && (_jsx(Alert, { variant: "info", title: "Limited access", children: "Your permission set doesn't include read access to any admin sections yet. Contact your admin to request the permissions you need." })), hasAny && (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)] mb-4", size: "lg", weight: "semibold", children: "Guides" }), _jsx(Grid, { cols: "cards", gap: "md", children: visibleCards.map(({ Icon, title, description, href }) => (_jsxs(TextLink, { variant: "bare", href: href, rounded: "xl", layout: "flex-col", gap: "md", paddingX: "lg", paddingY: "lg", shadow: "hover-md", className: "group border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] hover:border-[var(--appkit-color-primary)] transition-all", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-9 h-9 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "lg", children: _jsx(Icon, { className: "w-4 h-4 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text)] group-hover:text-[var(--appkit-color-primary)] transition-colors", weight: "semibold", children: title })] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: description }), _jsx(Text, { className: "text-[var(--appkit-color-primary)] mt-auto", size: "xs", weight: "medium", children: "Read guide \u2192" })] }, title))) })] })), _jsx(Section, { className: CLS_WARN_PANEL, children: _jsxs(Row, { align: "start", gap: "3", children: [_jsx(AlertTriangle, { className: "w-5 h-5 text-warning flex-shrink-0 mt-0.5" }), _jsxs(Div, { children: [_jsx(Text, { className: CLS_WARN_HEAD, children: "Remember: with great access comes responsibility" }), _jsx(Text, { className: CLS_WARN_BODY, children: "Admin actions are logged. Never access user PII (email, phone) without a legitimate support reason. Never grant capabilities or permissions without documented approval. When in doubt, escalate to a senior admin." })] })] }) })] }));
|
|
92
92
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { Alert, Button, Div, Input, Row, Select, Stack, StackedViewShell, Text, useToast } from "../../../ui";
|
|
4
|
+
import { Alert, Button, Div, Grid, Input, Row, Select, Stack, StackedViewShell, Text, useToast } from "../../../ui";
|
|
5
5
|
import { normalizeError } from "../../../errors/normalize";
|
|
6
6
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
@@ -64,7 +64,7 @@ function MediaBrowser({ onCopy }) {
|
|
|
64
64
|
return files;
|
|
65
65
|
return files.filter((f) => f.name.toLowerCase().includes(q));
|
|
66
66
|
}, [files, search]);
|
|
67
|
-
return (_jsxs(Stack, { className: `${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Browse existing media" }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Select, { value: prefix, onChange: (e) => setPrefix(e.target.value), options: PREFIX_OPTIONS, "aria-label": "Prefix filter", wrapperClassName: "min-w-[140px]" }), _jsx(Input, { type: "text", placeholder: "Search filename\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "flex-1 min-w-[160px]" })] }), error && (_jsx(Alert, { variant: "error", title: "Load failed", children: error })), _jsxs(
|
|
67
|
+
return (_jsxs(Stack, { className: `${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Browse existing media" }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Select, { value: prefix, onChange: (e) => setPrefix(e.target.value), options: PREFIX_OPTIONS, "aria-label": "Prefix filter", wrapperClassName: "min-w-[140px]" }), _jsx(Input, { type: "text", placeholder: "Search filename\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "flex-1 min-w-[160px]" })] }), error && (_jsx(Alert, { variant: "error", title: "Load failed", children: error })), _jsxs(Grid, { cols: "navTiles", gap: "sm", children: [filtered.map((f) => {
|
|
68
68
|
const isImage = (f.contentType ?? "").startsWith("image/");
|
|
69
69
|
return (_jsxs(Div, { className: "group overflow-hidden", surface: "muted", rounded: "lg", border: "default", children: [_jsx(Row, { color: "muted", textSize: "xs", className: "relative aspect-square", surface: "subtle", align: "center", justify: "center", children: isImage ? (_jsx(MediaImage, { src: f.downloadURL, alt: f.name, size: "thumbnail" })) : (_jsx(Text, { align: "center", paddingX: "x-xs", className: "break-all", children: f.contentType ?? "file" })) }), _jsxs(Div, { className: "p-[0.375rem]", children: [_jsx(Text, { className: "truncate text-[10px] font-mono", color: "muted", children: f.name.split("/").pop() }), _jsx(Button, { type: "button", variant: "ghost", textSize: "xs", className: "mt-1 w-full", onClick: () => onCopy(f.downloadURL), children: ACTIONS.MEDIA["copy-url"].label })] })] }, f.name));
|
|
70
70
|
}), filtered.length === 0 && !isLoading && (_jsx(Text, { paddingY: "lg", className: "col-span-full", color: "muted", size: "sm", align: "center", children: "No files found." }))] }), _jsxs(Row, { align: "center", justify: "between", children: [_jsxs(Text, { size: "xs", color: "muted", children: [filtered.length, " file(s)", nextPageToken ? " · more available" : ""] }), nextPageToken && (_jsx(Button, { type: "button", variant: "outline", onClick: () => void load(nextPageToken), disabled: isLoading, children: isLoading ? "Loading…" : "Load more" }))] })] }));
|
|
@@ -69,7 +69,7 @@ export function AdminViewCards({ rows, view, isLoading, emptyLabel = "No items f
|
|
|
69
69
|
if (view === "list") {
|
|
70
70
|
return (_jsx(Div, { rounded: "xl", border: "subtle", className: `${__O.hidden} divide-y divide-zinc-100 divide-[var(--appkit-color-border)]`, children: Array.from({ length: count }).map((_, i) => _jsx(SkeletonCard, { view: "list" }, i)) }));
|
|
71
71
|
}
|
|
72
|
-
return (_jsx(Grid, {
|
|
72
|
+
return (_jsx(Grid, { cols: "cards", gap: "md", children: Array.from({ length: count }).map((_, i) => _jsx(SkeletonCard, { view: "grid" }, i)) }));
|
|
73
73
|
}
|
|
74
74
|
if (rows.length === 0) {
|
|
75
75
|
return (_jsx(Text, { paddingY: "3xl", size: "sm", color: "muted", align: "center", children: emptyLabel }));
|
|
@@ -77,5 +77,5 @@ export function AdminViewCards({ rows, view, isLoading, emptyLabel = "No items f
|
|
|
77
77
|
if (view === "list") {
|
|
78
78
|
return (_jsx(Div, { rounded: "xl", border: "subtle", className: `${__O.hidden} divide-y divide-zinc-100 divide-[var(--appkit-color-border)]`, children: rows.map((row) => (_jsx(AdminCardItem, { row: row, view: "list", selected: selectedIdSet?.has(row.id) ?? false, onToggleSelect: onToggleSelect, onRowClick: onRowClick, renderRowActions: renderRowActions, resourceIcon: resourceIcon }, row.id))) }));
|
|
79
79
|
}
|
|
80
|
-
return (_jsx(Grid, {
|
|
80
|
+
return (_jsx(Grid, { cols: "cards", gap: "md", children: rows.map((row) => (_jsx(AdminCardItem, { row: row, view: "grid", selected: selectedIdSet?.has(row.id) ?? false, onToggleSelect: onToggleSelect, onRowClick: onRowClick, renderRowActions: renderRowActions, resourceIcon: resourceIcon }, row.id))) }));
|
|
81
81
|
}
|
|
@@ -203,6 +203,6 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
203
203
|
bidCount: typeof r.bidCount === "number" ? r.bidCount : undefined,
|
|
204
204
|
slug: typeof r.slug === "string" ? r.slug : undefined,
|
|
205
205
|
}));
|
|
206
|
-
return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related
|
|
206
|
+
return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related })) })), _jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] }));
|
|
207
207
|
} }), renderAuctionStoreReviews(storeId), onPlaceBid && (_jsx(AuctionBottomActions, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow }))] })] }));
|
|
208
208
|
}
|
|
@@ -13,10 +13,9 @@ export interface MarketplaceAuctionGridProps {
|
|
|
13
13
|
selectable?: boolean;
|
|
14
14
|
selectedIds?: string[];
|
|
15
15
|
onSelectionChange?: (ids: string[]) => void;
|
|
16
|
-
gridClassName?: string;
|
|
17
16
|
emptyIcon?: ReactNode;
|
|
18
17
|
labels?: MarketplaceAuctionGridLabels;
|
|
19
18
|
cardLabels?: MarketplaceAuctionCardLabels;
|
|
20
19
|
wishlistActions?: WishlistToggleActions;
|
|
21
20
|
}
|
|
22
|
-
export declare function MarketplaceAuctionGrid({ auctions, loading, skeletonCount, variant, selectable, selectedIds, onSelectionChange,
|
|
21
|
+
export declare function MarketplaceAuctionGrid({ auctions, loading, skeletonCount, variant, selectable, selectedIds, onSelectionChange, emptyIcon, labels, cardLabels, wishlistActions, }: MarketplaceAuctionGridProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Gavel } from "lucide-react";
|
|
4
|
-
import { Div, Row, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Div, Grid, Row, Stack, Text } from "../../../ui";
|
|
5
5
|
import { MarketplaceAuctionCard, } from "./MarketplaceAuctionCard";
|
|
6
6
|
const __P = {
|
|
7
7
|
p3: "p-[var(--appkit-space-3)]",
|
|
@@ -20,7 +20,7 @@ function AuctionCardSkeleton({ variant = "grid", }) {
|
|
|
20
20
|
}
|
|
21
21
|
return (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, rounded: "xl", surface: "subtle", children: [_jsx(Div, { surface: "skeleton", className: "aspect-square" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { surface: "skeleton", className: "h-4 w-3/4", rounded: "default" }), _jsx(Div, { surface: "skeleton", className: "h-3 w-1/3", rounded: "default" }), _jsx(Div, { surface: "skeleton", className: "h-5 w-1/2", rounded: "default" }), _jsxs(Row, { justify: "between", children: [_jsx(Div, { surface: "skeleton", className: "h-3 w-1/4", rounded: "default" }), _jsx(Div, { surface: "skeleton", className: "h-3 w-1/3", rounded: "default" })] }), _jsx(Row, { gap: "sm", children: _jsx(Div, { className: CLS_SKELETON_BTN }) })] })] }));
|
|
22
22
|
}
|
|
23
|
-
export function MarketplaceAuctionGrid({ auctions, loading = false, skeletonCount = 12, variant = "grid", selectable = false, selectedIds = [], onSelectionChange,
|
|
23
|
+
export function MarketplaceAuctionGrid({ auctions, loading = false, skeletonCount = 12, variant = "grid", selectable = false, selectedIds = [], onSelectionChange, emptyIcon, labels, cardLabels, wishlistActions, }) {
|
|
24
24
|
const mergedLabels = { ...DEFAULT_GRID_LABELS, ...labels };
|
|
25
25
|
const handleSelect = (id, selected) => {
|
|
26
26
|
if (!onSelectionChange)
|
|
@@ -31,14 +31,15 @@ export function MarketplaceAuctionGrid({ auctions, loading = false, skeletonCoun
|
|
|
31
31
|
}
|
|
32
32
|
onSelectionChange(selectedIds.filter((selectedId) => selectedId !== id));
|
|
33
33
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
/* The container is either the canonical card ladder or a plain stack — the
|
|
35
|
+
caller no longer supplies a grid className, so a store's auctions tab and
|
|
36
|
+
/auctions can no longer drift apart in column count. */
|
|
37
|
+
const Container = ({ children }) => variant === "list" ? (_jsx(Stack, { gap: "md", children: children })) : (_jsx(Grid, { cols: "cards", gap: "lg", children: children }));
|
|
37
38
|
if (loading) {
|
|
38
|
-
return (_jsx(
|
|
39
|
+
return (_jsx(Container, { children: Array.from({ length: skeletonCount }).map((_, index) => (_jsx(AuctionCardSkeleton, { variant: variant }, `auction-skeleton-${index}`))) }));
|
|
39
40
|
}
|
|
40
41
|
if (auctions.length === 0) {
|
|
41
42
|
return (_jsxs(Stack, { justify: "center", padding: "y-6xl", align: "start", gap: "3", className: "text-left", children: [emptyIcon ?? _jsx(Gavel, { className: "h-16 w-16" }), _jsx(Text, { size: "xl", weight: "medium", color: "primary", children: mergedLabels.emptyTitle }), _jsx(Text, { size: "sm", color: "muted", children: mergedLabels.emptyDescription })] }));
|
|
42
43
|
}
|
|
43
|
-
return (_jsx(
|
|
44
|
+
return (_jsx(Container, { children: auctions.map((auction) => (_jsx(MarketplaceAuctionCard, { product: auction, variant: variant, selectable: selectable, isSelected: selectedIds.includes(auction.id), onSelect: handleSelect, labels: cardLabels, wishlistActions: wishlistActions }, auction.id))) }));
|
|
44
45
|
}
|
|
@@ -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)
|
|
@@ -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;
|