@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeError } from "./normalize";
|
|
1
2
|
import { ERROR_CODES, BID_ERROR_CODES, OFFER_ERROR_CODES } from "./error-codes";
|
|
2
3
|
import { HTTP_ERROR_CODES } from "./error-mapping";
|
|
3
4
|
const k = (code) => `errors.codes.${code}`;
|
|
@@ -240,3 +241,48 @@ export const ERROR_DISPLAY_MAP = {
|
|
|
240
241
|
export function getErrorDisplay(code) {
|
|
241
242
|
return ERROR_DISPLAY_MAP[code] ?? { messageKey: "errors.codes.UNKNOWN" };
|
|
242
243
|
}
|
|
244
|
+
/** Last-resort copy when no translator is available. Never a server string. */
|
|
245
|
+
export const GENERIC_USER_MESSAGE = "Something went wrong. Please try again.";
|
|
246
|
+
/**
|
|
247
|
+
* The user-facing message for an error code. **Never returns server text.**
|
|
248
|
+
*
|
|
249
|
+
* 🛑 This is the whole point: every previous way of doing this ended in a raw
|
|
250
|
+
* fallback. `ERROR_DISPLAY_MAP[code] ?? error` — the pattern CLAUDE.md Rule #9
|
|
251
|
+
* §6 itself documented — falls through to the server's own message, and that
|
|
252
|
+
* is how a Node `MODULE_NOT_FOUND`, complete with
|
|
253
|
+
* `/var/task/.next/server/chunks/...` and its full require stack, was rendered
|
|
254
|
+
* as copy inside the "Place your bid" modal. `surfaceError` had the same tail.
|
|
255
|
+
* There was no shared helper that did this correctly, which is why ~60 call
|
|
256
|
+
* sites each invented `err.message`.
|
|
257
|
+
*
|
|
258
|
+
* The chain is: code -> messageKey -> translation -> `errors.codes.UNKNOWN` ->
|
|
259
|
+
* `GENERIC_USER_MESSAGE`. It terminates in a constant, deliberately.
|
|
260
|
+
*
|
|
261
|
+
* @param code stable error code from an `ActionResult` / error envelope
|
|
262
|
+
* @param t a next-intl translator; omitted, the generic string is used
|
|
263
|
+
* @param options `fallback` overrides the generic string with FEATURE-SPECIFIC
|
|
264
|
+
* copy you have authored ("Could not place your bid"). It must
|
|
265
|
+
* never be a value derived from an error object.
|
|
266
|
+
*/
|
|
267
|
+
export function toUserMessage(code, t, options) {
|
|
268
|
+
const generic = options?.fallback ?? GENERIC_USER_MESSAGE;
|
|
269
|
+
if (!code)
|
|
270
|
+
return generic;
|
|
271
|
+
const { messageKey } = getErrorDisplay(code);
|
|
272
|
+
if (!t)
|
|
273
|
+
return generic;
|
|
274
|
+
try {
|
|
275
|
+
const translated = t(messageKey);
|
|
276
|
+
// next-intl returns the key itself (or throws) when a message is missing;
|
|
277
|
+
// either way that is not something to show a user.
|
|
278
|
+
if (translated && translated !== messageKey)
|
|
279
|
+
return translated;
|
|
280
|
+
// next-intl throws MISSING_MESSAGE for an absent key. This function's whole
|
|
281
|
+
// contract is to degrade to generic copy rather than let ANY internal
|
|
282
|
+
// string — including an i18n internal — reach the user.
|
|
283
|
+
}
|
|
284
|
+
catch (translateErr) {
|
|
285
|
+
void normalizeError(translateErr);
|
|
286
|
+
}
|
|
287
|
+
return generic;
|
|
288
|
+
}
|
|
@@ -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
|
}
|
|
@@ -3,7 +3,18 @@ import { type AddressBanStatus, type AddressCreateInput, type AddressDocument, t
|
|
|
3
3
|
export declare class AddressesRepository extends BaseRepository<AddressDocument> {
|
|
4
4
|
constructor();
|
|
5
5
|
private decryptAddress;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Declared, not hand-rolled.
|
|
8
|
+
*
|
|
9
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
10
|
+
* `encryptAddressData` did — encrypt the listed fields, then add the mapped blind
|
|
11
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
12
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
13
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
14
|
+
* encryption lived in a private method only three call sites remembered to
|
|
15
|
+
* call.
|
|
16
|
+
*/
|
|
17
|
+
protected piiFields: readonly ["fullName", "phone", "addressLine1", "addressLine2"];
|
|
7
18
|
protected mapDoc<D = AddressDocument>(snap: DocumentSnapshot): D;
|
|
8
19
|
createWithId(id: string, data: Partial<AddressDocument>): Promise<AddressDocument>;
|
|
9
20
|
update(id: string, data: Partial<AddressDocument>): Promise<AddressDocument>;
|
|
@@ -13,28 +13,37 @@ import { normalizeError } from "../../../errors/normalize";
|
|
|
13
13
|
import { DatabaseError } from "../../../errors";
|
|
14
14
|
import { serverLogger } from "../../../monitoring";
|
|
15
15
|
import { BaseRepository, getFirestoreCount, prepareForFirestore, } from "../../../providers/db-firebase";
|
|
16
|
-
import { ADDRESS_PII_FIELDS, decryptPiiFields,
|
|
16
|
+
import { ADDRESS_PII_FIELDS, decryptPiiFields, } from "../../../security";
|
|
17
17
|
import { ADDRESS_FIELDS, ADDRESSES_COLLECTION, } from "../schemas";
|
|
18
18
|
export class AddressesRepository extends BaseRepository {
|
|
19
19
|
constructor() {
|
|
20
20
|
super(ADDRESSES_COLLECTION);
|
|
21
|
+
/**
|
|
22
|
+
* Declared, not hand-rolled.
|
|
23
|
+
*
|
|
24
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
25
|
+
* `encryptAddressData` did — encrypt the listed fields, then add the mapped blind
|
|
26
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
27
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
28
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
29
|
+
* encryption lived in a private method only three call sites remembered to
|
|
30
|
+
* call.
|
|
31
|
+
*/
|
|
32
|
+
this.piiFields = ADDRESS_PII_FIELDS;
|
|
21
33
|
}
|
|
22
34
|
decryptAddress(doc) {
|
|
23
35
|
return decryptPiiFields(doc, [...ADDRESS_PII_FIELDS]);
|
|
24
36
|
}
|
|
25
|
-
encryptAddressData(data) {
|
|
26
|
-
return encryptPiiFields(data, [...ADDRESS_PII_FIELDS]);
|
|
27
|
-
}
|
|
28
37
|
mapDoc(snap) {
|
|
29
38
|
const raw = super.mapDoc(snap);
|
|
30
39
|
return this.decryptAddress(raw);
|
|
31
40
|
}
|
|
32
41
|
async createWithId(id, data) {
|
|
33
|
-
const encrypted = this.
|
|
42
|
+
const encrypted = this.applyWriteHooks(data);
|
|
34
43
|
return super.createWithId(id, encrypted);
|
|
35
44
|
}
|
|
36
45
|
async update(id, data) {
|
|
37
|
-
const encrypted = this.
|
|
46
|
+
const encrypted = this.applyWriteHooks(data);
|
|
38
47
|
return super.update(id, encrypted);
|
|
39
48
|
}
|
|
40
49
|
async listByOwner(ownerType, ownerId) {
|
|
@@ -91,7 +100,7 @@ export class AddressesRepository extends BaseRepository {
|
|
|
91
100
|
createdAt: now,
|
|
92
101
|
updatedAt: now,
|
|
93
102
|
};
|
|
94
|
-
const encrypted = this.
|
|
103
|
+
const encrypted = this.applyWriteHooks(addressData);
|
|
95
104
|
await docRef.set(prepareForFirestore(encrypted));
|
|
96
105
|
serverLogger.info("Address created", {
|
|
97
106
|
ownerType,
|
|
@@ -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
|
}
|