@mohasinac/appkit 3.6.4 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +17 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
- package/dist/_internal/client/features/tour/TourProvider.d.ts +5 -2
- package/dist/_internal/client/features/tour/TourProvider.js +125 -12
- package/dist/_internal/client/theme/ThemeProvider.js +1 -1
- package/dist/_internal/server/features/faqs/og.js +5 -1
- package/dist/_internal/server/features/reviews/og.js +5 -1
- package/dist/_internal/server/features/seo/manifest.js +2 -1
- package/dist/_internal/server/features/seo/og.js +7 -1
- package/dist/_internal/server/features/tester/visibility.d.ts +13 -0
- package/dist/_internal/server/features/tester/visibility.js +28 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +9 -1
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/resetOtpVerification.d.ts +17 -0
- package/dist/_internal/server/jobs/core/resetOtpVerification.js +88 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +6 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +35 -0
- package/dist/_internal/server/jobs/handlers/_helpers.d.ts +6 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +12 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +2 -0
- package/dist/_internal/server/jobs/handlers/messages.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/messages.js +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +48 -0
- package/dist/_internal/shared/tokens/index.d.ts +12 -12
- package/dist/client.d.ts +15 -3
- package/dist/client.js +10 -2
- package/dist/constants/api-endpoints.d.ts +36 -12
- package/dist/constants/api-endpoints.js +16 -8
- package/dist/constants/field-names.d.ts +9 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/features/account/components/LinkedAccountsSection.d.ts +11 -0
- package/dist/features/account/components/LinkedAccountsSection.js +11 -0
- package/dist/features/account/components/UserSidebar.d.ts +3 -1
- package/dist/features/account/components/UserSidebar.js +11 -15
- package/dist/features/account/hooks/useCollapsedSections.d.ts +20 -0
- package/dist/features/account/hooks/useCollapsedSections.js +63 -0
- package/dist/features/account/hooks/useProfile.d.ts +8 -0
- package/dist/features/account/index.d.ts +1 -0
- package/dist/features/account/index.js +1 -0
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -2
- package/dist/features/admin/components/AdminCarouselView.js +2 -2
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminProductEditorView.js +5 -2
- package/dist/features/admin/components/AdminSectionsView.js +3 -18
- package/dist/features/admin/components/AdminSidebar.js +10 -8
- package/dist/features/admin/components/AdminSiteSettingsView.js +133 -127
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.d.ts +9 -0
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +129 -0
- package/dist/features/admin/components/AdminTesterChecklistView.d.ts +6 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +93 -0
- package/dist/features/admin/components/AdminUserEditorView.d.ts +3 -1
- package/dist/features/admin/components/AdminUserEditorView.js +7 -1
- package/dist/features/admin/components/AdminUsersView.js +1 -1
- package/dist/features/admin/components/DataListingView.js +2 -2
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +8 -0
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +41 -0
- package/dist/features/admin/components/index.d.ts +6 -0
- package/dist/features/admin/components/index.js +3 -0
- package/dist/features/admin/schemas/firestore.d.ts +0 -7
- package/dist/features/admin/schemas/firestore.js +6 -6
- package/dist/features/analytics/components/PageViewTracker.d.ts +12 -0
- package/dist/features/analytics/components/PageViewTracker.js +22 -0
- package/dist/features/analytics/repository/page-views.repository.d.ts +32 -0
- package/dist/features/analytics/repository/page-views.repository.js +43 -0
- package/dist/features/analytics/types.d.ts +6 -0
- package/dist/features/analytics/types.js +13 -0
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +12 -0
- package/dist/features/auth/hooks/useAuth.js +109 -0
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/schemas/firestore.d.ts +17 -1
- package/dist/features/auth/schemas/firestore.js +2 -0
- package/dist/features/blog/components/BlogIndexListing.js +2 -2
- package/dist/features/blog/schemas/firestore.d.ts +4 -0
- package/dist/features/cart/actions/cart-actions.js +20 -2
- package/dist/features/categories/components/CategoriesIndexListing.js +2 -2
- package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +3 -1
- package/dist/features/categories/components/CategoryProductsListing.js +2 -2
- package/dist/features/categories/schemas/firestore.d.ts +4 -0
- package/dist/features/classified/components/ClassifiedIndexListing.js +2 -2
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +2 -2
- package/dist/features/email/primitives.d.ts +2 -2
- package/dist/features/email/primitives.js +2 -2
- package/dist/features/events/components/EventsIndexListing.js +2 -2
- package/dist/features/events/schemas/firestore.d.ts +4 -0
- package/dist/features/layout/BackToTop.d.ts +6 -1
- package/dist/features/layout/BackToTop.js +31 -6
- package/dist/features/live/components/LiveItemsIndexListing.js +2 -2
- package/dist/features/media/MediaVideo.js +5 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -2
- package/dist/features/products/components/ProductsIndexListing.js +2 -2
- package/dist/features/products/constants/action-defs.d.ts +2 -0
- package/dist/features/products/constants/action-defs.js +2 -0
- package/dist/features/products/schemas/firestore.d.ts +4 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -2
- package/dist/features/reviews/components/ReviewsIndexListing.js +2 -2
- package/dist/features/seller/actions/seller-actions.js +11 -6
- package/dist/features/seller/components/SellerAddressesView.js +2 -2
- package/dist/features/seller/components/SellerBidsView.js +2 -2
- package/dist/features/seller/components/SellerOrdersView.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +2 -2
- package/dist/features/seller/components/SellerSidebar.d.ts +3 -1
- package/dist/features/seller/components/SellerSidebar.js +11 -10
- package/dist/features/seller/components/SellerStoreCategoriesView.js +2 -2
- package/dist/features/seller/components/SellerTemplatesView.js +2 -2
- package/dist/features/shell/StepForm.js +4 -4
- package/dist/features/stores/actions/store-query-actions.d.ts +5 -4
- package/dist/features/stores/actions/store-query-actions.js +11 -8
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -2
- package/dist/features/stores/components/StoreClassifiedsListing.js +2 -2
- package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -2
- package/dist/features/stores/components/StoreLiveItemsListing.js +2 -2
- package/dist/features/stores/components/StorePreOrdersListing.js +2 -2
- package/dist/features/stores/components/StoreProductsListing.js +2 -2
- package/dist/features/stores/components/StoreReviewsListing.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +2 -2
- package/dist/features/stores/schemas/firestore.d.ts +4 -0
- package/dist/features/tester/actions/checklist-item-actions.d.ts +69 -0
- package/dist/features/tester/actions/checklist-item-actions.js +42 -0
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +22 -0
- package/dist/features/tester/components/AdminTesterFeedbackListView.d.ts +3 -0
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +100 -0
- package/dist/features/tester/components/AdminTesterFeedbackReportView.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackReportView.js +30 -0
- package/dist/features/tester/components/AdminTesterFeedbackView.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackView.js +9 -0
- package/dist/features/tester/components/TesterChecklistStepRow.d.ts +18 -0
- package/dist/features/tester/components/TesterChecklistStepRow.js +29 -0
- package/dist/features/tester/components/TesterFeedbackChart.d.ts +14 -0
- package/dist/features/tester/components/TesterFeedbackChart.js +18 -0
- package/dist/features/tester/components/TesterHubView.d.ts +5 -0
- package/dist/features/tester/components/TesterHubView.js +71 -0
- package/dist/features/tester/components/index.d.ts +11 -0
- package/dist/features/tester/components/index.js +7 -0
- package/dist/features/tester/index.d.ts +2 -0
- package/dist/features/tester/index.js +2 -0
- package/dist/features/tester/repository/index.d.ts +2 -0
- package/dist/features/tester/repository/index.js +2 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +13 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.js +55 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +42 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +98 -0
- package/dist/features/tester/schemas/firestore.d.ts +85 -0
- package/dist/features/tester/schemas/firestore.js +93 -0
- package/dist/features/tester/schemas/index.d.ts +1 -0
- package/dist/features/tester/schemas/index.js +1 -0
- package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/blog-tester-seed-data.js +40 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +177 -0
- package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/events-tester-seed-data.js +50 -0
- package/dist/features/tester/seed-data/index.d.ts +6 -0
- package/dist/features/tester/seed-data/index.js +6 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +151 -0
- package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +40 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +248 -0
- package/dist/features/tester/seed-data/tester-ttl.d.ts +4 -0
- package/dist/features/tester/seed-data/tester-ttl.js +6 -0
- package/dist/features/tester/server.d.ts +9 -0
- package/dist/features/tester/server.js +8 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +51 -1
- package/dist/next/api/api-response.js +5 -1
- package/dist/next/api/routeHandler.js +10 -1
- package/dist/next/middleware/chain.js +1 -1
- package/dist/next/routing/route-map.d.ts +6 -7
- package/dist/next/routing/route-map.js +3 -7
- package/dist/providers/payment-manual/index.d.ts +2 -2
- package/dist/providers/payment-manual/index.js +2 -2
- package/dist/react/contexts/SessionContext.d.ts +4 -0
- package/dist/react/contexts/SessionContext.js +4 -0
- package/dist/repositories/index.d.ts +4 -0
- package/dist/repositories/index.js +3 -0
- package/dist/security/pii-encrypt.js +12 -2
- package/dist/seed/addresses-seed-data.js +1 -1
- package/dist/seed/bids-seed-data.js +1 -1
- package/dist/seed/blog-posts-seed-data.js +1 -1
- package/dist/seed/carousel-slides-seed-data.js +22 -16
- package/dist/seed/carousels-seed-data.js +1 -1
- package/dist/seed/cart-seed-data.js +1 -1
- package/dist/seed/categories-seed-data.js +1 -1
- package/dist/seed/claimed-coupons-seed-data.js +1 -1
- package/dist/seed/conversations-seed-data.js +1 -1
- package/dist/seed/coupon-usage-seed-data.js +1 -1
- package/dist/seed/coupons-seed-data.js +1 -1
- package/dist/seed/events-seed-data.js +1 -1
- package/dist/seed/faq-seed-data.js +119 -3
- package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
- package/dist/seed/grouped-listings-seed-data.js +1 -1
- package/dist/seed/history-seed-data.js +1 -1
- package/dist/seed/homepage-sections-seed-data.js +1 -1
- package/dist/seed/index.d.ts +2 -2
- package/dist/seed/index.js +7 -3
- package/dist/seed/lottery-entries-seed-data.js +1 -1
- package/dist/seed/manifest.d.ts +4 -3
- package/dist/seed/manifest.js +13 -6
- package/dist/seed/notifications-seed-data.js +1 -1
- package/dist/seed/offers-seed-data.js +1 -1
- package/dist/seed/orders-seed-data.js +1 -1
- package/dist/seed/payouts-seed-data.js +1 -1
- package/dist/seed/products-art-seed-data.js +1 -1
- package/dist/seed/products-auctions-seed-data.js +1 -1
- package/dist/seed/products-classifieds-seed-data.js +1 -1
- package/dist/seed/products-digital-codes-seed-data.js +1 -1
- package/dist/seed/products-live-items-seed-data.js +1 -1
- package/dist/seed/products-preorders-seed-data.d.ts +2 -0
- package/dist/seed/products-preorders-seed-data.js +1 -1
- package/dist/seed/products-prize-draws-seed-data.js +1 -1
- package/dist/seed/products-standard-seed-data.js +1 -1
- package/dist/seed/products-stickers-seed-data.js +1 -1
- package/dist/seed/reviews-seed-data.js +1 -1
- package/dist/seed/scammers-seed-data.js +1 -1
- package/dist/seed/sessions-seed-data.js +1 -1
- package/dist/seed/site-settings-seed-data.js +22 -16
- package/dist/seed/store-addresses-seed-data.js +1 -1
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/seed/stores-seed-data.js +1 -1
- package/dist/seed/support-tickets-seed-data.js +1 -1
- package/dist/seed/tester-checklist-seed-data.d.ts +2 -0
- package/dist/seed/tester-checklist-seed-data.js +248 -0
- package/dist/seed/users-seed-data.js +1 -1
- package/dist/seed/wishlists-seed-data.js +1 -1
- package/dist/server.d.ts +7 -2
- package/dist/server.js +19 -12
- package/dist/styles.css +163 -147
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/color-pairs.d.ts +46 -0
- package/dist/tokens/color-pairs.js +93 -0
- package/dist/tokens/index.d.ts +60 -60
- package/dist/tokens/index.js +30 -30
- package/dist/tokens/themes/cobalt-night.d.ts +3 -2
- package/dist/tokens/themes/cobalt-night.js +3 -2
- package/dist/tokens/themes/default-dark.d.ts +4 -1
- package/dist/tokens/themes/default-dark.js +44 -41
- package/dist/tokens/themes/default-light.d.ts +5 -1
- package/dist/tokens/themes/default-light.js +39 -35
- package/dist/tokens/themes/sunset.js +4 -4
- package/dist/tokens/tokens.css +110 -104
- package/dist/ui/components/CollapsibleSection.d.ts +15 -0
- package/dist/ui/components/CollapsibleSection.js +13 -0
- package/dist/ui/components/ListingToolbar.js +2 -1
- package/dist/ui/components/Semantic.style.css +3 -3
- package/dist/ui/components/SideDrawer.style.css +19 -19
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/StickyToolbar.d.ts +36 -7
- package/dist/ui/components/StickyToolbar.js +63 -5
- package/dist/ui/components/Tabs.js +17 -1
- package/dist/ui/components/Tabs.style.css +10 -0
- package/dist/ui/components/Typography.js +7 -0
- package/dist/ui/components/surface-tokens.d.ts +0 -7
- package/dist/ui/components/surface-tokens.js +7 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.style.css +20 -20
- package/dist/utils/id-generators.d.ts +13 -0
- package/dist/utils/id-generators.js +17 -0
- package/package.json +1 -1
- package/scripts/seed-cli-loader.mjs +15 -0
- package/scripts/seed-cli.mjs +71 -17
|
@@ -183,6 +183,115 @@ export function useGoogleLogin(options) {
|
|
|
183
183
|
authEvent.status === RealtimeEventStatus.PENDING;
|
|
184
184
|
return { mutate, isLoading };
|
|
185
185
|
}
|
|
186
|
+
/** Connects the current (already-logged-in) user's account to a Google account,
|
|
187
|
+
* without switching sessions. Reuses the same popup + RTDB/postMessage signal
|
|
188
|
+
* mechanism as useGoogleLogin, but POSTs { mode: "link" } to EVENT_INIT so the
|
|
189
|
+
* server callback records the link on the caller's own profile instead of
|
|
190
|
+
* resolving/creating a separate Firebase Auth user. */
|
|
191
|
+
export function useLinkGoogleAccount(options) {
|
|
192
|
+
const authEvent = useAuthEvent();
|
|
193
|
+
const [initiating, setInitiating] = useState(false);
|
|
194
|
+
const [popupPending, setPopupPending] = useState(false);
|
|
195
|
+
const calledRef = useRef(false);
|
|
196
|
+
const closePollRef = useRef(null);
|
|
197
|
+
const clearClosePoll = useCallback(() => {
|
|
198
|
+
if (closePollRef.current !== null) {
|
|
199
|
+
clearInterval(closePollRef.current);
|
|
200
|
+
closePollRef.current = null;
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
const onSuccessRef = useRef(options?.onSuccess);
|
|
204
|
+
const onErrorRef = useRef(options?.onError);
|
|
205
|
+
const authEventResetRef = useRef(authEvent.reset);
|
|
206
|
+
useEffect(() => { onSuccessRef.current = options?.onSuccess; }, [options?.onSuccess]);
|
|
207
|
+
useEffect(() => { onErrorRef.current = options?.onError; }, [options?.onError]);
|
|
208
|
+
useEffect(() => { authEventResetRef.current = authEvent.reset; }, [authEvent.reset]);
|
|
209
|
+
useEffect(() => clearClosePoll, [clearClosePoll]);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (authEvent.status === RealtimeEventStatus.SUCCESS) {
|
|
212
|
+
setPopupPending(false);
|
|
213
|
+
if (calledRef.current)
|
|
214
|
+
return;
|
|
215
|
+
calledRef.current = true;
|
|
216
|
+
clearClosePoll();
|
|
217
|
+
onSuccessRef.current?.();
|
|
218
|
+
}
|
|
219
|
+
else if (authEvent.status === RealtimeEventStatus.TIMEOUT) {
|
|
220
|
+
setPopupPending(false);
|
|
221
|
+
if (calledRef.current)
|
|
222
|
+
return;
|
|
223
|
+
calledRef.current = true;
|
|
224
|
+
clearClosePoll();
|
|
225
|
+
onErrorRef.current?.(new Error(authEvent.error ?? "Linking timed out. Please try again."));
|
|
226
|
+
}
|
|
227
|
+
}, [authEvent.status, authEvent.error, clearClosePoll]);
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const handleMessage = (event) => {
|
|
230
|
+
if (event.origin !== window.location.origin)
|
|
231
|
+
return;
|
|
232
|
+
if (!event.data || event.data.type !== "letitrip_auth_close")
|
|
233
|
+
return;
|
|
234
|
+
if (calledRef.current)
|
|
235
|
+
return;
|
|
236
|
+
calledRef.current = true;
|
|
237
|
+
setPopupPending(false);
|
|
238
|
+
clearClosePoll();
|
|
239
|
+
authEventResetRef.current();
|
|
240
|
+
if (event.data.status === "success") {
|
|
241
|
+
onSuccessRef.current?.();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
onErrorRef.current?.(new Error(event.data.error ?? "Linking failed. Please try again."));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
window.addEventListener("message", handleMessage);
|
|
248
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
249
|
+
}, [clearClosePoll]);
|
|
250
|
+
const mutate = useCallback(async () => {
|
|
251
|
+
calledRef.current = false;
|
|
252
|
+
const popup = window.open(`${window.location.origin}/auth.html`, "oauth_google_link", "width=500,height=660,left=400,top=100");
|
|
253
|
+
if (!popup) {
|
|
254
|
+
onErrorRef.current?.(new Error("Popup blocked. Please allow popups for this site."));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
setPopupPending(true);
|
|
258
|
+
clearClosePoll();
|
|
259
|
+
closePollRef.current = setInterval(() => {
|
|
260
|
+
if (popup.closed && !calledRef.current) {
|
|
261
|
+
calledRef.current = true;
|
|
262
|
+
clearClosePoll();
|
|
263
|
+
setPopupPending(false);
|
|
264
|
+
authEventResetRef.current();
|
|
265
|
+
onErrorRef.current?.(new Error("Linking was cancelled."));
|
|
266
|
+
}
|
|
267
|
+
}, 500);
|
|
268
|
+
try {
|
|
269
|
+
setInitiating(true);
|
|
270
|
+
authEvent.reset();
|
|
271
|
+
const { eventId, customToken, rtdbEnabled } = await apiClient.post(AUTH_ENDPOINTS.EVENT_INIT, { mode: "link" });
|
|
272
|
+
const url = `${window.location.origin}${AUTH_ENDPOINTS.GOOGLE_START}?eventId=${encodeURIComponent(eventId)}`;
|
|
273
|
+
localStorage.setItem("letitrip_oauth_redirect", url);
|
|
274
|
+
if (rtdbEnabled !== false) {
|
|
275
|
+
authEvent.subscribe(eventId, customToken);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
void normalizeError(err);
|
|
280
|
+
clearClosePoll();
|
|
281
|
+
popup.close();
|
|
282
|
+
setPopupPending(false);
|
|
283
|
+
onErrorRef.current?.(err instanceof Error ? err : new Error("Failed to start account linking."));
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
setInitiating(false);
|
|
287
|
+
}
|
|
288
|
+
}, [authEvent, clearClosePoll]);
|
|
289
|
+
const isLoading = initiating ||
|
|
290
|
+
popupPending ||
|
|
291
|
+
authEvent.status === RealtimeEventStatus.SUBSCRIBING ||
|
|
292
|
+
authEvent.status === RealtimeEventStatus.PENDING;
|
|
293
|
+
return { mutate, isLoading };
|
|
294
|
+
}
|
|
186
295
|
export function useRegister(options) {
|
|
187
296
|
return useMutation({
|
|
188
297
|
mutationFn: async (data) => {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Permission naming: admin:resource:action
|
|
12
12
|
* Ban action naming: plain verb phrase — write_reviews, place_bids, etc.
|
|
13
13
|
*/
|
|
14
|
-
export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
|
|
14
|
+
export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
|
|
15
15
|
/**
|
|
16
16
|
* Granular user actions that can be soft-banned individually.
|
|
17
17
|
* A user may have multiple simultaneous soft bans of different types.
|
|
@@ -47,6 +47,7 @@ export interface UserDocument extends BaseDocument {
|
|
|
47
47
|
storeId?: string;
|
|
48
48
|
storeSlug?: string;
|
|
49
49
|
storeStatus?: "pending" | "approved" | "rejected";
|
|
50
|
+
isTester?: boolean;
|
|
50
51
|
publicProfile?: {
|
|
51
52
|
isPublic: boolean;
|
|
52
53
|
showEmail: boolean;
|
|
@@ -99,6 +100,20 @@ export interface UserDocument extends BaseDocument {
|
|
|
99
100
|
/** User-controlled notification channel and type preferences. */
|
|
100
101
|
notificationPreferences?: import("../../account/types").NotificationPreferences;
|
|
101
102
|
dismissedBannerHash?: string;
|
|
103
|
+
/** True once this account has completed Google sign-in/linking at least once.
|
|
104
|
+
* Set from the server-side OAuth callback — Firebase Auth's own providerData
|
|
105
|
+
* is NOT populated here because the app uses a server-side custom-token
|
|
106
|
+
* exchange rather than the client SDK's linkWithCredential(). */
|
|
107
|
+
googleLinked?: boolean;
|
|
108
|
+
/** The Google account email this profile is linked to, once linked. */
|
|
109
|
+
googleLinkedEmail?: string | null;
|
|
110
|
+
uiPreferences?: {
|
|
111
|
+
/** IDs of collapsible dashboard sections currently collapsed, flat across
|
|
112
|
+
* every page (IDs are page-prefixed, e.g. "admin-dashboard:alerts", to
|
|
113
|
+
* stay unique). Absent/missing IDs default to collapsed for a section the
|
|
114
|
+
* user has never interacted with. See useCollapsedSections. */
|
|
115
|
+
collapsedSections?: string[];
|
|
116
|
+
};
|
|
102
117
|
}
|
|
103
118
|
export interface UserSoftBan {
|
|
104
119
|
action: BannedAction;
|
|
@@ -150,7 +165,7 @@ export interface SellerPayoutDetails {
|
|
|
150
165
|
}
|
|
151
166
|
export declare const USER_COLLECTION: "users";
|
|
152
167
|
export declare const DEFAULT_USER_DATA: Partial<UserDocument>;
|
|
153
|
-
export declare const USER_INDEXED_FIELDS: readonly ["email", "emailIndex", "phoneNumber", "phoneIndex", "role", "disabled", "emailVerified", "phoneVerified", "storeSlug", "storeStatus"];
|
|
168
|
+
export declare const USER_INDEXED_FIELDS: readonly ["email", "emailIndex", "phoneNumber", "phoneIndex", "role", "disabled", "emailVerified", "phoneVerified", "storeSlug", "storeStatus", "isTester"];
|
|
154
169
|
export declare const USER_PUBLIC_FIELDS: readonly ["uid", "displayName", "photoURL", "avatarMetadata", "role", "createdAt", "storeSlug", "publicProfile", "stats"];
|
|
155
170
|
export declare const USER_UPDATABLE_FIELDS: readonly ["displayName", "photoURL"];
|
|
156
171
|
export type UserCreateInput = Omit<UserDocument, "uid" | "id" | "createdAt" | "updatedAt">;
|
|
@@ -196,6 +211,7 @@ export declare const USER_FIELDS: {
|
|
|
196
211
|
readonly STORE_ID: "storeId";
|
|
197
212
|
readonly STORE_SLUG: "storeSlug";
|
|
198
213
|
readonly STORE_STATUS: "storeStatus";
|
|
214
|
+
readonly IS_TESTER: "isTester";
|
|
199
215
|
readonly AVATAR: {
|
|
200
216
|
readonly URL: "avatarMetadata.url";
|
|
201
217
|
readonly POSITION: "avatarMetadata.position";
|
|
@@ -57,6 +57,7 @@ export const USER_INDEXED_FIELDS = [
|
|
|
57
57
|
"phoneVerified",
|
|
58
58
|
"storeSlug",
|
|
59
59
|
"storeStatus",
|
|
60
|
+
"isTester",
|
|
60
61
|
];
|
|
61
62
|
export const USER_PUBLIC_FIELDS = [
|
|
62
63
|
"uid",
|
|
@@ -106,6 +107,7 @@ export const USER_FIELDS = {
|
|
|
106
107
|
STORE_ID: "storeId",
|
|
107
108
|
STORE_SLUG: "storeSlug",
|
|
108
109
|
STORE_STATUS: "storeStatus",
|
|
110
|
+
IS_TESTER: "isTester",
|
|
109
111
|
AVATAR: {
|
|
110
112
|
URL: "avatarMetadata.url",
|
|
111
113
|
POSITION: "avatarMetadata.position",
|
|
@@ -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 } from "../../../ui";
|
|
6
|
+
import { Div, 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";
|
|
@@ -113,5 +113,5 @@ export function BlogIndexListing({ initialData }) {
|
|
|
113
113
|
const commitSearch = useCallback(() => {
|
|
114
114
|
table.set(TABLE_KEYS.QUERY, searchInput.trim());
|
|
115
115
|
}, [searchInput, table]);
|
|
116
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(
|
|
116
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderBlogGrid({ isLoading, posts, view }) }), renderBlogFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable })] }));
|
|
117
117
|
}
|
|
@@ -14,6 +14,10 @@ export interface BlogPostDocument extends BaseDocument {
|
|
|
14
14
|
title: string;
|
|
15
15
|
slug: string;
|
|
16
16
|
excerpt: string;
|
|
17
|
+
/** Tester sandbox flag — set on the shared admin-seeded test post; swept by testerSandboxCleanup. */
|
|
18
|
+
isTestData?: boolean;
|
|
19
|
+
/** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
|
|
20
|
+
testDataExpiresAt?: Date;
|
|
17
21
|
content: string;
|
|
18
22
|
coverImage?: MediaFieldInput;
|
|
19
23
|
contentImages?: MediaField[];
|
|
@@ -8,8 +8,24 @@ import { serverLogger } from "../../../monitoring";
|
|
|
8
8
|
import { ValidationError } from "../../../errors";
|
|
9
9
|
import { cartRepository } from "../repository/cart.repository";
|
|
10
10
|
import { productRepository } from "../../products/repository/products.repository";
|
|
11
|
+
import { storeRepository } from "../../stores/repository/store.repository";
|
|
11
12
|
import { normalizeListingType } from "../../products/utils/listing-type";
|
|
12
13
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
14
|
+
/**
|
|
15
|
+
* Product documents don't always carry a denormalized `storeName` — the
|
|
16
|
+
* create routes only set it when the caller explicitly passes one, so
|
|
17
|
+
* listings created without it leave the field undefined forever. Cart items
|
|
18
|
+
* built from those products then fell back to displaying the raw `storeId`
|
|
19
|
+
* slug (e.g. "store-letitrip-official") in the UI. Resolving from the store
|
|
20
|
+
* document here — the single add-to-cart chokepoint — fixes every caller at
|
|
21
|
+
* once instead of patching each product-creation call site.
|
|
22
|
+
*/
|
|
23
|
+
async function resolveStoreName(storeId, candidate) {
|
|
24
|
+
if (candidate)
|
|
25
|
+
return candidate;
|
|
26
|
+
const store = await storeRepository.findById(storeId).catch(() => null);
|
|
27
|
+
return store?.storeName ?? storeId;
|
|
28
|
+
}
|
|
13
29
|
export async function addItemToCart(userId, input) {
|
|
14
30
|
serverLogger.debug("addItemToCart", { userId, productId: input.productId });
|
|
15
31
|
// SB-UNI-F 2026-05-13 — capability gate. Classified + live listings reject
|
|
@@ -19,7 +35,8 @@ export async function addItemToCart(userId, input) {
|
|
|
19
35
|
if (!canAddToCart(input.listingType)) {
|
|
20
36
|
throw new ValidationError(`Listings of type "${input.listingType}" cannot be added to the cart.`);
|
|
21
37
|
}
|
|
22
|
-
|
|
38
|
+
const storeName = await resolveStoreName(input.storeId, input.storeName);
|
|
39
|
+
return cartRepository.addItem(userId, { ...input, storeName });
|
|
23
40
|
}
|
|
24
41
|
export async function updateCartItem(userId, itemId, input) {
|
|
25
42
|
return cartRepository.updateItem(userId, itemId, input);
|
|
@@ -39,6 +56,7 @@ export async function mergeGuestCart(userId, items) {
|
|
|
39
56
|
if (product.availableQuantity < 1)
|
|
40
57
|
continue;
|
|
41
58
|
const safeQty = Math.min(item.quantity, product.availableQuantity);
|
|
59
|
+
const storeName = await resolveStoreName(product.storeId, product.storeName);
|
|
42
60
|
await cartRepository.addItem(userId, {
|
|
43
61
|
productId: product.id,
|
|
44
62
|
productTitle: product.title,
|
|
@@ -47,7 +65,7 @@ export async function mergeGuestCart(userId, items) {
|
|
|
47
65
|
currency: product.currency ?? getDefaultCurrency(),
|
|
48
66
|
quantity: safeQty,
|
|
49
67
|
storeId: product.storeId,
|
|
50
|
-
storeName
|
|
68
|
+
storeName,
|
|
51
69
|
listingType: normalizeListingType(product),
|
|
52
70
|
});
|
|
53
71
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useCallback, useMemo } from "react";
|
|
4
4
|
import { useCategoriesFiltered } from "../hooks/useCategories";
|
|
5
5
|
import { ROUTES } from "../../../next";
|
|
6
|
-
import { Button, Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text } from "../../../ui";
|
|
6
|
+
import { Button, Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
|
|
7
7
|
import { CategoryCard } from "./CategoryGrid";
|
|
8
8
|
import { CategoryFilters } from "./CategoryFilters";
|
|
9
9
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
@@ -147,5 +147,5 @@ export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
|
|
|
147
147
|
: "border-transparent text-[var(--appkit-color-text-muted)] hover:text-zinc-800 text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text-muted)]",
|
|
148
148
|
].join(" "), children: tab.label }, tab.key))) })), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: activeTab === "brands" || brandsOnly ? "Search brands..." : "Search categories...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
|
|
149
149
|
{ label: "Featured only", active: table.get(TABLE_KEYS.IS_FEATURED) === "true", onChange: (next) => table.set(TABLE_KEYS.IS_FEATURED, next ? "true" : "") },
|
|
150
|
-
] }), totalPages > 1 && (_jsx(
|
|
150
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryGrid({ isLoading, categories, view, activeSearch, activeTab, brandsOnly }) }), renderCategoryFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, applyFilters, pendingTable })] }));
|
|
151
151
|
}
|
|
@@ -10,7 +10,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
*/
|
|
11
11
|
import { Row, Stack, sortBy } from "@mohasinac/appkit";
|
|
12
12
|
import { useMemo, useCallback, useState } from "react";
|
|
13
|
-
import { Div, Label, Span, Text, Toggle } from "../../../ui";
|
|
13
|
+
import { Div, Label, Span, Text, Toggle, StickyToolbar } from "../../../ui";
|
|
14
14
|
import { ListingToolbar, Pagination, FilterDrawer } from "../../../ui";
|
|
15
15
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
16
16
|
import { BundleBuyNowCta } from "./BundleBuyNowCta";
|
|
@@ -93,5 +93,5 @@ export function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, })
|
|
|
93
93
|
// Apply → close. <FilterDrawer> below stays available for parity
|
|
94
94
|
// with every other listing page's filter affordance.
|
|
95
95
|
{ label: "Show out-of-stock", active: showOutOfStock, onChange: (next) => table.setMany({ showOutOfStock: next ? "true" : "", page: "1" }) },
|
|
96
|
-
] }), totalPages > 1 && (_jsx(
|
|
96
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: safePage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: pageItems.length === 0 ? (_jsxs(Text, { paddingY: "4xl", color: "muted", size: "sm", align: "start", children: ["No bundles match your search", brandName ? ` for ${brandName}` : "", "."] })) : (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: pageItems.map((bundle) => (_jsxs(Stack, { children: [_jsx(MarketplaceBundleCard, { bundle: bundle }), onBuyNow && (_jsx(Div, { border: "default", surface: "default", className: "-mt-px border-t border-t-0 border-[var(--appkit-color-border)] pt-[0.5rem] pb-[0.75rem]", padding: "x-sm", roundedBottom: "lg", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: bundle.bundleStockStatus === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }, bundle.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: () => setPendingShowOutOfStock(false), activeCount: filterActiveCount, hideTrigger: true, children: _jsx(Stack, { className: `${__P.p4}`, gap: "md", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "justify-[space-between] cursor-pointer", children: [_jsx(Span, { size: "sm", weight: "medium", color: "muted", children: "Show out-of-stock bundles" }), _jsx(Toggle, { checked: pendingShowOutOfStock, onChange: setPendingShowOutOfStock, size: "sm" })] }) }) })] }));
|
|
97
97
|
}
|
|
@@ -112,7 +112,9 @@ export async function CategoryDetailPageView({ slug }) {
|
|
|
112
112
|
const storeCount = categoryStores.length;
|
|
113
113
|
const coverImage = category?.display?.coverImage;
|
|
114
114
|
const hasCover = Boolean(coverImage);
|
|
115
|
-
return (_jsxs(Main, { children: [_jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORIES), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Categories" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: category?.name ?? slug })] }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), childCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: "max-w-7xl mx-auto", padding: "inline", children: _jsx(Div, { layout: "flex", gap: "2", className: `.5 ${__O.xAuto} [scrollbar-width:none]`, padding: "b-2xs", children: childCategories.map((child) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(child.slug)),
|
|
115
|
+
return (_jsxs(Main, { children: [_jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORIES), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 transition-colors", children: "Categories" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: category?.name ?? slug })] }), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), childCategories.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: "max-w-7xl mx-auto", padding: "inline", children: _jsx(Div, { layout: "flex", gap: "2", className: `.5 ${__O.xAuto} [scrollbar-width:none]`, padding: "b-2xs", children: childCategories.map((child) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(child.slug)),
|
|
116
|
+
// audit-responsive-wrap-ok: deliberate horizontal-scroll chip row (parent has overflow-x-auto) — each chip staying single-line is the intended design, not hidden content
|
|
117
|
+
className: "flex-shrink-0 flex items-center gap-[var(--appkit-space-1-5)] rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-elevated)] px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:border-primary hover:text-primary transition-colors whitespace-nowrap", children: [child.display?.icon && (_jsx(Span, { className: "leading-none", children: child.display.icon })), child.name, (child.metrics?.productCount ?? 0) > 0 && (_jsx(Span, { size: "xs", color: "muted", children: (child.metrics?.productCount ?? 0).toLocaleString() }))] }, child.id))) }) }) })), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(CategoryDetailTabs, { categorySlug: slug, categoryId: category?.id, initialProductsData: productsResult ?? undefined, initialBundles: bundlesResult ?? [], initialStores: categoryStores, counts: {
|
|
116
118
|
products: productCount,
|
|
117
119
|
auctions: auctionCount,
|
|
118
120
|
preOrders: preOrderCount,
|
|
@@ -4,7 +4,7 @@ import { useState, useCallback } from "react";
|
|
|
4
4
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
5
|
import { usePendingTable } from "../../../react/hooks/usePendingTable";
|
|
6
6
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
7
|
-
import { Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast } from "../../../ui";
|
|
7
|
+
import { Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, useToast, StickyToolbar } from "../../../ui";
|
|
8
8
|
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
9
9
|
import { ACTION_ID } from "../../products/constants/action-defs";
|
|
10
10
|
import { ROUTES } from "../../../next";
|
|
@@ -103,7 +103,7 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
|
|
|
103
103
|
}, [localCart, showToast]);
|
|
104
104
|
return (_jsxs(Div, { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: `Search in ${categorySlug.replace(/-/g, " ")}...`, onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => {
|
|
105
105
|
table.set("sort", v);
|
|
106
|
-
}, view: view === "card" ? "grid" : "list", onViewChange: (v) => handleViewToggle(v === "grid" ? "card" : "list"), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(
|
|
106
|
+
}, view: view === "card" ? "grid" : "list", onViewChange: (v) => handleViewToggle(v === "grid" ? "card" : "list"), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: renderCategoryProductGrid({
|
|
107
107
|
isLoading, products: products, view,
|
|
108
108
|
getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)),
|
|
109
109
|
onWishlistToggle: handleWishlistToggle, wishlistedIds, onAddToCart: handleAddToCart,
|
|
@@ -82,6 +82,10 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
82
82
|
name: string;
|
|
83
83
|
slug: string;
|
|
84
84
|
description?: string;
|
|
85
|
+
/** Tester sandbox flag — set on shared admin-seeded test categories/brands/bundles; swept by testerSandboxCleanup. */
|
|
86
|
+
isTestData?: boolean;
|
|
87
|
+
/** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
|
|
88
|
+
testDataExpiresAt?: Date;
|
|
85
89
|
rootId: string;
|
|
86
90
|
parentIds: string[];
|
|
87
91
|
childrenIds: string[];
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useCallback } from "react";
|
|
4
4
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
5
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
6
|
-
import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text } from "../../../ui";
|
|
6
|
+
import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
|
|
7
7
|
import { usePendingTable } from "../../../react/hooks/usePendingTable";
|
|
8
8
|
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
9
9
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
@@ -119,5 +119,5 @@ export function ClassifiedIndexListing({ initialData }) {
|
|
|
119
119
|
// → close for every toggle. The full facet set (category/city/price/
|
|
120
120
|
// negotiable) remains in <FilterDrawer> below.
|
|
121
121
|
{ label: "Ships nationwide", active: table.get(TABLE_KEYS.ACCEPTS_SHIPPING) === "true", onChange: (next) => table.set(TABLE_KEYS.ACCEPTS_SHIPPING, next ? "true" : "") },
|
|
122
|
-
] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(
|
|
122
|
+
] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No classified listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.CLASSIFIED_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(ClassifiedFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
123
123
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useCallback } from "react";
|
|
4
4
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
5
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
6
|
-
import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast } from "../../../ui";
|
|
6
|
+
import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast, StickyToolbar } from "../../../ui";
|
|
7
7
|
import { usePendingTable } from "../../../react/hooks/usePendingTable";
|
|
8
8
|
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
9
9
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
@@ -110,5 +110,5 @@ export function DigitalCodesIndexListing({ initialData }) {
|
|
|
110
110
|
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] } } : {});
|
|
111
111
|
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search digital codes\u2026", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: (e) => { if (e.key === "Enter")
|
|
112
112
|
commitSearch(); }, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: (v) => { if (v !== "table")
|
|
113
|
-
setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(
|
|
113
|
+
setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No digital code listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.DIGITAL_CODE_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(DigitalCodeFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
114
114
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Brand colours flow through the active light-theme palette
|
|
16
16
|
* (`var(--appkit-color-*)`). Because email clients don't resolve CSS
|
|
17
|
-
* variables, we hard-code the
|
|
17
|
+
* variables, we hard-code the teal + magenta brand defaults inline; admin
|
|
18
18
|
* themes do not affect email rendering.
|
|
19
19
|
*/
|
|
20
20
|
import type { ReactNode } from "react";
|
|
@@ -39,7 +39,7 @@ export declare function EmailContainer({ width, background, children, }: EmailCo
|
|
|
39
39
|
export interface EmailHeaderProps {
|
|
40
40
|
/** Brand name shown in the header. */
|
|
41
41
|
brandName: string;
|
|
42
|
-
/** Optional brand colour override (defaults to the
|
|
42
|
+
/** Optional brand colour override (defaults to the teal primary). */
|
|
43
43
|
background?: string;
|
|
44
44
|
children?: ReactNode;
|
|
45
45
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useCallback, useMemo } from "react";
|
|
4
4
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
5
5
|
import { useEvents } from "../hooks/useEvents";
|
|
6
|
-
import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text } from "../../../ui";
|
|
6
|
+
import { Div, ListingFilterDrawer, ListingToolbar, Pagination, Row, Stack, Text, StickyToolbar } from "../../../ui";
|
|
7
7
|
import { EventCard } from "./EventCard";
|
|
8
8
|
import { EventFilters, EVENT_PUBLIC_SORT_OPTIONS } from "./EventFilters";
|
|
9
9
|
import { EVENT_FIELDS } from "../../../constants/field-names";
|
|
@@ -123,5 +123,5 @@ export function EventsIndexListing({ initialData }) {
|
|
|
123
123
|
}, [searchInput, table]);
|
|
124
124
|
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get(TABLE_KEYS.SORT) || sortBy(EVENT_FIELDS.STARTS_AT, "ASC"), sortOptions: EVENT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
|
|
125
125
|
{ label: "Show expired", active: showExpired, onChange: (next) => table.set(TABLE_KEYS.SHOW_EXPIRED, next ? "true" : "") },
|
|
126
|
-
] }), totalPages > 1 && (_jsx(
|
|
126
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-2/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8 mt-2", surface: "subtle", rounded: "default" })] })] }, i))) })) : events.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No events found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(EventFilters, { table: pendingTable, variant: "public" }) })] }));
|
|
127
127
|
}
|
|
@@ -14,6 +14,10 @@ export interface EventDocument extends BaseDocument {
|
|
|
14
14
|
title: string;
|
|
15
15
|
description: string;
|
|
16
16
|
status: EventStatus;
|
|
17
|
+
/** Tester sandbox flag — set on the shared admin-seeded test event; swept by testerSandboxCleanup. */
|
|
18
|
+
isTestData?: boolean;
|
|
19
|
+
/** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
|
|
20
|
+
testDataExpiresAt?: Date;
|
|
17
21
|
startsAt: Date;
|
|
18
22
|
endsAt: Date;
|
|
19
23
|
coverImage?: MediaField | null;
|
|
@@ -6,7 +6,12 @@ export interface BackToTopProps {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* BackToTop — floating scroll-to-top button.
|
|
9
|
-
* Appears after the page has scrolled past `threshold` pixels.
|
|
9
|
+
* Appears after the page has scrolled past `threshold` pixels. A small
|
|
10
|
+
* dismiss control lets the user hide it for the rest of the browser
|
|
11
|
+
* session (sessionStorage) — on narrow viewports it can sit close enough
|
|
12
|
+
* to the bottom nav / other floating controls to feel like clutter, so it
|
|
13
|
+
* needs the same "get out of my way" escape hatch as StickyToolbar.
|
|
14
|
+
* Re-appears on the next page load.
|
|
10
15
|
*/
|
|
11
16
|
export declare function BackToTop({ threshold, ariaLabel, className, }: BackToTopProps): import("react").JSX.Element | null;
|
|
12
17
|
/** SkipToMain — visually hidden link that becomes visible on focus for keyboard users. */
|
|
@@ -1,23 +1,48 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
|
-
import { ChevronUp } from "lucide-react";
|
|
6
|
-
import { Button } from "../../ui";
|
|
5
|
+
import { ChevronUp, X } from "lucide-react";
|
|
6
|
+
import { Button, Div } from "../../ui";
|
|
7
|
+
import { normalizeError } from "../../errors/normalize";
|
|
8
|
+
const DISMISSED_KEY = "appkit:back-to-top-dismissed";
|
|
7
9
|
/**
|
|
8
10
|
* BackToTop — floating scroll-to-top button.
|
|
9
|
-
* Appears after the page has scrolled past `threshold` pixels.
|
|
11
|
+
* Appears after the page has scrolled past `threshold` pixels. A small
|
|
12
|
+
* dismiss control lets the user hide it for the rest of the browser
|
|
13
|
+
* session (sessionStorage) — on narrow viewports it can sit close enough
|
|
14
|
+
* to the bottom nav / other floating controls to feel like clutter, so it
|
|
15
|
+
* needs the same "get out of my way" escape hatch as StickyToolbar.
|
|
16
|
+
* Re-appears on the next page load.
|
|
10
17
|
*/
|
|
11
18
|
export function BackToTop({ threshold = 400, ariaLabel = "Back to top", className = "", }) {
|
|
12
19
|
const [visible, setVisible] = useState(false);
|
|
20
|
+
const [dismissed, setDismissed] = useState(false);
|
|
13
21
|
useEffect(() => {
|
|
22
|
+
try {
|
|
23
|
+
setDismissed(window.sessionStorage.getItem(DISMISSED_KEY) === "1");
|
|
24
|
+
}
|
|
25
|
+
catch (_err) {
|
|
26
|
+
void normalizeError(_err);
|
|
27
|
+
// sessionStorage unavailable — never dismissed for this render.
|
|
28
|
+
}
|
|
14
29
|
const handleScroll = () => setVisible(window.scrollY > threshold);
|
|
15
30
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
16
31
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
17
32
|
}, [threshold]);
|
|
18
|
-
if (!visible)
|
|
33
|
+
if (!visible || dismissed)
|
|
19
34
|
return null;
|
|
20
|
-
|
|
35
|
+
const handleDismiss = () => {
|
|
36
|
+
setDismissed(true);
|
|
37
|
+
try {
|
|
38
|
+
window.sessionStorage.setItem(DISMISSED_KEY, "1");
|
|
39
|
+
}
|
|
40
|
+
catch (_err) {
|
|
41
|
+
void normalizeError(_err);
|
|
42
|
+
// sessionStorage unavailable — dismissal still applies for this render.
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return (_jsxs(Div, { layout: "flex", align: "center", gap: "1", className: `fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] right-4 z-[var(--appkit-z-bottom-nav)] ${className}`, children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": ariaLabel, onClick: () => window.scrollTo({ top: 0, behavior: "smooth" }), className: "flex items-center justify-center w-10 h-10 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] dark:hover:bg-slate-700 transition-colors", children: _jsx(ChevronUp, { className: "w-5 h-5", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": "Hide back-to-top button", onClick: handleDismiss, className: "flex items-center justify-center w-6 h-6 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text)] transition-colors", children: _jsx(X, { className: "w-3 h-3", "aria-hidden": "true" }) })] }));
|
|
21
46
|
}
|
|
22
47
|
/** SkipToMain — visually hidden link that becomes visible on focus for keyboard users. */
|
|
23
48
|
export function SkipToMain({ mainId = "main-content" }) {
|