@mohasinac/appkit 4.42.18 → 4.43.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.
Files changed (50) hide show
  1. package/dist/_internal/client/features/lottery/LotteryDetailView.js +22 -1
  2. package/dist/_internal/server/features/products/list-public.d.ts +24 -0
  3. package/dist/_internal/server/features/products/list-public.js +10 -2
  4. package/dist/_internal/server/jobs/core/onCategoryWrite.js +5 -0
  5. package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
  6. package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
  7. package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
  8. package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
  9. package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
  10. package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
  11. package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
  12. package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
  13. package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
  14. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
  15. package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
  16. package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
  17. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  18. package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
  19. package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
  20. package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
  21. package/dist/features/blog/api/route.js +16 -1
  22. package/dist/features/categories/actions/category-actions.d.ts +2 -2
  23. package/dist/features/categories/actions/category-actions.js +22 -10
  24. package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
  25. package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
  26. package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
  27. package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
  28. package/dist/features/categories/repository/categories.repository.d.ts +11 -1
  29. package/dist/features/categories/repository/categories.repository.js +25 -7
  30. package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
  31. package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
  32. package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
  33. package/dist/features/homepage/lib/live-stats.js +18 -8
  34. package/dist/features/media/MediaImage.js +19 -2
  35. package/dist/features/products/actions/product-actions.d.ts +0 -9
  36. package/dist/features/products/actions/product-actions.js +25 -0
  37. package/dist/features/products/components/ArtStickersListView.js +7 -1
  38. package/dist/features/products/components/PrizeDrawsSection.js +5 -0
  39. package/dist/features/products/components/ProductsIndexPageView.js +1 -1
  40. package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
  41. package/dist/features/reviews/repository/reviews.repository.js +35 -0
  42. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +2 -2
  43. package/dist/seed/_helpers/media.d.ts +1 -1
  44. package/dist/seed/_helpers/media.js +44 -8
  45. package/dist/seed/orders-seed-data.js +5 -3
  46. package/dist/server.d.ts +2 -0
  47. package/dist/server.js +6 -0
  48. package/dist/utils/media-url.d.ts +26 -0
  49. package/dist/utils/media-url.js +144 -0
  50. package/package.json +1 -1
@@ -38,6 +38,122 @@ export const APPROVED_MEDIA_DOMAINS = [
38
38
  "images.unsplash.com",
39
39
  ];
40
40
  export const MEDIA_URL_MESSAGE = "Must be a stored media reference (/media/<slug>) or a URL on an approved CDN domain";
41
+ /* ── Placeholder imagery ─────────────────────────────────────────────────── */
42
+ /**
43
+ * Local, static stand-ins for seed photography. Served straight out of
44
+ * `public/` — no function, no upstream fetch, no sharp pipeline.
45
+ *
46
+ * 🛑 This is the single most load-bearing cost control in the app. Every seed
47
+ * image used to resolve to `/api/media/ext?url=https://placehold.co/…`, i.e. a
48
+ * Node lambda that fetched a third party (up to 2 × 4 s) and ran a full sharp
49
+ * decode/watermark/encode — ONE INVOCATION PER IMAGE. Measured 2026-10-09: the
50
+ * homepage referenced **160** such images at ~49.5 KB each, and
51
+ * `52.52 GB of Fast Origin Transfer ÷ 49.5 KB ≈ 1.06 M` proxy responses in a
52
+ * week. That is what suspended the project (HTTP 402).
53
+ *
54
+ * Six tiles, because the stored URL already encodes which of the six
55
+ * `SEED_PHOTO_COLOURS` it was generated with — so the mapping needs no new
56
+ * data and no migration. The homepage goes 160 images → 6.
57
+ */
58
+ export const SEED_TILE_PREFIX = "/images/seed-tiles/";
59
+ /**
60
+ * Must stay in lockstep with `SEED_PHOTO_COLOURS` in
61
+ * `appkit/src/seed/_helpers/media.ts`, and with the filenames in
62
+ * `public/images/seed-tiles/`. A colour missing here is not an error — it falls
63
+ * back to a deterministic pick — so the failure mode is "less variety", never a
64
+ * broken image.
65
+ */
66
+ const SEED_TILE_COLOURS = [
67
+ "1e293b",
68
+ "334155",
69
+ "3f3f46",
70
+ "44403c",
71
+ "312e81",
72
+ "164e63",
73
+ ];
74
+ /**
75
+ * Hosts that only ever serve synthetic placeholder imagery.
76
+ *
77
+ * These are the four the seed helper documents having cycled through as each
78
+ * one fell over (picsum 503 on 2026-08-31, placekitten 521, loremflickr too
79
+ * slow for the proxy's 4 s timeout). Watermarking a generated placeholder is
80
+ * pointless, and proxying one costs a function invocation plus full origin
81
+ * transfer for an image we can synthesise locally for free.
82
+ */
83
+ const PLACEHOLDER_MEDIA_HOSTS = new Set([
84
+ "placehold.co",
85
+ "placeholder.com",
86
+ "via.placeholder.com",
87
+ "picsum.photos",
88
+ "fastly.picsum.photos",
89
+ "placekitten.com",
90
+ "loremflickr.com",
91
+ "dummyimage.com",
92
+ ]);
93
+ /** Deterministic 32-bit hash — same input, same tile, every render. */
94
+ function tileHash(input) {
95
+ let h = 2166136261;
96
+ for (let i = 0; i < input.length; i += 1) {
97
+ h ^= input.charCodeAt(i);
98
+ h = Math.imul(h, 16777619);
99
+ }
100
+ return h >>> 0;
101
+ }
102
+ /**
103
+ * The inner `url=` of a stored `/api/media/ext?url=…` value, or null.
104
+ *
105
+ * This is what lets the fix reach data that is ALREADY in Firestore. `seedPhoto`
106
+ * persisted the wrapped form for ~400 assets, and `resolveMediaUrl` used to hand
107
+ * such a value straight back untouched — `new URL()` throws on the relative path
108
+ * and the `catch` returns the input — so the proxy was entered on every render.
109
+ * Unwrapping here fixes every stored row with no migration.
110
+ */
111
+ function innerExtUrl(value) {
112
+ if (!value.startsWith(MEDIA_ENDPOINTS.EXT))
113
+ return null;
114
+ const q = value.indexOf("?");
115
+ if (q === -1)
116
+ return null;
117
+ try {
118
+ return new URLSearchParams(value.slice(q + 1)).get("url");
119
+ }
120
+ catch (_err) {
121
+ void normalizeError(_err);
122
+ return null;
123
+ }
124
+ }
125
+ /**
126
+ * A local tile for `absUrl` when it points at a placeholder host, else undefined.
127
+ *
128
+ * placehold.co bakes its colours into the path (`/{w}x{h}/{bg}/{fg}/png`), so the
129
+ * first 6-hex segment IS the background the fixture was generated with — the
130
+ * size segment contains an `x` and cannot match, and the foreground comes after
131
+ * the background, so `.find()` picks the right one.
132
+ */
133
+ export function placeholderTileFor(absUrl) {
134
+ let parsed;
135
+ try {
136
+ parsed = new URL(absUrl);
137
+ }
138
+ catch (_err) {
139
+ void normalizeError(_err);
140
+ return undefined;
141
+ }
142
+ const host = parsed.hostname.replace(/^www\./, "");
143
+ if (!PLACEHOLDER_MEDIA_HOSTS.has(host))
144
+ return undefined;
145
+ const bg = parsed.pathname
146
+ .split("/")
147
+ .find((seg) => /^[0-9a-fA-F]{6}$/.test(seg))
148
+ ?.toLowerCase();
149
+ if (bg && SEED_TILE_COLOURS.includes(bg))
150
+ return `${SEED_TILE_PREFIX}${bg}.svg`;
151
+ // Unknown or absent colour (picsum/placekitten carry none) — pick
152
+ // deterministically from the path so the same fixture always gets the same
153
+ // tile and the grid keeps its variety.
154
+ const fallback = SEED_TILE_COLOURS[tileHash(parsed.pathname) % SEED_TILE_COLOURS.length];
155
+ return `${SEED_TILE_PREFIX}${fallback}.svg`;
156
+ }
41
157
  /**
42
158
  * Is `value` something we are willing to PERSIST as a media reference?
43
159
  *
@@ -95,6 +211,11 @@ export function isStoredMediaRef(value) {
95
211
  return false;
96
212
  if (value.startsWith(MEDIA_PROXY_PREFIX))
97
213
  return true;
214
+ // A local seed tile (`/images/seed-tiles/<hex>.svg`). Persistable: it is a
215
+ // static asset we ship, so unlike the `/api/media/ext?url=` form rejected
216
+ // below it is not a render-time transform and does not double-wrap.
217
+ if (value.startsWith(SEED_TILE_PREFIX))
218
+ return true;
98
219
  if (value.startsWith("blob:") || value.startsWith("data:"))
99
220
  return false;
100
221
  if (value.startsWith(MEDIA_ENDPOINTS.EXT))
@@ -148,6 +269,8 @@ export function resolveMediaUrl(url, opts) {
148
269
  }
149
270
  if (url.startsWith(PROXY_PREFIX))
150
271
  return url;
272
+ if (url.startsWith(SEED_TILE_PREFIX))
273
+ return url;
151
274
  // A blob:/data: URI is only ever valid in the tab that created it (e.g. a
152
275
  // freshly-selected file preview via URL.createObjectURL, or a FileReader
153
276
  // data URL fed to a crop modal). Routing it through the external-URL
@@ -155,8 +278,29 @@ export function resolveMediaUrl(url, opts) {
155
278
  // rendered directly instead.
156
279
  if (url.startsWith("blob:") || url.startsWith("data:"))
157
280
  return url;
281
+ /*
282
+ * An ALREADY-WRAPPED `/api/media/ext?url=…` value, which is what the seed
283
+ * catalogue persisted ~400 times. Unwrap and look at what it actually points
284
+ * at: a placeholder host becomes a local tile (no function, no fetch), and
285
+ * anything else falls through to the old behaviour of returning the wrapped
286
+ * value untouched. Without this the branch below is unreachable for stored
287
+ * data, because `new URL()` throws on the relative path.
288
+ */
289
+ const inner = innerExtUrl(url);
290
+ if (inner) {
291
+ const innerTile = placeholderTileFor(inner);
292
+ if (innerTile)
293
+ return innerTile;
294
+ return url;
295
+ }
158
296
  try {
159
297
  const parsed = new URL(url);
298
+ // Synthetic placeholder imagery: never worth a function invocation. The
299
+ // placeholder hosts and our own Storage hosts are disjoint sets, so this
300
+ // can sit before the own-bucket branches without shadowing a real asset.
301
+ const tile = placeholderTileFor(url);
302
+ if (tile)
303
+ return tile;
160
304
  if (parsed.hostname.endsWith(FIREBASE_STORAGE_HOST)) {
161
305
  const m = parsed.pathname.match(/\/o\/([^?]+)/);
162
306
  if (m)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.42.18",
3
+ "version": "4.43.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"