@mohasinac/appkit 4.47.0 → 4.48.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/.build-stamp.json +4 -4
- package/dist/_internal/server/features/checkout/expand-order-items.js +3 -0
- package/dist/_internal/server/features/messages/actions.d.ts +68 -0
- package/dist/_internal/server/features/messages/actions.js +249 -0
- package/dist/_internal/server/features/parts/buildable.d.ts +47 -0
- package/dist/_internal/server/features/parts/buildable.js +159 -0
- package/dist/_internal/server/features/products/list-public.d.ts +17 -0
- package/dist/_internal/server/features/products/list-public.js +92 -5
- package/dist/_internal/server/features/products/og.d.ts +7 -0
- package/dist/_internal/server/features/products/og.js +38 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +6 -0
- package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +28 -0
- package/dist/_internal/server/jobs/core/notificationPrune.js +87 -3
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +36 -1
- package/dist/_internal/shared/actions/action-registry.js +8 -0
- package/dist/_internal/shared/features/notifications/action-url.js +8 -0
- package/dist/_internal/shared/features/notifications/email-eligibility.js +3 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +4 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +8 -0
- package/dist/constants/api-endpoints.d.ts +48 -0
- package/dist/constants/api-endpoints.js +30 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/features/about/components/GstExplainerView.d.ts +28 -0
- package/dist/features/about/components/GstExplainerView.js +59 -0
- package/dist/features/about/components/GuidesHubView.d.ts +28 -0
- package/dist/features/about/components/GuidesHubView.js +58 -0
- package/dist/features/about/components/PublicProfileView.js +5 -0
- package/dist/features/about/components/SpotAFakeView.d.ts +29 -0
- package/dist/features/about/components/SpotAFakeView.js +49 -0
- package/dist/features/admin/actions/notification-actions.js +4 -0
- package/dist/features/admin/schemas/firestore.d.ts +13 -3
- package/dist/features/admin/schemas/firestore.js +8 -0
- package/dist/features/blog/repository/blog.repository.js +21 -7
- package/dist/features/categories/api/route.js +41 -1
- package/dist/features/categories/components/BreadcrumbTrail.d.ts +39 -17
- package/dist/features/categories/components/BreadcrumbTrail.js +46 -50
- package/dist/features/categories/components/CategoryDetailPageView.js +92 -38
- package/dist/features/categories/components/ContextualTrail.d.ts +62 -0
- package/dist/features/categories/components/ContextualTrail.js +81 -0
- package/dist/features/categories/components/LastCategoryBeacon.d.ts +18 -0
- package/dist/features/categories/components/LastCategoryBeacon.js +33 -0
- package/dist/features/categories/repository/categories.repository.d.ts +21 -0
- package/dist/features/categories/repository/categories.repository.js +36 -3
- package/dist/features/categories/repository/price-index.repository.d.ts +25 -0
- package/dist/features/email/notification-templates.js +4 -0
- package/dist/features/events/repository/events.repository.js +10 -3
- package/dist/features/faq/constants/read-caps.d.ts +29 -0
- package/dist/features/faq/constants/read-caps.js +29 -0
- package/dist/features/faq/repository/faqs.repository.d.ts +0 -2
- package/dist/features/faq/repository/faqs.repository.js +19 -16
- package/dist/features/homepage/components/AdSlot.js +10 -4
- package/dist/features/homepage/hooks/useActiveAd.d.ts +18 -1
- package/dist/features/homepage/hooks/useActiveAd.js +42 -2
- package/dist/features/homepage/lib/section-renderer.js +12 -2
- package/dist/features/layout/AppLayoutShell.d.ts +4 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +4 -1
- package/dist/features/layout/TitleBarLayout.js +8 -4
- package/dist/features/media/AvatarUpload.js +8 -1
- package/dist/features/media/MediaAvatar.js +1 -1
- package/dist/features/media/MediaImage.js +3 -1
- package/dist/features/media/MediaLightbox.js +4 -1
- package/dist/features/media/MediaPickerModal.js +6 -1
- package/dist/features/media/MediaVideo.js +3 -1
- package/dist/features/messages/components/ConversationThreadView.d.ts +3 -0
- package/dist/features/messages/components/ConversationThreadView.js +75 -0
- package/dist/features/messages/components/ConversationsInboxView.d.ts +1 -0
- package/dist/features/messages/components/ConversationsInboxView.js +39 -0
- package/dist/features/messages/components/MessageSellerButton.d.ts +17 -0
- package/dist/features/messages/components/MessageSellerButton.js +33 -0
- package/dist/features/messages/hooks/useConversationChannel.d.ts +24 -0
- package/dist/features/messages/hooks/useConversationChannel.js +89 -0
- package/dist/features/messages/hooks/useMessages.d.ts +347 -0
- package/dist/features/messages/hooks/useMessages.js +113 -0
- package/dist/features/messages/repository/conversations.repository.d.ts +44 -0
- package/dist/features/messages/repository/conversations.repository.js +173 -0
- package/dist/features/messages/repository/messages.repository.d.ts +42 -0
- package/dist/features/messages/repository/messages.repository.js +142 -0
- package/dist/features/messages/schemas/firestore.d.ts +99 -0
- package/dist/features/messages/schemas/firestore.js +46 -0
- package/dist/features/orders/repository/orders.repository.js +21 -7
- package/dist/features/orders/schemas/firestore.d.ts +11 -0
- package/dist/features/products/components/CityCollectiblesView.d.ts +31 -0
- package/dist/features/products/components/CityCollectiblesView.js +82 -0
- package/dist/features/products/components/MarketPriceAnchor.d.ts +45 -0
- package/dist/features/products/components/MarketPriceAnchor.js +62 -0
- package/dist/features/products/components/ProductDetailPageView.js +71 -5
- package/dist/features/products/constants/meetup-cities.d.ts +47 -0
- package/dist/features/products/constants/meetup-cities.js +49 -0
- package/dist/features/products/repository/category-assignment.d.ts +82 -11
- package/dist/features/products/repository/category-assignment.js +93 -12
- package/dist/features/products/repository/products.repository.d.ts +12 -1
- package/dist/features/products/repository/products.repository.js +66 -34
- package/dist/features/products/schemas/firestore.d.ts +24 -2
- package/dist/features/products/schemas/firestore.js +13 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/promotions/repository/coupons.repository.js +11 -4
- package/dist/features/reviews/repository/reviews.repository.js +11 -4
- package/dist/features/scams/repository/scammer.repository.js +21 -7
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
- package/dist/features/seller/components/SellerProductShell.js +2 -2
- package/dist/features/seller/repository/offer.repository.js +21 -6
- package/dist/features/seller/schemas/product-form.d.ts +3 -3
- package/dist/features/seller/schemas/product-form.js +1 -1
- package/dist/features/server-errors/repository/server-errors.repository.js +11 -0
- package/dist/features/server-errors/schemas/firestore.d.ts +17 -0
- package/dist/features/server-errors/schemas/firestore.js +2 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +17 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +31 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +1 -1
- package/dist/features/store-extensions/schemas/firestore.js +4 -0
- package/dist/features/stores/repository/store.repository.js +11 -3
- package/dist/features/tester/seed-data/authored/community-support__messages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/community-support__messages.js +200 -0
- package/dist/features/tester/seed-data/authored/index.js +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +64 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +1 -1
- package/dist/next/routing/route-map.d.ts +14 -0
- package/dist/next/routing/route-map.js +26 -0
- package/dist/providers/db-firebase/rtdb-paths.d.ts +8 -0
- package/dist/providers/db-firebase/rtdb-paths.js +8 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +4 -0
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +17 -0
- package/dist/security/pii-schemas.js +15 -7
- package/dist/seed/_helpers/category-content-generated.d.ts +3 -3
- package/dist/seed/_helpers/category-content-generated.js +23 -7
- package/dist/seed/_helpers/category-models.d.ts +1 -1
- package/dist/seed/_helpers/category-models.js +25 -1
- package/dist/seed/_helpers/category-names.d.ts +54 -0
- package/dist/seed/_helpers/category-names.js +118 -0
- package/dist/seed/_helpers/character-features.d.ts +1 -1
- package/dist/seed/_helpers/character-features.js +5 -4
- package/dist/seed/_helpers/final-sale-fixtures.js +1 -1
- package/dist/seed/categories-seed-data.js +55 -1
- package/dist/seed/grouped-listings-seed-data.js +2 -2
- package/dist/seed/products-art-seed-data.js +2 -1
- package/dist/seed/products-auctions-seed-data.js +42 -22
- package/dist/seed/products-classifieds-seed-data.js +21 -3
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.js +26 -11
- package/dist/seed/products-prize-draws-seed-data.js +8 -1
- package/dist/seed/products-standard-seed-data.js +26 -25
- package/dist/seed/products-stickers-seed-data.js +12 -11
- package/dist/server.d.ts +8 -0
- package/dist/server.js +15 -0
- package/dist/styles.css +113 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/sunset.js +17 -0
- package/dist/tokens/tokens.css +5 -0
- package/dist/ui/components/Breadcrumb.d.ts +8 -1
- package/dist/ui/components/Breadcrumb.js +2 -2
- package/dist/ui/components/Breadcrumb.style.css +37 -0
- package/dist/ui/components/Details.d.ts +11 -1
- package/dist/ui/components/Details.js +2 -2
- package/dist/ui/components/ImageMosaic.d.ts +43 -0
- package/dist/ui/components/ImageMosaic.js +46 -0
- package/dist/ui/components/ImageMosaic.style.css +69 -0
- package/dist/ui/components/SplitTile.d.ts +34 -0
- package/dist/ui/components/SplitTile.js +28 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +4 -0
- package/dist/utils/search-txt-query.d.ts +77 -0
- package/dist/utils/search-txt-query.js +50 -28
- package/package.json +1 -1
|
@@ -41,6 +41,7 @@ import { PRODUCT_FIELDS } from "../../../../constants/field-names.js";
|
|
|
41
41
|
import { TABLE_KEYS } from "../../../../constants/table-keys.js";
|
|
42
42
|
import { sortBy } from "../../../../constants/sort.js";
|
|
43
43
|
import { SIEVE_OP, sieveAnd, sieveFilter, expandSieveParam, } from "../../../../utils/sieve-builder.js";
|
|
44
|
+
import { planSearchTxt } from "../../../../utils/search-txt-query.js";
|
|
44
45
|
import { ALL_LISTING_TYPES } from "../../../shared/listing-types/feature-flags.js";
|
|
45
46
|
import { isListingRowAvailable, unavailableClausesFor, } from "../../../shared/listing-types/_registry.js";
|
|
46
47
|
import { AVAILABILITY_VALUES, isAvailabilityFilter, } from "../../../../constants/field-names.js";
|
|
@@ -241,7 +242,15 @@ export function parsePublicProductParams(params, defaults) {
|
|
|
241
242
|
* index in an order nobody declares — it fails with FAILED_PRECONDITION, which
|
|
242
243
|
* upstream reads as a bare "no results".
|
|
243
244
|
*/
|
|
244
|
-
|
|
245
|
+
/**
|
|
246
|
+
* The Firestore-safe Sieve filter string.
|
|
247
|
+
*
|
|
248
|
+
* `omitCategory` exists for the one query Firestore cannot express as written:
|
|
249
|
+
* a search term inside a category. Both are array operators and Firestore
|
|
250
|
+
* permits one, so the category leaves `filters` and travels as `categoriesIn`,
|
|
251
|
+
* where the repository refines it in memory over the search window.
|
|
252
|
+
*/
|
|
253
|
+
function buildFirestoreSafeFilters(input, opts) {
|
|
245
254
|
const parts = [];
|
|
246
255
|
// Published-only unless the caller explicitly asks otherwise. Without this
|
|
247
256
|
// default, any refetch that omits `status` leaks drafts (Root Cause #30).
|
|
@@ -253,7 +262,7 @@ function buildFirestoreSafeFilters(input) {
|
|
|
253
262
|
if (types.length > 0) {
|
|
254
263
|
parts.push(sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, types.join("|")));
|
|
255
264
|
}
|
|
256
|
-
if (input.category) {
|
|
265
|
+
if (input.category && !opts?.omitCategory) {
|
|
257
266
|
parts.push(expandSieveParam(PRODUCT_FIELDS.CATEGORY_SLUGS, input.category, SIEVE_OP.CONTAINS));
|
|
258
267
|
}
|
|
259
268
|
if (input.brand)
|
|
@@ -373,7 +382,15 @@ async function defaultExecutor(query) {
|
|
|
373
382
|
// Without this second argument the repository's searchTxt read path is
|
|
374
383
|
// unreachable on the default executor, which is what every SSR listing view
|
|
375
384
|
// uses. It was implemented and simply never called.
|
|
376
|
-
|
|
385
|
+
//
|
|
386
|
+
// `categoriesIn` rides along for the one query Firestore cannot express as
|
|
387
|
+
// two array clauses — a search inside a category. See `PublicProductQuery`.
|
|
388
|
+
query.search || query.categoriesIn?.length
|
|
389
|
+
? {
|
|
390
|
+
...(query.search ? { search: query.search } : {}),
|
|
391
|
+
...(query.categoriesIn?.length ? { categoriesIn: [...query.categoriesIn] } : {}),
|
|
392
|
+
}
|
|
393
|
+
: undefined);
|
|
377
394
|
return {
|
|
378
395
|
items: result.items,
|
|
379
396
|
total: result.total,
|
|
@@ -415,6 +432,38 @@ export async function listPublicProducts(input, opts) {
|
|
|
415
432
|
const sorts = input.sorts || DEFAULT_SORTS;
|
|
416
433
|
const requestedSortField = sorts.replace(/^-/, "");
|
|
417
434
|
const sortDesc = sorts.startsWith("-");
|
|
435
|
+
/*
|
|
436
|
+
* 🛑 THE EMPTY GUARD, HERE RATHER THAN ONLY IN THE REPOSITORY — measured
|
|
437
|
+
* live: `/api/products?q=!!!` returned the WHOLE CATALOGUE (50+, capped),
|
|
438
|
+
* which is A2's bug still reachable in production.
|
|
439
|
+
*
|
|
440
|
+
* `productRepository.list` has carried this guard for a while, but this
|
|
441
|
+
* function prefers the colocated `listingProcessor` Function on the one
|
|
442
|
+
* route that serves `/api/products`, and that Function has no such guard: it
|
|
443
|
+
* is handed a filter string with no search clause in it and answers with an
|
|
444
|
+
* unfiltered page. So the rule has to live where BOTH executors pass, which
|
|
445
|
+
* is here.
|
|
446
|
+
*
|
|
447
|
+
* `parseSearchTxtQuery` normalises punctuation away, so `"!!!"`, `"—"` and
|
|
448
|
+
* `"@#$"` all reduce to no usable token — and a query that produced none
|
|
449
|
+
* must return NOTHING. Falling through to the other filters is what turns a
|
|
450
|
+
* one-character search into "here is everything".
|
|
451
|
+
*/
|
|
452
|
+
if (planSearchTxt(input.q).empty) {
|
|
453
|
+
return {
|
|
454
|
+
items: [],
|
|
455
|
+
total: 0,
|
|
456
|
+
page: 1,
|
|
457
|
+
pageSize: 0,
|
|
458
|
+
totalPages: 0,
|
|
459
|
+
hasMore: false,
|
|
460
|
+
cursor: null,
|
|
461
|
+
// Not truncated — this is an exact zero, not a saturated window.
|
|
462
|
+
truncated: false,
|
|
463
|
+
filters: "",
|
|
464
|
+
sorts: input.sorts || DEFAULT_SORTS,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
418
467
|
const availability = input.availability ?? AVAILABILITY_VALUES.ALL;
|
|
419
468
|
const wantAvailable = availability === AVAILABILITY_VALUES.AVAILABLE;
|
|
420
469
|
const wantUnavailable = availability === AVAILABILITY_VALUES.UNAVAILABLE;
|
|
@@ -423,7 +472,43 @@ export async function listPublicProducts(input, opts) {
|
|
|
423
472
|
// Firestore DOES accept one inequality when the query sorts by that same
|
|
424
473
|
// field, so a matching date facet can still be pushed down.
|
|
425
474
|
const canPushDate = hasDateRange && !wantAvailable && requestedSortField === dateField;
|
|
426
|
-
|
|
475
|
+
/*
|
|
476
|
+
* 🛑 A SEARCH INSIDE A CATEGORY CANNOT RUN ON THE REMOTE EXECUTOR, and
|
|
477
|
+
* measured against the live container it returned **zero rows**, not a wrong
|
|
478
|
+
* set — which reads as "this category has nothing matching" and is the most
|
|
479
|
+
* convincing way to be wrong.
|
|
480
|
+
*
|
|
481
|
+
* The reason is Firestore's one-array-operator rule meeting two array
|
|
482
|
+
* clauses: the category travels inside `filters` as `categorySlugs @= …`
|
|
483
|
+
* (array-contains) and the search term becomes a second `array-contains` on
|
|
484
|
+
* `searchTxt` inside the repository. One of the two has to be refined in
|
|
485
|
+
* memory, and the colocated Function CANNOT do that — it is handed a filter
|
|
486
|
+
* string and returns a page.
|
|
487
|
+
*
|
|
488
|
+
* So when both are present this query runs on the DEFAULT executor, which
|
|
489
|
+
* holds the repository's refine (`beginSearchWindow` + the `categoriesIn`
|
|
490
|
+
* predicate). Measured live before the fix: `?q=dragoon` → 30,
|
|
491
|
+
* `?category=category-spinning-tops` → 50+, and the two together → 0.
|
|
492
|
+
*
|
|
493
|
+
* 🛑 The alternative — moving the category OUT of `filters` so the Function
|
|
494
|
+
* only sees the search — is strictly worse: the Function would then return
|
|
495
|
+
* the UNSCOPED catalogue and the page would silently widen instead of
|
|
496
|
+
* silently emptying. Same family as Root Cause #85: one handler reachable
|
|
497
|
+
* through two paths with different capabilities, where an env var decides
|
|
498
|
+
* which one answers.
|
|
499
|
+
*/
|
|
500
|
+
const needsMemoryRefine = Boolean(input.q && input.category);
|
|
501
|
+
const executor = needsMemoryRefine ? defaultExecutor : (opts?.executor ?? defaultExecutor);
|
|
502
|
+
/*
|
|
503
|
+
* The category ids, as the repository's `categoriesIn` option rather than as
|
|
504
|
+
* a Sieve clause. Products carry their FULL ancestor chain, so a page passing
|
|
505
|
+
* its own id still matches its whole subtree — the refine and the pushdown
|
|
506
|
+
* agree by construction.
|
|
507
|
+
*/
|
|
508
|
+
const categoriesIn = needsMemoryRefine
|
|
509
|
+
? input.category.split("|").filter(Boolean)
|
|
510
|
+
: undefined;
|
|
511
|
+
const safeFilters = buildFirestoreSafeFilters(input, { omitCategory: needsMemoryRefine });
|
|
427
512
|
const dateClauses = [
|
|
428
513
|
...(canPushDate && input.dateFrom
|
|
429
514
|
? [sieveFilter(dateField, SIEVE_OP.GTE, input.dateFrom)]
|
|
@@ -432,7 +517,6 @@ export async function listPublicProducts(input, opts) {
|
|
|
432
517
|
? [sieveFilter(dateField, SIEVE_OP.LTE, input.dateTo)]
|
|
433
518
|
: []),
|
|
434
519
|
];
|
|
435
|
-
const executor = opts?.executor ?? defaultExecutor;
|
|
436
520
|
const types = asListingTypes(input.listingTypes);
|
|
437
521
|
// ── Path B: one type whose unavailability IS a pushable inequality, and the
|
|
438
522
|
// caller is already sorting by that field. Ended auctions sorted by end
|
|
@@ -465,6 +549,7 @@ export async function listPublicProducts(input, opts) {
|
|
|
465
549
|
pageSize,
|
|
466
550
|
cursor: input.cursor ?? null,
|
|
467
551
|
search: input.q,
|
|
552
|
+
categoriesIn,
|
|
468
553
|
});
|
|
469
554
|
if (!result)
|
|
470
555
|
return null;
|
|
@@ -502,6 +587,7 @@ export async function listPublicProducts(input, opts) {
|
|
|
502
587
|
pageSize: PUBLIC_PRODUCT_MAX_PAGE_SIZE,
|
|
503
588
|
cursor: null,
|
|
504
589
|
search: input.q,
|
|
590
|
+
categoriesIn,
|
|
505
591
|
});
|
|
506
592
|
}));
|
|
507
593
|
// One failed branch must not blank the whole scope — but it is already
|
|
@@ -617,6 +703,7 @@ export async function listPublicProducts(input, opts) {
|
|
|
617
703
|
pageSize: inMemory ? windowSize : pageSize,
|
|
618
704
|
cursor: inMemory ? null : (input.cursor ?? null),
|
|
619
705
|
search: input.q,
|
|
706
|
+
categoriesIn,
|
|
620
707
|
});
|
|
621
708
|
if (!result)
|
|
622
709
|
return null;
|
|
@@ -18,4 +18,11 @@ export declare function renderProductOg(doc: ProductDocLike | null | undefined,
|
|
|
18
18
|
baseUrl?: string;
|
|
19
19
|
}): ReactElement;
|
|
20
20
|
export declare function renderProductOgImage(data: ProductOgData, siteName: string): ReactElement;
|
|
21
|
+
export interface CityOgData {
|
|
22
|
+
city: string;
|
|
23
|
+
state: string;
|
|
24
|
+
/** Exact listing count, or `null` when the read saturated its cap. */
|
|
25
|
+
listingCount: number | null;
|
|
26
|
+
}
|
|
27
|
+
export declare function renderCityOg(data: CityOgData, siteName: string): ReactElement;
|
|
21
28
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
2
|
import { renderOgLayout } from "../seo/og-layout.js";
|
|
2
3
|
import { resolveOgImageUrl } from "../seo/og.js";
|
|
3
4
|
/** High-level OG renderer — accepts the raw product document from `getProductForDetail`. */
|
|
@@ -20,3 +21,40 @@ export function renderProductOgImage(data, siteName) {
|
|
|
20
21
|
accentSlot: null,
|
|
21
22
|
});
|
|
22
23
|
}
|
|
24
|
+
export function renderCityOg(data, siteName) {
|
|
25
|
+
const { city, state, listingCount } = data;
|
|
26
|
+
const sub = listingCount === null
|
|
27
|
+
? "Classified listings — meet the seller, inspect, pay in person"
|
|
28
|
+
: listingCount === 0
|
|
29
|
+
? "No classified listings here right now — everything on site ships nationwide"
|
|
30
|
+
: `${listingCount} classified listing${listingCount === 1 ? "" : "s"} — meet the seller, inspect, pay in person`;
|
|
31
|
+
return (_jsxs("div", { style: {
|
|
32
|
+
display: "flex",
|
|
33
|
+
width: "100%",
|
|
34
|
+
height: "100%",
|
|
35
|
+
background: "#0f172a",
|
|
36
|
+
fontFamily: "sans-serif",
|
|
37
|
+
position: "relative",
|
|
38
|
+
overflow: "hidden",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
justifyContent: "center",
|
|
41
|
+
}, children: [_jsx("div", { style: {
|
|
42
|
+
position: "absolute",
|
|
43
|
+
inset: 0,
|
|
44
|
+
background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
|
|
45
|
+
} }), _jsxs("div", { style: {
|
|
46
|
+
position: "relative",
|
|
47
|
+
display: "flex",
|
|
48
|
+
flexDirection: "column",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
gap: 24,
|
|
51
|
+
padding: "60px",
|
|
52
|
+
textAlign: "center",
|
|
53
|
+
}, children: [_jsx("div", { style: {
|
|
54
|
+
fontSize: 18,
|
|
55
|
+
color: "#94a3b8",
|
|
56
|
+
fontWeight: 500,
|
|
57
|
+
letterSpacing: 2,
|
|
58
|
+
textTransform: "uppercase",
|
|
59
|
+
}, children: siteName }), _jsx("div", { style: { fontSize: 68, fontWeight: 800, color: "#f1f5f9", lineHeight: 1.1 }, children: city }), _jsx("div", { style: { fontSize: 28, color: "#cbd5e1", fontWeight: 600 }, children: state }), _jsx("div", { style: { fontSize: 24, color: "#94a3b8", maxWidth: 820, lineHeight: 1.5 }, children: sub })] })] }));
|
|
60
|
+
}
|
|
@@ -37,7 +37,7 @@ import type { PublicSiteSettings } from "../../../shared/features/site-settings/
|
|
|
37
37
|
* was traced to a call site; anything without one belongs in the private list,
|
|
38
38
|
* because an unused public field is pure attack surface.
|
|
39
39
|
*/
|
|
40
|
-
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes"];
|
|
40
|
+
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes", "chat"];
|
|
41
41
|
/** Everything else, with the reason it stays server-side. */
|
|
42
42
|
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
|
|
43
43
|
export interface ToPublicSiteSettingsOptions {
|
|
@@ -33,6 +33,10 @@ export const PUBLIC_SITE_SETTINGS_FIELDS = [
|
|
|
33
33
|
* alternative is putting a per-request Firestore read back on the Edge.
|
|
34
34
|
*/
|
|
35
35
|
"disabledRoutes",
|
|
36
|
+
// D9 — a signed-out visitor's listing/detail view needs to know whether to
|
|
37
|
+
// render a "Message seller" CTA at all. Narrowed to `enabled` only; there is
|
|
38
|
+
// nothing else in the group.
|
|
39
|
+
"chat",
|
|
36
40
|
];
|
|
37
41
|
/** Everything else, with the reason it stays server-side. */
|
|
38
42
|
export const PRIVATE_SITE_SETTINGS_FIELDS = [
|
|
@@ -106,5 +110,7 @@ export function toPublicSiteSettings(doc, opts) {
|
|
|
106
110
|
// and leave the next person wondering whether the projection ran at all.
|
|
107
111
|
disabledRoutes: doc.disabledRoutes ?? [],
|
|
108
112
|
effectiveWatermark: opts?.effectiveWatermark,
|
|
113
|
+
// D9 — undefined reads as OFF, never a default-true `??`.
|
|
114
|
+
chat: { enabled: doc.chat?.enabled === true },
|
|
109
115
|
};
|
|
110
116
|
}
|
|
@@ -13,6 +13,9 @@ const BULK_STALE_MS = 15 * 60 * 1000;
|
|
|
13
13
|
* exist to prevent.
|
|
14
14
|
*/
|
|
15
15
|
const EMAIL_STALE_MS = 60 * 60 * 1000;
|
|
16
|
+
/** Long enough for an open thread to notice a ping before it prunes; the
|
|
17
|
+
* message itself lives in Firestore regardless — this only clears the ping. */
|
|
18
|
+
const CHAT_STALE_MS = 15 * 60 * 1000;
|
|
16
19
|
const JOBS_TTL_DAYS = 30;
|
|
17
20
|
export async function runCleanupRtdbEvents(ctx) {
|
|
18
21
|
ctx.logger.info("Starting RTDB events cleanup");
|
|
@@ -108,6 +111,31 @@ export async function runCleanupRtdbEvents(ctx) {
|
|
|
108
111
|
void normalizeError(emailErr);
|
|
109
112
|
ctx.logger.error("Email events cleanup failed (non-fatal)", emailErr);
|
|
110
113
|
}
|
|
114
|
+
/*
|
|
115
|
+
* D9 — chat new-message pings. Its own file says it: "an unpruned RTDB
|
|
116
|
+
* channel is precisely the thing that quietly accumulates… Do not make it
|
|
117
|
+
* the second" (see the email block above, which is the first). The message
|
|
118
|
+
* content never lived here — only a ping — so pruning this touches nothing
|
|
119
|
+
* a thread needs; the Firestore conversation/message documents are handled
|
|
120
|
+
* by `notificationPrune`'s retention stages, entirely separately.
|
|
121
|
+
*/
|
|
122
|
+
try {
|
|
123
|
+
const chatSnap = await rtdb.ref(RTDB_PATHS.CHAT_EVENTS).get();
|
|
124
|
+
if (chatSnap.exists()) {
|
|
125
|
+
const allChatEvents = chatSnap.val();
|
|
126
|
+
const staleChatIds = Object.entries(allChatEvents)
|
|
127
|
+
.filter(([, node]) => (node.updatedAt ?? 0) < now - CHAT_STALE_MS)
|
|
128
|
+
.map(([id]) => id);
|
|
129
|
+
if (staleChatIds.length > 0) {
|
|
130
|
+
await Promise.all(staleChatIds.map((id) => rtdb.ref(`${RTDB_PATHS.CHAT_EVENTS}/${id}`).remove()));
|
|
131
|
+
ctx.logger.info("Chat event pings removed", { count: staleChatIds.length });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (chatErr) {
|
|
136
|
+
void normalizeError(chatErr);
|
|
137
|
+
ctx.logger.error("Chat events cleanup failed (non-fatal)", chatErr);
|
|
138
|
+
}
|
|
111
139
|
try {
|
|
112
140
|
const staleJobRefs = await jobsRepository.getStaleFinishedRefs(JOBS_TTL_DAYS);
|
|
113
141
|
if (staleJobRefs.length > 0) {
|
|
@@ -1,13 +1,97 @@
|
|
|
1
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
1
2
|
import { notificationRepository } from "../../../../repositories/index.js";
|
|
3
|
+
import { conversationsRepository } from "../../../../features/messages/repository/conversations.repository.js";
|
|
4
|
+
import { CONVERSATION_ACTIVE_DAYS, CONVERSATION_ARCHIVE_DAYS, } from "../../../../features/messages/schemas/firestore.js";
|
|
5
|
+
import { deleteAllMessages, listAllAttachmentUrls, } from "../../../../features/messages/repository/messages.repository.js";
|
|
6
|
+
import { mediaAssetsRepository } from "../../../../features/media/repository/media-assets.repository.js";
|
|
7
|
+
import { getAdminStorageLite } from "../../../../providers/db-firebase/admin-storage-lite.js";
|
|
2
8
|
import { NOTIFICATION_TTL_DAYS } from "../handlers/messages.js";
|
|
3
9
|
import { batchDelete } from "../handlers/_helpers.js";
|
|
10
|
+
const RETENTION_BATCH_LIMIT = 100;
|
|
11
|
+
const PROXY_URL_PREFIX = "/media/";
|
|
12
|
+
/**
|
|
13
|
+
* Delete a chat attachment's STORAGE OBJECT, then its `mediaAssets` doc.
|
|
14
|
+
*
|
|
15
|
+
* 🛑 Attachments die BEFORE the Firestore conversation document — the same
|
|
16
|
+
* class of defect as the missing TTL that got the feature deleted the first
|
|
17
|
+
* time: `mediaTmpCleanup` only sweeps `tmp/`, so a finalized `media/*` object
|
|
18
|
+
* would survive this prune as a permanent orphan if the document went first
|
|
19
|
+
* and nothing remembered which assets belonged to it.
|
|
20
|
+
*/
|
|
21
|
+
async function deleteAttachmentByProxyUrl(ctx, url) {
|
|
22
|
+
if (!url.startsWith(PROXY_URL_PREFIX))
|
|
23
|
+
return;
|
|
24
|
+
const shortId = url.slice(PROXY_URL_PREFIX.length);
|
|
25
|
+
if (shortId.includes("/"))
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const asset = await mediaAssetsRepository.findById(shortId);
|
|
29
|
+
if (!asset)
|
|
30
|
+
return;
|
|
31
|
+
await getAdminStorageLite().bucket().file(asset.storagePath).delete({ ignoreNotFound: true });
|
|
32
|
+
await mediaAssetsRepository.delete(shortId);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
void normalizeError(err);
|
|
36
|
+
ctx.logger.warn("notificationPrune: failed to delete chat attachment (non-fatal)", { url });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* D9 retention, folded into this job per its own "prune user-facing records
|
|
41
|
+
* past retention" charter — query-based with `.limit()`, never a
|
|
42
|
+
* whole-collection snapshot.
|
|
43
|
+
*
|
|
44
|
+
* | Stage | Age since lastMessageAt | Action |
|
|
45
|
+
* |---|---|---|
|
|
46
|
+
* | Active | 0–30d | normal |
|
|
47
|
+
* | Archived | 30d–7mo | hidden from the default inbox, read-only |
|
|
48
|
+
* | Deleted | >7mo | document + subcollection + attachments |
|
|
49
|
+
*/
|
|
50
|
+
async function runChatRetention(ctx) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const archiveCutoff = new Date(now - CONVERSATION_ACTIVE_DAYS * 24 * 60 * 60 * 1000);
|
|
53
|
+
const deleteCutoff = new Date(now - CONVERSATION_ARCHIVE_DAYS * 24 * 60 * 60 * 1000);
|
|
54
|
+
try {
|
|
55
|
+
const toArchive = await conversationsRepository.archiveStale(archiveCutoff, RETENTION_BATCH_LIMIT);
|
|
56
|
+
for (const conv of toArchive) {
|
|
57
|
+
await conversationsRepository.update(conv.id, { status: "archived" });
|
|
58
|
+
}
|
|
59
|
+
if (toArchive.length > 0) {
|
|
60
|
+
ctx.logger.info("Conversations archived", { count: toArchive.length });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
void normalizeError(err);
|
|
65
|
+
ctx.logger.error("Chat archive stage failed (non-fatal)", err);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const toDelete = await conversationsRepository.findDeletable(deleteCutoff, RETENTION_BATCH_LIMIT);
|
|
69
|
+
for (const conv of toDelete) {
|
|
70
|
+
const attachmentUrls = await listAllAttachmentUrls(conv.id);
|
|
71
|
+
for (const url of attachmentUrls) {
|
|
72
|
+
await deleteAttachmentByProxyUrl(ctx, url);
|
|
73
|
+
}
|
|
74
|
+
await deleteAllMessages(conv.id);
|
|
75
|
+
await conversationsRepository.delete(conv.id);
|
|
76
|
+
}
|
|
77
|
+
if (toDelete.length > 0) {
|
|
78
|
+
ctx.logger.info("Conversations deleted past retention", { count: toDelete.length });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
void normalizeError(err);
|
|
83
|
+
ctx.logger.error("Chat delete stage failed (non-fatal)", err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
4
86
|
export async function runNotificationPrune(ctx) {
|
|
5
87
|
ctx.logger.info(`Pruning read notifications older than ${NOTIFICATION_TTL_DAYS} days`);
|
|
6
88
|
const refs = await notificationRepository.getOldReadRefs(NOTIFICATION_TTL_DAYS);
|
|
7
89
|
if (refs.length === 0) {
|
|
8
90
|
ctx.logger.info("No stale read notifications found");
|
|
9
|
-
return;
|
|
10
91
|
}
|
|
11
|
-
|
|
12
|
-
|
|
92
|
+
else {
|
|
93
|
+
const deleted = await batchDelete(ctx, refs);
|
|
94
|
+
ctx.logger.info("Notification prune complete", { deleted });
|
|
95
|
+
}
|
|
96
|
+
await runChatRetention(ctx);
|
|
13
97
|
}
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import { orderRepository, productRepository } from "../../../../repositories/index.js";
|
|
45
45
|
import { priceIndexRepository, } from "../../../../features/categories/repository/price-index.repository.js";
|
|
46
|
+
import { normaliseColourway } from "../../../../features/parts/utils/colourway-match.js";
|
|
46
47
|
/** How far back a band looks. */
|
|
47
48
|
export const PRICE_INDEX_WINDOW_DAYS = 90;
|
|
48
49
|
/**
|
|
@@ -160,6 +161,28 @@ function toBand(samples) {
|
|
|
160
161
|
for (const [condition, bucket] of byConditionSamples) {
|
|
161
162
|
byCondition[condition] = toConditionBand(bucket);
|
|
162
163
|
}
|
|
164
|
+
/*
|
|
165
|
+
* The per-COLOURWAY slice (I9), on exactly the same terms as the condition
|
|
166
|
+
* slice above — skipped when unrecorded, omitted rather than emitted empty.
|
|
167
|
+
*
|
|
168
|
+
* 🛑 It earns an axis because for a part it is a BIGGER price variable than
|
|
169
|
+
* condition: the silver Metal Ball and the transparent-black one are the same
|
|
170
|
+
* mould with the same stats, and one is rare and costly while the other is
|
|
171
|
+
* ordinary. One band over both reports a median that describes neither.
|
|
172
|
+
*/
|
|
173
|
+
const byColourwaySamples = new Map();
|
|
174
|
+
for (const s of samples) {
|
|
175
|
+
const key = normaliseColourway(s.colourway);
|
|
176
|
+
if (!key)
|
|
177
|
+
continue;
|
|
178
|
+
const bucket = byColourwaySamples.get(key) ?? [];
|
|
179
|
+
bucket.push(s);
|
|
180
|
+
byColourwaySamples.set(key, bucket);
|
|
181
|
+
}
|
|
182
|
+
const byColourway = {};
|
|
183
|
+
for (const [colourway, bucket] of byColourwaySamples) {
|
|
184
|
+
byColourway[colourway] = toConditionBand(bucket);
|
|
185
|
+
}
|
|
163
186
|
return {
|
|
164
187
|
min: amounts[0],
|
|
165
188
|
median: median(amounts),
|
|
@@ -168,6 +191,7 @@ function toBand(samples) {
|
|
|
168
191
|
sampleSize: samples.length,
|
|
169
192
|
byPath,
|
|
170
193
|
...(Object.keys(byCondition).length > 0 ? { byCondition } : {}),
|
|
194
|
+
...(Object.keys(byColourway).length > 0 ? { byColourway } : {}),
|
|
171
195
|
...(latest ? { lastSold: { amount: latest.amount, at: latest.at, via: latest.via } } : {}),
|
|
172
196
|
};
|
|
173
197
|
}
|
|
@@ -198,6 +222,7 @@ export async function runPriceIndexRollup(ctx) {
|
|
|
198
222
|
const leafOf = new Map();
|
|
199
223
|
const typeOf = new Map();
|
|
200
224
|
const conditionOf = new Map();
|
|
225
|
+
const colourwayOf = new Map();
|
|
201
226
|
for (const p of rows) {
|
|
202
227
|
if (!p.id)
|
|
203
228
|
continue;
|
|
@@ -208,6 +233,8 @@ export async function runPriceIndexRollup(ctx) {
|
|
|
208
233
|
typeOf.set(p.id, p.listingType);
|
|
209
234
|
if (p.condition)
|
|
210
235
|
conditionOf.set(p.id, p.condition);
|
|
236
|
+
if (p.colourway)
|
|
237
|
+
colourwayOf.set(p.id, p.colourway);
|
|
211
238
|
}
|
|
212
239
|
const delivered = (await orderRepository.findByStatus("delivered"));
|
|
213
240
|
/** leafId → listingType → samples */
|
|
@@ -259,8 +286,16 @@ export async function runPriceIndexRollup(ctx) {
|
|
|
259
286
|
* the map only answers for orders written before that.
|
|
260
287
|
*/
|
|
261
288
|
const condition = item.condition ?? (item.productId ? conditionOf.get(item.productId) : undefined);
|
|
289
|
+
// Snapshot first, join second — same rule, same reason as `condition`.
|
|
290
|
+
const colourway = item.colourway ?? (item.productId ? colourwayOf.get(item.productId) : undefined);
|
|
262
291
|
const samples = types.get(listingType) ?? [];
|
|
263
|
-
samples.push({
|
|
292
|
+
samples.push({
|
|
293
|
+
amount,
|
|
294
|
+
at,
|
|
295
|
+
via,
|
|
296
|
+
...(condition ? { condition } : {}),
|
|
297
|
+
...(colourway ? { colourway } : {}),
|
|
298
|
+
});
|
|
264
299
|
types.set(listingType, samples);
|
|
265
300
|
byLeaf.set(leaf, types);
|
|
266
301
|
considered += 1;
|
|
@@ -160,6 +160,14 @@ export const ACTIONS = {
|
|
|
160
160
|
description: "Open the native share sheet or copy the listing URL.",
|
|
161
161
|
kind: "ghost",
|
|
162
162
|
},
|
|
163
|
+
"message-seller": {
|
|
164
|
+
iconKey: "message",
|
|
165
|
+
id: "product.message-seller",
|
|
166
|
+
label: "Message Seller",
|
|
167
|
+
ariaLabel: "Message this listing's seller",
|
|
168
|
+
description: "Open or start a conversation with the seller. Absent entirely when chat is off.",
|
|
169
|
+
kind: "ghost",
|
|
170
|
+
},
|
|
163
171
|
"compare": {
|
|
164
172
|
iconKey: "analytics",
|
|
165
173
|
id: "product.compare",
|
|
@@ -97,6 +97,12 @@ export function resolveNotificationActionUrl(relatedType, relatedId, audience) {
|
|
|
97
97
|
return audience === "admin"
|
|
98
98
|
? String(ROUTES.ADMIN.STORE_DETAIL(relatedId))
|
|
99
99
|
: String(ROUTES.STORE.DASHBOARD);
|
|
100
|
+
case "conversation":
|
|
101
|
+
// D9 — one universal inbox, reachable from any portal; there is no
|
|
102
|
+
// admin-specific thread view (admins moderate via reports, not a
|
|
103
|
+
// parallel inbox route). Lives under ROUTES.USER, same precedent as
|
|
104
|
+
// WISHLIST/CART — a real top-level path, not /user/messages.
|
|
105
|
+
return String(ROUTES.USER.MESSAGE_THREAD(relatedId));
|
|
100
106
|
default: {
|
|
101
107
|
/*
|
|
102
108
|
* Exhaustiveness. A new `relatedType` cannot compile without an answer
|
|
@@ -164,4 +170,6 @@ export const TYPE_AUDIENCE = {
|
|
|
164
170
|
*/
|
|
165
171
|
support_ticket_update: "buyer",
|
|
166
172
|
scam_report_update: "buyer",
|
|
173
|
+
// D9 — one universal inbox; which human reads it is not role-dependent.
|
|
174
|
+
message_received: "buyer",
|
|
167
175
|
};
|
|
@@ -111,6 +111,9 @@ export const EMAIL_ELIGIBLE_TYPES = {
|
|
|
111
111
|
// digest. This was the second 100-way fan-out — `onScamReportCreate` sent one
|
|
112
112
|
// notification per employee, up to 100, for every report filed.
|
|
113
113
|
scam_report_update: false,
|
|
114
|
+
// D9 — the conversation lives in the UI, same reasoning as
|
|
115
|
+
// support_ticket_update. A chat message is not a deadline.
|
|
116
|
+
message_received: false,
|
|
114
117
|
};
|
|
115
118
|
/**
|
|
116
119
|
* Prefer this over indexing the map directly — the lookup is `boolean`, and a
|
|
@@ -38,6 +38,10 @@ export interface PublicSiteSettings {
|
|
|
38
38
|
listings: {
|
|
39
39
|
listingTypes?: NonNullable<SiteSettingsDocument["listings"]>["listingTypes"];
|
|
40
40
|
};
|
|
41
|
+
/** D9 — off by default; `enabled: false` is emitted rather than omitted, same `always-present` reasoning as `disabledRoutes` below. */
|
|
42
|
+
chat?: {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
};
|
|
41
45
|
notificationChannels?: NotificationChannelConfig;
|
|
42
46
|
announcementBar?: SiteSettingsDocument["announcementBar"];
|
|
43
47
|
navConfig?: SiteSettingsDocument["navConfig"];
|
package/dist/client.d.ts
CHANGED
|
@@ -300,6 +300,16 @@ export { useActiveAd } from "./features/homepage/hooks/useActiveAd";
|
|
|
300
300
|
export type { ActiveAdRecord, ActiveAdCreative } from "./features/homepage/hooks/useActiveAd";
|
|
301
301
|
export { registerAdSlot, registerAdSlots, unregisterAdSlot, clearAdRegistry, setAdConsentGranted, isAdConsentGranted, isAdSlotRenderable, } from "./features/homepage/ad-registry";
|
|
302
302
|
export type { AdSlotId, AdProvider, AdSlotConfig } from "./features/homepage/ad-registry";
|
|
303
|
+
/**
|
|
304
|
+
* D9 — chat. Client-only (every hook here is "use client"); the write path
|
|
305
|
+
* (actions) is server-only and lives in server.ts, never here.
|
|
306
|
+
*/
|
|
307
|
+
export { ConversationsInboxView } from "./features/messages/components/ConversationsInboxView";
|
|
308
|
+
export { ConversationThreadView } from "./features/messages/components/ConversationThreadView";
|
|
309
|
+
export { MessageSellerButton } from "./features/messages/components/MessageSellerButton";
|
|
310
|
+
export type { MessageSellerButtonProps } from "./features/messages/components/MessageSellerButton";
|
|
311
|
+
export { useConversations, useOpenConversation, useOpenConversationWithSeller, useConversationThread, useSendMessage, useMarkConversationRead, useBlockConversation, useUnreadMessageCount, } from "./features/messages/hooks/useMessages";
|
|
312
|
+
export type { PendingMessage } from "./features/messages/hooks/useMessages";
|
|
303
313
|
export { WishlistView, useGuestWishlist, useWishlistWithGuest } from "./features/wishlist/index";
|
|
304
314
|
export type { WishlistViewProps, GuestWishlistItem, WishlistItem, WishlistResponse, WishlistProductData, EnrichedWishlistItem } from "./features/wishlist/index";
|
|
305
315
|
export { WishlistCapWatcher } from "./features/wishlist/components/WishlistCapWatcher";
|
package/dist/client.js
CHANGED
|
@@ -332,6 +332,14 @@ export { UserSettingsView } from "./features/account/index.js";
|
|
|
332
332
|
export { AdSlot } from "./features/homepage/components/AdSlot.js";
|
|
333
333
|
export { useActiveAd } from "./features/homepage/hooks/useActiveAd.js";
|
|
334
334
|
export { registerAdSlot, registerAdSlots, unregisterAdSlot, clearAdRegistry, setAdConsentGranted, isAdConsentGranted, isAdSlotRenderable, } from "./features/homepage/ad-registry.js";
|
|
335
|
+
/**
|
|
336
|
+
* D9 — chat. Client-only (every hook here is "use client"); the write path
|
|
337
|
+
* (actions) is server-only and lives in server.ts, never here.
|
|
338
|
+
*/
|
|
339
|
+
export { ConversationsInboxView } from "./features/messages/components/ConversationsInboxView.js";
|
|
340
|
+
export { ConversationThreadView } from "./features/messages/components/ConversationThreadView.js";
|
|
341
|
+
export { MessageSellerButton } from "./features/messages/components/MessageSellerButton.js";
|
|
342
|
+
export { useConversations, useOpenConversation, useOpenConversationWithSeller, useConversationThread, useSendMessage, useMarkConversationRead, useBlockConversation, useUnreadMessageCount, } from "./features/messages/hooks/useMessages.js";
|
|
335
343
|
export { WishlistView, useGuestWishlist, useWishlistWithGuest } from "./features/wishlist/index.js";
|
|
336
344
|
export { WishlistCapWatcher } from "./features/wishlist/components/WishlistCapWatcher.js";
|
|
337
345
|
// The single wishlist heart. Exported here too because it is a "use client"
|
|
@@ -312,6 +312,22 @@ export declare const CONTACT_ENDPOINTS: {
|
|
|
312
312
|
export declare const AD_ENDPOINTS: {
|
|
313
313
|
readonly ACTIVE_BY_SLOT: (slotId: string) => string;
|
|
314
314
|
};
|
|
315
|
+
export declare const MESSAGE_ENDPOINTS: {
|
|
316
|
+
readonly LIST: (opts?: {
|
|
317
|
+
limit?: number;
|
|
318
|
+
cursor?: string;
|
|
319
|
+
}) => string;
|
|
320
|
+
readonly OPEN: "/api/messages";
|
|
321
|
+
readonly OPEN_WITH_SELLER: "/api/messages/with-seller";
|
|
322
|
+
readonly THREAD: (conversationId: string, opts?: {
|
|
323
|
+
limit?: number;
|
|
324
|
+
cursor?: string;
|
|
325
|
+
}) => string;
|
|
326
|
+
readonly READ: (conversationId: string) => string;
|
|
327
|
+
readonly BLOCK: (conversationId: string) => string;
|
|
328
|
+
readonly TOKEN: (conversationId: string) => string;
|
|
329
|
+
readonly UNREAD_COUNT: "/api/messages/unread-count";
|
|
330
|
+
};
|
|
315
331
|
export declare const EVENT_ENDPOINTS: {
|
|
316
332
|
readonly LIST: "/api/events";
|
|
317
333
|
readonly BY_ID: (id: string) => string;
|
|
@@ -799,6 +815,22 @@ export declare const API_ENDPOINTS: {
|
|
|
799
815
|
readonly ADS: {
|
|
800
816
|
readonly ACTIVE_BY_SLOT: (slotId: string) => string;
|
|
801
817
|
};
|
|
818
|
+
readonly MESSAGES: {
|
|
819
|
+
readonly LIST: (opts?: {
|
|
820
|
+
limit?: number;
|
|
821
|
+
cursor?: string;
|
|
822
|
+
}) => string;
|
|
823
|
+
readonly OPEN: "/api/messages";
|
|
824
|
+
readonly OPEN_WITH_SELLER: "/api/messages/with-seller";
|
|
825
|
+
readonly THREAD: (conversationId: string, opts?: {
|
|
826
|
+
limit?: number;
|
|
827
|
+
cursor?: string;
|
|
828
|
+
}) => string;
|
|
829
|
+
readonly READ: (conversationId: string) => string;
|
|
830
|
+
readonly BLOCK: (conversationId: string) => string;
|
|
831
|
+
readonly TOKEN: (conversationId: string) => string;
|
|
832
|
+
readonly UNREAD_COUNT: "/api/messages/unread-count";
|
|
833
|
+
};
|
|
802
834
|
readonly EVENTS: {
|
|
803
835
|
readonly LIST: "/api/events";
|
|
804
836
|
readonly BY_ID: (id: string) => string;
|
|
@@ -1285,6 +1317,22 @@ export declare const API_ROUTES: {
|
|
|
1285
1317
|
readonly ADS: {
|
|
1286
1318
|
readonly ACTIVE_BY_SLOT: (slotId: string) => string;
|
|
1287
1319
|
};
|
|
1320
|
+
readonly MESSAGES: {
|
|
1321
|
+
readonly LIST: (opts?: {
|
|
1322
|
+
limit?: number;
|
|
1323
|
+
cursor?: string;
|
|
1324
|
+
}) => string;
|
|
1325
|
+
readonly OPEN: "/api/messages";
|
|
1326
|
+
readonly OPEN_WITH_SELLER: "/api/messages/with-seller";
|
|
1327
|
+
readonly THREAD: (conversationId: string, opts?: {
|
|
1328
|
+
limit?: number;
|
|
1329
|
+
cursor?: string;
|
|
1330
|
+
}) => string;
|
|
1331
|
+
readonly READ: (conversationId: string) => string;
|
|
1332
|
+
readonly BLOCK: (conversationId: string) => string;
|
|
1333
|
+
readonly TOKEN: (conversationId: string) => string;
|
|
1334
|
+
readonly UNREAD_COUNT: "/api/messages/unread-count";
|
|
1335
|
+
};
|
|
1288
1336
|
readonly EVENTS: {
|
|
1289
1337
|
readonly LIST: "/api/events";
|
|
1290
1338
|
readonly BY_ID: (id: string) => string;
|