@wix/agent-skills 1.19.2 → 1.20.1

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 (215) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
  3. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
  4. package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
  5. package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
  6. package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
  7. package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
  8. package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
  9. package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
  10. package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
  11. package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
  12. package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
  13. package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
  14. package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
  15. package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
  16. package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
  17. package/skills/wix-headless/references/CAPABILITIES.md +2 -2
  18. package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
  19. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
  20. package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
  21. package/skills/wix-headless-fast/SKILL.md +200 -34
  22. package/skills/wix-headless-fast/entry/skill.md +10 -2
  23. package/skills/wix-headless-fast/install/attach.mjs +253 -0
  24. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  25. package/skills/wix-headless-fast/install/fast-path.mjs +28 -31
  26. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
  27. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  28. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  29. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  30. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  31. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  32. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  33. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  34. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  35. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  36. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  37. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  38. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  39. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  40. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  41. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
  42. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  43. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  44. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  45. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  46. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  47. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  48. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  49. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  50. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  51. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  52. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  53. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  54. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  55. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
  56. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  57. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  58. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  59. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  60. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  61. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  62. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  63. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  64. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
  65. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  66. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  67. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  68. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  69. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  70. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  71. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  72. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  73. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  74. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  75. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  76. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  77. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  78. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  79. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
  80. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  81. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  82. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  83. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  84. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  85. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  86. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  87. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  88. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  89. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  90. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
  91. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  92. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  93. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  94. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  95. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  96. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  97. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  98. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  99. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
  100. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  101. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  102. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  103. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  104. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  105. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  106. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  107. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  108. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  109. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  110. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  111. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  112. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  113. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  114. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
  115. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  116. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  117. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  118. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  119. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  120. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  121. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  122. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  123. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  124. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  125. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  126. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  127. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  128. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  129. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
  130. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  131. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  132. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  133. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  134. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  135. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  136. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  137. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  138. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  139. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  140. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  141. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  142. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  143. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  144. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  145. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  146. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  147. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +55 -1
  148. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  149. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  150. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  151. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  152. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +371 -81
  153. package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
  154. package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
  155. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  156. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +136 -0
  157. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
  158. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
  159. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  160. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +53 -136
  161. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
  162. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +73 -230
  163. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  164. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  165. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
  166. package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
  167. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  168. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  169. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -5
  170. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
  171. package/skills/wix-manage/SKILL.md +4 -1
  172. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  173. package/skills/wix-manage/references/events/create-wix-event.md +46 -0
  174. package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
  175. package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
  176. package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
  177. package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
  178. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
  179. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
  180. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  181. package/skills/wix-vibe-headless/platforms/base44.md +19 -4
  182. package/skills/wix-vibe-headless/platforms/generic.md +6 -0
  183. package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
  184. package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
  185. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
  186. package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
  187. package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
  188. package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
  189. package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
  190. package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
  191. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
  192. package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
  193. package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
  194. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
  195. package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
  196. package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
  197. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
  198. package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
  199. package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
  200. package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
  201. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
  202. package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
  203. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
  204. package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
  205. package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
  206. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
  207. package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
  208. package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
  209. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
  210. package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
  211. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
  212. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
  213. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  214. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  215. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -1,39 +1,21 @@
1
- // Collections gallery. SSR-friendly: pass server-fetched data as `initialCollections`
2
- // (Astro frontmatter / server component) and no client fetch happens; a SPA passes nothing.
3
- import { useEffect, useState } from "react";
4
- import { fetchCollections } from "../../wix/portfolio/portfolio";
5
- import type { CollectionSummary } from "../../wix/portfolio/types";
1
+ // React binding of the collections store (wix/portfolio/collections-store.ts) — the gallery's
2
+ // load state lives there, framework-free; this hook subscribes to one instance per mounted
3
+ // gallery. SSR-friendly: pass server-fetched data as `initialCollections` and no client fetch
4
+ // happens; a SPA passes nothing. A static page, Vue, or Svelte uses the store directly.
5
+ import { useEffect, useRef, useSyncExternalStore } from "react";
6
+ import { createCollectionsStore, type CollectionsState, type CollectionsStore, type CollectionsStoreOptions } from "../../wix/portfolio/collections-store";
6
7
 
7
- export interface UseCollectionsOptions {
8
- initialCollections?: CollectionSummary[];
9
- }
10
-
11
- export interface UseCollections {
12
- /** null while the first load is in flight — render skeletons, not an empty state. */
13
- collections: CollectionSummary[] | null;
14
- error: string | null;
15
- }
8
+ export type UseCollectionsOptions = CollectionsStoreOptions;
16
9
 
17
- export function useCollections({ initialCollections }: UseCollectionsOptions = {}): UseCollections {
18
- const [collections, setCollections] = useState<CollectionSummary[] | null>(initialCollections ?? null);
19
- const [error, setError] = useState<string | null>(null);
10
+ export type UseCollections = CollectionsState;
20
11
 
12
+ export function useCollections(options: UseCollectionsOptions = {}): UseCollections {
13
+ const ref = useRef<CollectionsStore | null>(null);
14
+ if (!ref.current) ref.current = createCollectionsStore(options);
15
+ const store = ref.current;
21
16
  useEffect(() => {
22
- let alive = true;
23
- if (!initialCollections) {
24
- fetchCollections()
25
- .then((c) => alive && setCollections(c))
26
- .catch((e) => {
27
- if (!alive) return;
28
- setCollections([]);
29
- setError(e instanceof Error ? e.message : String(e));
30
- });
31
- }
32
- return () => {
33
- alive = false;
34
- };
35
- // eslint-disable-next-line react-hooks/exhaustive-deps
36
- }, []);
37
-
38
- return { collections, error };
17
+ store.start();
18
+ return () => store.stop();
19
+ }, [store]);
20
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
39
21
  }
@@ -1,58 +1,31 @@
1
- // Project detail + media gallery, by slug. SSR-friendly: pass both `initial*` and no client
2
- // fetch happens. `notFound` is the real not-found signal (project stays null while loading).
3
- import { useEffect, useState } from "react";
4
- import { fetchProjectBySlug, fetchProjectGallery } from "../../wix/portfolio/portfolio";
5
- import type { GalleryItem, ProjectDetail } from "../../wix/portfolio/types";
1
+ // React binding of the project-detail store (wix/portfolio/project-detail-store.ts) — slug →
2
+ // project + media gallery lives there, framework-free; this hook subscribes to one instance per
3
+ // (slug, mount) and recreates it when the slug changes. SSR-friendly: pass both `initial*` and no
4
+ // client fetch happens. `notFound` is the real not-found signal (project stays null while loading).
5
+ import { useEffect, useRef, useSyncExternalStore } from "react";
6
+ import {
7
+ createProjectDetailStore,
8
+ type ProjectDetailState,
9
+ type ProjectDetailStore,
10
+ type ProjectDetailStoreOptions,
11
+ } from "../../wix/portfolio/project-detail-store";
6
12
 
7
- export interface UseProjectDetailOptions {
8
- initialProject?: ProjectDetail;
9
- initialItems?: GalleryItem[];
10
- }
13
+ export type UseProjectDetailOptions = ProjectDetailStoreOptions;
11
14
 
12
- export interface UseProjectDetail {
13
- /** null while loading AND when not found — check `notFound` to tell them apart. */
14
- project: ProjectDetail | null;
15
- notFound: boolean;
16
- /** null while the gallery load is in flight — render skeletons, not an empty state. */
17
- items: GalleryItem[] | null;
18
- error: string | null;
19
- }
15
+ export type UseProjectDetail = ProjectDetailState;
20
16
 
21
17
  export function useProjectDetail(
22
18
  slug: string,
23
19
  { initialProject, initialItems }: UseProjectDetailOptions = {},
24
20
  ): UseProjectDetail {
25
- const [project, setProject] = useState<ProjectDetail | null>(initialProject ?? null);
26
- const [notFound, setNotFound] = useState(false);
27
- const [items, setItems] = useState<GalleryItem[] | null>(initialItems ?? null);
28
- const [error, setError] = useState<string | null>(null);
29
-
21
+ const ref = useRef<{ slug: string; store: ProjectDetailStore } | null>(null);
22
+ if (!ref.current || ref.current.slug !== slug) {
23
+ ref.current = { slug, store: createProjectDetailStore(slug, { initialProject, initialItems }) };
24
+ }
25
+ const store = ref.current.store;
30
26
  useEffect(() => {
31
- if (initialProject && initialItems) return;
32
- let alive = true;
33
- (async () => {
34
- try {
35
- const proj = initialProject ?? (await fetchProjectBySlug(slug));
36
- if (!alive) return;
37
- if (!proj) {
38
- setNotFound(true);
39
- setItems([]);
40
- return;
41
- }
42
- setProject(proj);
43
- const gallery = await fetchProjectGallery(proj.id);
44
- if (alive) setItems(gallery);
45
- } catch (e) {
46
- if (!alive) return;
47
- setItems([]);
48
- setError(e instanceof Error ? e.message : String(e));
49
- }
50
- })();
51
- return () => {
52
- alive = false;
53
- };
54
- // eslint-disable-next-line react-hooks/exhaustive-deps
55
- }, [slug]);
56
-
57
- return { project, notFound, items, error };
27
+ store.start();
28
+ return () => store.stop();
29
+ }, [store]);
30
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
58
31
  }
@@ -0,0 +1,82 @@
1
+ // One collection's header + its projects, by slug, as a framework-free store — the logic behind
2
+ // useCollectionProjects, usable from React (the hook wraps it), from a static page's collection
3
+ // view, from Vue/Svelte, or as the specification for a port.
4
+ //
5
+ // SSR-friendly: pass BOTH `initialCollection` and `initialProjects` and nothing is fetched. With
6
+ // only the collection seeded, `start()` fetches its projects; with nothing seeded it resolves the
7
+ // slug first. `notFound` is the real not-found signal (collection stays null while loading too) —
8
+ // route a 404 off it, never off a transient null. One store per (slug, mount):
9
+ // createCollectionProjectsStore(slug, options), not a singleton.
10
+ import { fetchCollectionBySlug, fetchProjects } from "./portfolio";
11
+ import type { CollectionSummary, ProjectSummary } from "./types";
12
+
13
+ export interface CollectionProjectsStoreOptions {
14
+ initialCollection?: CollectionSummary;
15
+ initialProjects?: ProjectSummary[];
16
+ }
17
+
18
+ export interface CollectionProjectsState {
19
+ /** null while loading AND when not found — check `notFound` to tell them apart. */
20
+ collection: CollectionSummary | null;
21
+ notFound: boolean;
22
+ /** null while the first load is in flight — render skeletons, not an empty state. */
23
+ projects: ProjectSummary[] | null;
24
+ error: string | null;
25
+ }
26
+
27
+ export interface CollectionProjectsStore {
28
+ getState(): CollectionProjectsState;
29
+ subscribe(listener: () => void): () => void;
30
+ /** Resolve the slug and load its projects unless both were seeded. Call once when mounted. */
31
+ start(): void;
32
+ /** Stop reacting; drop late responses. */
33
+ stop(): void;
34
+ }
35
+
36
+ export function createCollectionProjectsStore(
37
+ slug: string,
38
+ { initialCollection, initialProjects }: CollectionProjectsStoreOptions = {},
39
+ ): CollectionProjectsStore {
40
+ let collection: CollectionSummary | null = initialCollection ?? null;
41
+ let notFound = false;
42
+ let projects: ProjectSummary[] | null = initialProjects ?? null;
43
+ let error: string | null = null;
44
+ let started = false;
45
+ const listeners = new Set<() => void>();
46
+ let snapshot: CollectionProjectsState | null = null;
47
+ const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
48
+
49
+ return {
50
+ getState() {
51
+ return (snapshot ??= { collection, notFound, projects, error });
52
+ },
53
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
54
+ start() {
55
+ if (started) return;
56
+ started = true;
57
+ if (initialCollection && initialProjects) return;
58
+ (async () => {
59
+ try {
60
+ const col = initialCollection ?? (await fetchCollectionBySlug(slug));
61
+ if (!started) return;
62
+ if (!col) {
63
+ notFound = true;
64
+ projects = [];
65
+ emit();
66
+ return;
67
+ }
68
+ collection = col;
69
+ emit();
70
+ const list = await fetchProjects({ collectionId: col.id });
71
+ if (started) { projects = list; emit(); }
72
+ } catch (e) {
73
+ if (!started) return;
74
+ projects = [];
75
+ error = e instanceof Error ? e.message : String(e);
76
+ emit();
77
+ }
78
+ })();
79
+ },
80
+ stop() { started = false; },
81
+ };
82
+ }
@@ -0,0 +1,58 @@
1
+ // The collections gallery as a framework-free store — the logic behind useCollections, usable
2
+ // from React (useCollections wraps it with useSyncExternalStore), from a static page or
3
+ // Vue/Svelte (subscribe and render), or as the specification for a port. State, subscribe/getState,
4
+ // start()/stop(), emit after every change.
5
+ //
6
+ // SSR-friendly: seed with `initialCollections` and nothing is fetched; without a seed `start()`
7
+ // fetches once. One store per mounted gallery: createCollectionsStore(), not a singleton.
8
+ import { fetchCollections } from "./portfolio";
9
+ import type { CollectionSummary } from "./types";
10
+
11
+ export interface CollectionsStoreOptions {
12
+ initialCollections?: CollectionSummary[];
13
+ }
14
+
15
+ export interface CollectionsState {
16
+ /** null while the first load is in flight — render skeletons, not an empty state. */
17
+ collections: CollectionSummary[] | null;
18
+ error: string | null;
19
+ }
20
+
21
+ export interface CollectionsStore {
22
+ getState(): CollectionsState;
23
+ subscribe(listener: () => void): () => void;
24
+ /** Fetch when no seed was given. Call once when mounted (a browser). */
25
+ start(): void;
26
+ /** Stop reacting; drop a late response. */
27
+ stop(): void;
28
+ }
29
+
30
+ export function createCollectionsStore({ initialCollections }: CollectionsStoreOptions = {}): CollectionsStore {
31
+ let collections: CollectionSummary[] | null = initialCollections ?? null;
32
+ let error: string | null = null;
33
+ let started = false;
34
+ const listeners = new Set<() => void>();
35
+ let snapshot: CollectionsState | null = null;
36
+ const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
37
+
38
+ return {
39
+ getState() {
40
+ return (snapshot ??= { collections, error });
41
+ },
42
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
43
+ start() {
44
+ if (started) return;
45
+ started = true;
46
+ if (initialCollections) return;
47
+ fetchCollections()
48
+ .then((c) => { if (started) { collections = c; emit(); } })
49
+ .catch((e) => {
50
+ if (!started) return;
51
+ collections = [];
52
+ error = e instanceof Error ? e.message : String(e);
53
+ emit();
54
+ });
55
+ },
56
+ stop() { started = false; },
57
+ };
58
+ }
@@ -0,0 +1,177 @@
1
+ // Portfolio rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./portfolio.ts
2
+ // (the SDK, managed Astro and React) and the REST twin in references/portfolio/rest/portfolio.ts
3
+ // (fetch, a static site or a port to another language). Every rule about hidden entities, order,
4
+ // covers, media kinds lives HERE, once. A raw entity may come from the SDK (`_id`) or from REST
5
+ // (`id`); the mappers accept both. Imports are type-only so a strip to JS emits no imports.
6
+ import type { CollectionSummary, GalleryItem, ProjectDetail, ProjectDetailRow, ProjectSummary } from "./types";
7
+
8
+ /** A raw Portfolio v1 entity as either transport returns it. */
9
+ export type Raw = Record<string, any>;
10
+ /** Media value + size → https URL. Injected: the SDK transport scales through @wix/sdk, REST through the URL form. */
11
+ export type ImgSrc = (value: any, width: number, height: number) => string;
12
+ /**
13
+ * A `wix:video://` reference → its mp4 rendition and poster, or null when it can't be resolved.
14
+ * Injected: only the SDK can resolve the string form (media.getVideoUrl); REST never receives it
15
+ * (its videoInfo is already an object with resolutions + posters, handled here).
16
+ */
17
+ export type VideoSrc = (wixVideoRef: string) => { url: string; thumbnail: string } | null;
18
+
19
+ const id = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
20
+
21
+ /**
22
+ * An image reference as either transport carries it — the SDK's bare `wix:image://` string, or
23
+ * REST's `imageInfo` object `{ id, url, filename }` — normalized to the `wix:image://` form so
24
+ * imgSrc scales both to the SAME URL (REST's `url` is a fixed 1024×1024 rendition; the id is the
25
+ * media file). "" when there is no image.
26
+ */
27
+ export function imageRef(value: unknown): string {
28
+ if (!value) return "";
29
+ if (typeof value === "string") return value;
30
+ const o = value as Raw;
31
+ if (typeof o.id === "string" && o.id) return `wix:image://v1/${o.id}/${o.filename ?? o.id}`;
32
+ return typeof o.url === "string" ? o.url : "";
33
+ }
34
+
35
+ /**
36
+ * A video as either transport carries it: the SDK's bare `wix:video://` string (resolved by the
37
+ * injected videoSrc — 720p is the standard transcode), or REST's videoInfo object
38
+ * `{ resolutions: [{ url }], posters: [{ id, url }] }` (optimal resolution first).
39
+ */
40
+ export function toVideo(value: unknown, imgSrc: ImgSrc, videoSrc: VideoSrc): { videoUrl: string | null; posterUrl: string } {
41
+ if (typeof value === "string") {
42
+ const v = value.startsWith("wix:video://") ? videoSrc(value) : null;
43
+ return v ? { videoUrl: v.url, posterUrl: imgSrc(imageRef(v.thumbnail), 1200, 675) } : { videoUrl: null, posterUrl: "" };
44
+ }
45
+ const raw = value as Raw | null | undefined;
46
+ const url = raw?.resolutions?.[0]?.url;
47
+ return {
48
+ videoUrl: typeof url === "string" ? url : null,
49
+ posterUrl: imgSrc(imageRef(raw?.posters?.[0]), 1200, 675),
50
+ };
51
+ }
52
+
53
+ // ---- DTO mappers -----------------------------------------------------------------------------------
54
+
55
+ export function toCollection(raw: Raw, imgSrc: ImgSrc): CollectionSummary {
56
+ return {
57
+ id: id(raw),
58
+ slug: raw.slug ?? "",
59
+ title: raw.title ?? "",
60
+ description: raw.description ?? "",
61
+ imageUrl: imgSrc(imageRef(raw.coverImage?.imageInfo), 1200, 900),
62
+ };
63
+ }
64
+
65
+ /** coverImage is ONE-OF with coverVideo — a video-covered project falls back to its poster. */
66
+ export function projectCover(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): string {
67
+ return imgSrc(imageRef(raw.coverImage?.imageInfo), 1200, 900) || toVideo(raw.coverVideo?.videoInfo, imgSrc, videoSrc).posterUrl;
68
+ }
69
+
70
+ export function toProject(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectSummary {
71
+ return {
72
+ id: id(raw),
73
+ slug: raw.slug ?? "",
74
+ title: raw.title ?? "",
75
+ description: raw.description ?? "",
76
+ imageUrl: projectCover(raw, imgSrc, videoSrc),
77
+ collectionIds: raw.collectionIds ?? [],
78
+ };
79
+ }
80
+
81
+ /** A details row is a one-of: { label, text } or { label, link: { text, url, target } }. */
82
+ export function toDetailRow(raw: Raw): ProjectDetailRow {
83
+ return {
84
+ label: raw.label ?? "",
85
+ text: raw.link?.text ?? raw.text ?? "",
86
+ url: raw.link?.url ?? null,
87
+ target: raw.link?.target ?? null,
88
+ };
89
+ }
90
+
91
+ export function toDetail(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectDetail {
92
+ return { ...toProject(raw, imgSrc, videoSrc), details: ((raw.details ?? []) as Raw[]).map(toDetailRow) };
93
+ }
94
+
95
+ /** IMAGE → image item, VIDEO → video item; UNDEFINED or unresolvable media → null (nothing renderable). */
96
+ export function toGalleryItem(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): GalleryItem | null {
97
+ const base = {
98
+ id: id(raw),
99
+ title: raw.title ?? "",
100
+ description: raw.description ?? "",
101
+ linkUrl: raw.link?.url ?? null,
102
+ linkTarget: raw.link?.target ?? null,
103
+ };
104
+ if (raw.type === "IMAGE") {
105
+ const imageUrl = imgSrc(imageRef(raw.image?.imageInfo), 1600, 1200);
106
+ return imageUrl ? { ...base, kind: "image", imageUrl, videoUrl: null } : null;
107
+ }
108
+ if (raw.type === "VIDEO") {
109
+ const { videoUrl, posterUrl } = toVideo(raw.video?.videoInfo, imgSrc, videoSrc);
110
+ return videoUrl ? { ...base, kind: "video", imageUrl: posterUrl, videoUrl } : null;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ // ---- list rules -----------------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Visible collections in the owner's dashboard order (sortOrder — collections have one, projects
119
+ * don't). An omitted `hidden` comes back ABSENT (proto3): test falsy, never === false.
120
+ */
121
+ export function visibleCollections(raws: Raw[], imgSrc: ImgSrc): CollectionSummary[] {
122
+ return raws
123
+ .filter((c) => !c.hidden)
124
+ .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
125
+ .map((c) => toCollection(c, imgSrc));
126
+ }
127
+
128
+ /** One visible collection, or null when missing or hidden (a real 404, never a fallback). */
129
+ export function visibleCollection(raw: Raw | undefined | null, imgSrc: ImgSrc): CollectionSummary | null {
130
+ return raw && !raw.hidden ? toCollection(raw, imgSrc) : null;
131
+ }
132
+
133
+ /**
134
+ * Visible projects in list order (Project exposes no sortOrder), optionally only one collection's.
135
+ * The collection filter is client-side over the one list (portfolios are small; a project links
136
+ * to collections via collectionIds).
137
+ */
138
+ export function visibleProjects(raws: Raw[], collectionId: string | undefined, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectSummary[] {
139
+ return raws
140
+ .filter((p) => !p.hidden)
141
+ .filter((p) => !collectionId || ((p.collectionIds ?? []) as string[]).includes(collectionId))
142
+ .map((p) => toProject(p, imgSrc, videoSrc));
143
+ }
144
+
145
+ /** One visible project with its details rows, or null when missing or hidden. */
146
+ export function visibleProject(raw: Raw | undefined | null, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectDetail | null {
147
+ return raw && !raw.hidden ? toDetail(raw, imgSrc, videoSrc) : null;
148
+ }
149
+
150
+ /** A project's gallery in dashboard order (item sortOrder); UNDEFINED and unresolvable items dropped. */
151
+ export function galleryItems(raws: Raw[], imgSrc: ImgSrc, videoSrc: VideoSrc): GalleryItem[] {
152
+ return raws
153
+ .slice()
154
+ .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
155
+ .map((it) => toGalleryItem(it, imgSrc, videoSrc))
156
+ .filter((g): g is GalleryItem => g !== null);
157
+ }
158
+
159
+ // ---- render rule ----------------------------------------------------------------------------------
160
+
161
+ /** The one element a gallery item renders as, attribute names as HTML spells them. */
162
+ export type MediaElement =
163
+ | { tag: "img"; attrs: { src: string; alt: string; loading: "lazy"; decoding: "async" } }
164
+ | { tag: "video"; attrs: { src: string; poster?: string; controls: ""; playsinline: "" } };
165
+
166
+ /**
167
+ * Branch on `kind` once, here: a video item is a <video> with its poster, an image item an <img>,
168
+ * anything else null — never a broken tag, never an <img> for a video. The shipped GalleryMedia.tsx
169
+ * renders this; a static page or a port creates the element from it.
170
+ */
171
+ export function mediaElement(item: GalleryItem): MediaElement | null {
172
+ if (item.kind === "video" && item.videoUrl) {
173
+ return { tag: "video", attrs: { src: item.videoUrl, ...(item.imageUrl ? { poster: item.imageUrl } : {}), controls: "", playsinline: "" } };
174
+ }
175
+ if (item.imageUrl) return { tag: "img", attrs: { src: item.imageUrl, alt: item.title, loading: "lazy", decoding: "async" } };
176
+ return null;
177
+ }
@@ -1,6 +1,8 @@
1
- // Portfolio reads (Wix Portfolio v1) — the only file that touches raw portfolio entities.
2
- // Read-only vertical: no cart, no checkout, no @wix/ecom. Everything it returns is a plain
3
- // DTO from ./types. Copy as-is; extend by adding functions, not by editing these.
1
+ // Portfolio reads (Wix Portfolio v1) over the SDK — the only file that touches raw portfolio
2
+ // entities on this transport. Read-only vertical: no cart, no checkout, no @wix/ecom. Everything
3
+ // it returns is a plain DTO from ./types. The rules and mappers live in ./portfolio-core (shared
4
+ // with the REST twin in references/portfolio/rest/); this file is the transport only. Copy as-is;
5
+ // extend by adding functions, not by editing these.
4
6
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/collections/list-collections.md
5
7
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/projects/list-projects.md
6
8
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/project-items/list-project-items.md
@@ -13,142 +15,57 @@ import {
13
15
  import { media, VideoResolution } from "@wix/sdk";
14
16
  import { wixModule } from "../sdk";
15
17
  import { imgSrc } from "../media";
16
- import type {
17
- CollectionSummary,
18
- GalleryItem,
19
- ProjectDetail,
20
- ProjectDetailRow,
21
- ProjectSummary,
22
- } from "./types";
18
+ import {
19
+ galleryItems,
20
+ visibleCollection,
21
+ visibleCollections,
22
+ visibleProject,
23
+ visibleProjects,
24
+ type Raw,
25
+ type VideoSrc,
26
+ } from "./portfolio-core";
27
+ import type { CollectionSummary, GalleryItem, ProjectDetail, ProjectSummary } from "./types";
23
28
 
24
29
  const collections = wixModule(collectionsModule);
25
30
  const projects = wixModule(projectsModule);
26
31
  const projectItems = wixModule(projectItemsModule);
27
32
 
28
- type Raw = Record<string, any>;
29
-
30
- // The SDK types coverImage.imageInfo / item.image.imageInfo as a bare wix:image:// STRING
31
- // (the REST docs show an object with .url — the package wins); imgSrc resolves either shape.
32
-
33
- // videoInfo is likewise a bare wix:video:// string; getVideoUrl derives the mp4 rendition +
34
- // poster (a wix:image:// string → imgSrc again). 720p is the standard transcode. Tolerates
35
- // the REST-shaped object ({ resolutions, posters }) in case a surface returns it at runtime.
36
- function toVideo(value: unknown): { videoUrl: string | null; posterUrl: string } {
37
- if (typeof value === "string" && value.startsWith("wix:video://")) {
38
- try {
39
- const v = media.getVideoUrl(value, VideoResolution.MID);
40
- return { videoUrl: v.url, posterUrl: imgSrc(v.thumbnail, 1200, 675) };
41
- } catch {
42
- return { videoUrl: null, posterUrl: "" };
43
- }
44
- }
45
- const raw = value as Raw | null | undefined;
46
- const url = raw?.resolutions?.[0]?.url;
47
- return {
48
- videoUrl: typeof url === "string" ? url : null,
49
- posterUrl: imgSrc(raw?.posters?.[0], 1200, 675),
50
- };
51
- }
52
-
53
- function toCollection(raw: Raw): CollectionSummary {
54
- return {
55
- id: raw._id ?? "",
56
- slug: raw.slug ?? "",
57
- title: raw.title ?? "",
58
- description: raw.description ?? "",
59
- imageUrl: imgSrc(raw.coverImage?.imageInfo, 1200, 900),
60
- };
61
- }
62
-
63
- // coverImage is ONE-OF with coverVideo — a video-covered project falls back to its poster.
64
- function projectCover(raw: Raw): string {
65
- return imgSrc(raw.coverImage?.imageInfo, 1200, 900) || toVideo(raw.coverVideo?.videoInfo).posterUrl;
66
- }
67
-
68
- function toProject(raw: Raw): ProjectSummary {
69
- return {
70
- id: raw._id ?? "",
71
- slug: raw.slug ?? "",
72
- title: raw.title ?? "",
73
- description: raw.description ?? "",
74
- imageUrl: projectCover(raw),
75
- collectionIds: raw.collectionIds ?? [],
76
- };
77
- }
78
-
79
- // A details row is a one-of: { label, text } or { label, link: { text, url, target } }.
80
- function toDetailRow(raw: Raw): ProjectDetailRow {
81
- return {
82
- label: raw.label ?? "",
83
- text: raw.link?.text ?? raw.text ?? "",
84
- url: raw.link?.url ?? null,
85
- target: raw.link?.target ?? null,
86
- };
87
- }
88
-
89
- function toDetail(raw: Raw): ProjectDetail {
90
- return { ...toProject(raw), details: (raw.details ?? []).map((d: Raw) => toDetailRow(d)) };
91
- }
92
-
93
- function toGalleryItem(raw: Raw): GalleryItem | null {
94
- const base = {
95
- id: raw._id ?? "",
96
- title: raw.title ?? "",
97
- description: raw.description ?? "",
98
- linkUrl: raw.link?.url ?? null,
99
- linkTarget: raw.link?.target ?? null,
100
- };
101
- if (raw.type === "IMAGE") {
102
- const imageUrl = imgSrc(raw.image?.imageInfo, 1600, 1200);
103
- return imageUrl ? { ...base, kind: "image", imageUrl, videoUrl: null } : null;
104
- }
105
- if (raw.type === "VIDEO") {
106
- const { videoUrl, posterUrl } = toVideo(raw.video?.videoInfo);
107
- return videoUrl ? { ...base, kind: "video", imageUrl: posterUrl, videoUrl } : null;
33
+ // The SDK types coverImage.imageInfo / item.image.imageInfo as a bare wix:image:// STRING and
34
+ // videoInfo as a bare wix:video:// string; getVideoUrl derives the mp4 rendition + poster.
35
+ const videoSrc: VideoSrc = (ref) => {
36
+ try {
37
+ const v = media.getVideoUrl(ref, VideoResolution.MID);
38
+ return { url: v.url, thumbnail: v.thumbnail };
39
+ } catch {
40
+ return null;
108
41
  }
109
- return null; // UNDEFINED — nothing renderable
110
- }
42
+ };
111
43
 
112
- /**
113
- * List visible collections in the owner's dashboard order (sortOrder — collections have one,
114
- * projects don't). An omitted `hidden` comes back ABSENT (proto3): test falsy, never === false.
115
- */
44
+ /** List visible collections in the owner's dashboard order (sortOrder). */
116
45
  export async function fetchCollections({ limit = 100 } = {}): Promise<CollectionSummary[]> {
117
46
  const res = await collections.listCollections({ paging: { limit } });
118
- return (res.collections ?? [])
119
- .filter((c: Raw) => !c.hidden)
120
- .sort((a: Raw, b: Raw) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
121
- .map((c: Raw) => toCollection(c));
47
+ return visibleCollections((res.collections ?? []) as Raw[], imgSrc);
122
48
  }
123
49
 
124
50
  /** Fetch one visible collection by its URL slug. Null when not found or hidden. */
125
51
  export async function fetchCollectionBySlug(slug: string): Promise<CollectionSummary | null> {
126
52
  const res = await collections.queryCollections().eq("slug", slug).limit(1).find();
127
- const raw = res.items?.[0] as Raw | undefined;
128
- return raw && !raw.hidden ? toCollection(raw) : null;
53
+ return visibleCollection(res.items?.[0] as Raw | undefined, imgSrc);
129
54
  }
130
55
 
131
- /**
132
- * List visible projects (list order — Project exposes no sortOrder), optionally only one
133
- * collection's. The collection filter is client-side over the one list call (portfolios are
134
- * small; a project links to collections via collectionIds).
135
- */
56
+ /** List visible projects (list order), optionally only one collection's. */
136
57
  export async function fetchProjects({
137
58
  collectionId,
138
59
  limit = 100,
139
60
  }: { collectionId?: string; limit?: number } = {}): Promise<ProjectSummary[]> {
140
61
  const res = await projects.listProjects({ paging: { limit } });
141
- return (res.projects ?? [])
142
- .filter((p: Raw) => !p.hidden)
143
- .filter((p: Raw) => !collectionId || (p.collectionIds ?? []).includes(collectionId))
144
- .map((p: Raw) => toProject(p));
62
+ return visibleProjects((res.projects ?? []) as Raw[], collectionId, imgSrc, videoSrc);
145
63
  }
146
64
 
147
65
  /** Fetch one visible project by its URL slug. Null when not found or hidden. */
148
66
  export async function fetchProjectBySlug(slug: string): Promise<ProjectDetail | null> {
149
67
  const res = await projects.queryProjects().eq("slug", slug).limit(1).find();
150
- const raw = res.items?.[0] as Raw | undefined;
151
- return raw && !raw.hidden ? toDetail(raw) : null;
68
+ return visibleProject(res.items?.[0] as Raw | undefined, imgSrc, videoSrc);
152
69
  }
153
70
 
154
71
  /**
@@ -157,9 +74,5 @@ export async function fetchProjectBySlug(slug: string): Promise<ProjectDetail |
157
74
  */
158
75
  export async function fetchProjectGallery(projectId: string): Promise<GalleryItem[]> {
159
76
  const res = await projectItems.listProjectItems(projectId, { paging: { limit: 100 } });
160
- return (res.items ?? [])
161
- .slice()
162
- .sort((a: Raw, b: Raw) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
163
- .map((it: Raw) => toGalleryItem(it))
164
- .filter((g: GalleryItem | null): g is GalleryItem => g !== null);
77
+ return galleryItems((res.items ?? []) as Raw[], imgSrc, videoSrc);
165
78
  }