@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,55 +1,83 @@
1
1
  # Portfolio — playbook
2
2
 
3
3
  The portfolio machinery ships as files — collections, projects, the per-project media gallery
4
- (image AND video items), typed end-to-end. Portfolio is **read-only**: no cart, no checkout,
5
- no `@wix/ecom`. **The presentation is yours**: you design and implement the collection card,
6
- the project card + grid, the project-detail gallery, and the home page on the shipped
7
- hooks/DTOs, plus the brand. You never write read logic; you never skip designing.
4
+ (image AND video items), the state machines behind every surface, typed end-to-end. Portfolio is
5
+ **read-only**: no cart, no checkout, no `@wix/ecom`. **The presentation is yours**: you design and
6
+ implement the collection card, the project card + grid, the project-detail gallery, and the home
7
+ page on the shipped hooks/DTOs, plus the brand. You never write read logic; you never skip designing.
8
8
 
9
9
  ## The file map (deployed into `src/`)
10
10
 
11
- **Don't read the shipped files** — this table and the contracts below are everything you
12
- need. Open a shipped file's source only on a real fallback (runtime error / uncovered field),
13
- or to read a reference component's pattern.
11
+ **On Astro and React the shipped files are tested and work as they are** — this table and the
12
+ contracts below are everything you need to use them, so don't spend the run reading their source;
13
+ wire them and build your surfaces. Reading them is the right move when something is off (a runtime
14
+ error, a field this playbook doesn't cover) or when the brief wants a behaviour they don't offer —
15
+ then read the file that owns it and change or extend it. On `lib`, `static`, and a port the
16
+ components don't deploy at all, and each wiring section below opens with the files to read before
17
+ writing their equivalents. Files you edit: `SiteLayout.astro`, `styles/global.css`, and the three
18
+ pages' island imports. Files you **create**: your gallery, collection, and project-detail islands
19
+ (skeletons below), plus your home page.
14
20
 
15
21
  | file | what it is |
16
22
  |---|---|
17
- | `wix/config.ts` · `wix/sdk.ts` · `wix/media.ts` · `wix/money.ts` | shared auth seam + helpers (deploy configures; nothing to set) |
18
- | `wix/portfolio/types.ts` | the DTOs (`CollectionSummary`, `ProjectSummary`, `ProjectDetail`, `ProjectDetailRow`, `GalleryItem`) — contracts below |
19
- | `wix/portfolio/portfolio.ts` | `fetchCollections`, `fetchCollectionBySlug`, `fetchProjects`, `fetchProjectBySlug`, `fetchProjectGallery` |
20
- | `hooks/portfolio/useCollections.ts` | collections gallery — contract below |
21
- | `hooks/portfolio/useCollectionProjects.ts` | one collection + its projects, by slug — contract below |
22
- | `hooks/portfolio/useProjectDetail.ts` | one project + its media gallery, by slug — contract below |
23
- | `components/portfolio/CollectionsView.tsx` (+ `CollectionCard`) · `CollectionProjectsView.tsx` (+ `ProjectCard`) · `ProjectDetailView.tsx` (+ `GalleryMedia`) | **REFERENCE implementations** — correct, plain; build your own instead of shipping them |
24
- | `styles/global.css` | the design system: Tailwind v4 + the `@theme` token block (shared across verticals) |
23
+ | `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
24
+ | `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes, ratio?)` — every `<img>` attribute for a DTO image (`src`, `srcSet`, `sizes`, lazy): `<img {...imgAttrs(c.imageUrl, "33vw", 0.75)} alt={c.title} />`; `imgSrc()` / `imgSrcSet()` underneath, already used by everything shipped |
25
+ | `wix/portfolio/types.ts` | the DTOs (`CollectionSummary`, `ProjectSummary`, `ProjectDetail`, `ProjectDetailRow`, `GalleryItem`) — contracts inlined below |
26
+ | `wix/portfolio/portfolio.ts` | `fetchCollections`, `fetchCollectionBySlug`, `fetchProjects`, `fetchProjectBySlug`, `fetchProjectGallery` — the transport; the rules and DTO mappers (hidden filter, dashboard order, cover fallback, media kinds, `mediaElement`) are in `portfolio-core.ts` beside it (shared with the REST layer) |
27
+ | `wix/portfolio/collections-store.ts` · `collection-projects-store.ts` · `project-detail-store.ts` | the gallery, collection-page, and project-page state machines, framework-free (`createCollectionsStore()`, `createCollectionProjectsStore(slug)`, `createProjectDetailStore(slug)` — `getState`/`subscribe`/`start`/`stop`, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
28
+ | `hooks/portfolio/useCollections.ts` | React binding of `collections-store.ts` — contract below |
29
+ | `hooks/portfolio/useCollectionProjects.ts` | React binding of `collection-projects-store.ts`: one collection + its projects, by slug — contract below |
30
+ | `hooks/portfolio/useProjectDetail.ts` | React binding of `project-detail-store.ts`: one project + its media gallery, by slug — contract below |
31
+ | `components/portfolio/GalleryMedia.tsx` | one gallery item as its one element — `<video>` with poster for a video item, `<img>` for an image item, nothing for an unrenderable one; `GalleryLink` wraps it in the owner's link when set — **wire as-is** in your project surface (`<GalleryLink item={i}><GalleryMedia item={i} /></GalleryLink>`) |
32
+ | `components/portfolio/CollectionsView.tsx` (+ `CollectionCard`) · `CollectionProjectsView.tsx` (+ `ProjectCard`) · `ProjectDetailView.tsx` | **REFERENCE implementations** — correct, plain; build your own instead of shipping them |
33
+ | `styles/global.css` | **the design system**: Tailwind v4 + the `@theme` token block (colors, radii, fonts). Everything, shipped and yours, styles from these tokens |
25
34
 
26
35
  Astro stack additionally gets:
27
36
 
28
37
  | file | what it is |
29
38
  |---|---|
30
- | `layouts/SiteLayout.astro` | site chrome — **yours to brand** (keep the `seo-tags` slot + global.css import). If another vertical is also deployed, its layout won — add a Portfolio nav link there |
39
+ | `layouts/SiteLayout.astro` | the site chrome — **yours to brand**: header, footer, nav. Keep the `<slot name="seo-tags" />` and the global.css import. If another vertical is also deployed, its layout won — add a Portfolio nav link there |
31
40
  | `pages/portfolio.astro` | SSR collections gallery — **keep the frontmatter**, swap the island import to YOUR component |
32
41
  | `pages/portfolio/[slug].astro` | SSR collection page (header + projects) — **keep the frontmatter**, swap the island import |
33
- | `pages/projects/[slug].astro` | SSR project detail + gallery — **keep the frontmatter**, swap the island import. Portfolio has no documented Wix SEO item type, so these pages carry plain `<title>`/`<meta>` from the DTO (no `wixMetadata`/`<SEO.Tags>`) |
42
+ | `pages/projects/[slug].astro` | SSR project detail + gallery — **keep the frontmatter**, swap the island import. Portfolio has no Wix SEO item type, so these pages carry plain `<title>`/`<meta>` from the DTO (no `wixMetadata`/`<SEO.Tags>`) |
34
43
 
35
44
  ## What you build — the design job
36
45
 
37
- 1. **The collection card + gallery surface** — your tile (cover, title, description) and
38
- rhythm, with skeletons while loading and an honest empty state — on `useCollections`.
46
+ 1. **The collection card + gallery surface** — your tile (cover, title, description) and rhythm,
47
+ with skeletons while loading and an honest empty state — on `useCollections`.
39
48
  2. **The project card + collection page** — the collection header and your project grid — on
40
- `useCollectionProjects` (route a not-found state off `notFound`, never off a transient
41
- null).
49
+ `useCollectionProjects` (route a not-found state off `notFound`, never off a transient null).
42
50
  3. **The project-detail surface** — title/description, the `details` rows (text or link, as
43
- given), and the media gallery: `kind: "image"` → `<img>`; `kind: "video"` → `<video>` with
44
- `videoUrl` + `imageUrl` as poster; wrap in a link when `linkUrl` is set; empty gallery →
45
- the project cover when it exists, else an honest empty note — on `useProjectDetail`.
51
+ given), and the media gallery through the shipped `GalleryMedia`; empty gallery → the project
52
+ cover when it exists, else an honest empty note — on `useProjectDetail`.
46
53
  4. **The home page** — hero, featured collections or projects (fetch in frontmatter → your
47
54
  components), brand story.
48
55
 
49
56
  Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
50
- Style everything with Tailwind utilities on the tokens.
51
57
 
52
- ### The contracts your components consume
58
+ ### What a complete portfolio shows (recommended defaults)
59
+
60
+ Defaults for a brief that says nothing about them; the prompt wins where it differs. Look at the
61
+ content before designing (how many collections, covers, project count, media mix) and design for
62
+ this portfolio, not for a stereotype of its field. Then, by default:
63
+
64
+ - **Home:** what the work is and one path into it in the first screen; real collections or
65
+ projects under truthful headings; a collection tile links to `/portfolio/<slug>` and shows that
66
+ collection's own cover.
67
+ - **Gallery (`/portfolio`):** a real collection card — cover, title — in the first screen;
68
+ loading, empty, and error states that look different.
69
+ - **Collection page:** the collection's title and description as the header, then only its
70
+ projects; a slug that resolves to nothing shows the not-found state alone — no header or empty
71
+ grid around it.
72
+ - **Project page:** title, description, and the `details` rows before the gallery; gallery items
73
+ in dashboard order, each through `GalleryMedia`, captions from `item.title` when present; on a
74
+ phone a gallery image is a bounded band, not a full-height hero.
75
+ - **Images:** every DTO image through `imgAttrs` (a `src`, a `srcSet`, and `sizes` in one spread;
76
+ an empty `imageUrl` gives `{}` — render your placeholder then).
77
+ - **Copy:** nothing the owner didn't supply — no invented clients, awards, or dates; no Wix IDs or
78
+ technical words in visible text.
79
+
80
+ ### The contracts your components consume (tested and work as they are; read when something is off or the brief wants more)
53
81
 
54
82
  ```ts
55
83
  // CollectionSummary (tiles): { id, slug, title, description, imageUrl /* "" when none */ }
@@ -73,62 +101,213 @@ Style everything with Tailwind utilities on the tokens.
73
101
  // { project: ProjectDetail|null, notFound,
74
102
  // items: GalleryItem[]|null, // dashboard order; null = loading
75
103
  // error }
104
+
105
+ // <GalleryMedia item={i} className? /> renders i as ONE element by kind (video+poster / img /
106
+ // nothing); <GalleryLink item={i}>…</GalleryLink> wraps in the owner's link when linkUrl is set.
107
+ ```
108
+
109
+ ### The pages and islands you create — skeletons
110
+
111
+ The class names here are the Astro/React spelling of layout rules that hold on every stack; on a
112
+ stack where the components don't deploy (`lib`, `static`, a port), keep the rule and write it in
113
+ your own CSS. The three pages ship with their frontmatter (fetch → DTO props → island, guarded
114
+ against an SSR throw, a real 404 on a missing slug) — keep it, and point the island import at
115
+ yours. Hooks first, branches after (an early return above a hook changes hook order between
116
+ renders and React throws). Render every state totally; nothing in a render path may throw.
117
+
118
+ ```tsx
119
+ // src/components/portfolio/Gallery.tsx — YOU build it; pages/portfolio.astro mounts it.
120
+ import { useCollections } from "../../hooks/portfolio/useCollections";
121
+ import { imgAttrs } from "../../wix/media";
122
+ import type { CollectionSummary } from "../../wix/portfolio/types";
123
+
124
+ export default function Gallery(props: { initialCollections?: CollectionSummary[] }) {
125
+ const { collections, error } = useCollections(props);
126
+ // …you implement the render:
127
+ // • error → a short inline message
128
+ // • collections === null → skeleton tiles; [] → your honest empty state
129
+ // • else YOUR grid of YOUR tiles: <a href={`/portfolio/${c.slug}`}> around the cover
130
+ // (<img {...imgAttrs(c.imageUrl, "(min-width: 1024px) 33vw, 50vw", 0.75)} alt={c.title} />,
131
+ // your placeholder when imageUrl is ""), title, description; the title WRAPS (`min-w-0`,
132
+ // `break-words`), never `truncate`
133
+ }
134
+ ```
135
+
136
+ ```tsx
137
+ // src/components/portfolio/CollectionPage.tsx — YOU build it; pages/portfolio/[slug].astro mounts it.
138
+ import { useCollectionProjects } from "../../hooks/portfolio/useCollectionProjects";
139
+ import type { CollectionSummary, ProjectSummary } from "../../wix/portfolio/types";
140
+
141
+ export default function CollectionPage(props: { slug: string; initialCollection?: CollectionSummary; initialProjects?: ProjectSummary[] }) {
142
+ const { collection, notFound, projects, error } = useCollectionProjects(props.slug, props);
143
+ // …you implement the render. Handle in order:
144
+ // notFound → the not-found state ALONE (no header, no empty grid around it)
145
+ // header: collection.title and description (a skeleton line while collection is null)
146
+ // error → inline message; projects === null → skeleton tiles; [] → your empty state
147
+ // else YOUR grid of YOUR project tiles linking to `/projects/${p.slug}` (cover via imgAttrs)
148
+ }
76
149
  ```
77
150
 
151
+ ```tsx
152
+ // src/components/portfolio/ProjectPage.tsx — YOU build it; pages/projects/[slug].astro mounts it.
153
+ import { useProjectDetail } from "../../hooks/portfolio/useProjectDetail";
154
+ import { GalleryLink, GalleryMedia } from "./GalleryMedia";
155
+ import { imgAttrs } from "../../wix/media";
156
+ import type { GalleryItem, ProjectDetail } from "../../wix/portfolio/types";
157
+
158
+ export default function ProjectPage(props: { slug: string; initialProject?: ProjectDetail; initialItems?: GalleryItem[] }) {
159
+ const { project, notFound, items, error } = useProjectDetail(props.slug, props);
160
+ // …you implement the render. Handle in order:
161
+ // notFound → "doesn't exist (anymore)"; !project → a loading placeholder
162
+ // else: title, description, then project.details as a definition list — a row with url is a
163
+ // link (target as given), else plain text; never invent a row
164
+ // items === null → a skeleton; [] → the cover (imgAttrs) when project.imageUrl, else an honest
165
+ // empty note; else each item as
166
+ // <figure><GalleryLink item={i}><GalleryMedia item={i} className="…" /></GalleryLink>
167
+ // {i.title && <figcaption>{i.title}</figcaption>}</figure> — never your own <img>/<video>
168
+ // branch (the shipped one owns kind, poster, and the unrenderable case)
169
+ // on a phone the media column is a bounded band (`max-h-[70vh]`, `object-contain`)
170
+ }
171
+ ```
172
+
173
+ ### The reference files for stacks where the components don't deploy
174
+
175
+ On `lib`, `static`, and a port, nothing under `components/` or `hooks/` arrives. The state machines
176
+ behind the hooks do arrive — `wix/portfolio/collections-store.ts`, `collection-projects-store.ts`,
177
+ `project-detail-store.ts` — so you never rewrite them: create a store per surface, `subscribe`,
178
+ render from `getState()`. Their `*State` interfaces are the render contract; read those. The one
179
+ rendering rule worth reading before writing your gallery:
180
+
181
+ 1. `components/portfolio/GalleryMedia.tsx` — the kind branch as working code, and the rule it
182
+ renders, `mediaElement(item)` in `wix/portfolio/portfolio-core.ts`, which ships on every stack:
183
+ it returns `{ tag: "video", attrs }`, `{ tag: "img", attrs }`, or null — create the element
184
+ from it (`document.createElement(el.tag)` + `setAttribute` per entry) instead of branching on
185
+ `kind` yourself.
186
+
187
+ All under `references/portfolio/app/`.
188
+
78
189
  ### Wiring — Astro (default)
79
190
 
80
191
  1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass — merge into the
81
192
  existing layout instead if another vertical is deployed).
82
- 2. Write your components under `src/components/portfolio/` (new names — don't overwrite the
83
- references), swap the island imports in `pages/portfolio.astro`,
84
- `pages/portfolio/[slug].astro`, and `pages/projects/[slug].astro`. Islands are
85
- `client:load` with the SSR props. **Author your surfaces in as few messages as possible** —
86
- batch multiple Writes per message.
193
+ 2. Write your islands under `src/components/portfolio/` (new names — don't overwrite the
194
+ references) per the skeletons above; swap the island imports in `pages/portfolio.astro`,
195
+ `pages/portfolio/[slug].astro`, and `pages/projects/[slug].astro`. Islands are `client:load`
196
+ with the SSR props. Author your surfaces in as few messages as possible — batch multiple Writes
197
+ per message.
87
198
  3. Write `pages/index.astro` (home) — it exists from the scaffold; Read it before overwriting.
88
199
 
200
+ ### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
201
+
202
+ Read the reference files listed above before writing any surface.
203
+
204
+ `deploy.mjs portfolio --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts`
205
+ (the visitor client, configured with the public client id), `media.ts`, `money.ts`, and
206
+ `wix/portfolio/` — `portfolio.ts`, `types.ts`, the `portfolio-core.ts` rules, and the three stores.
207
+ None of it is React. The hooks and components don't ship on this stack; the stores replace the
208
+ hooks, and you write the components in your framework to the contracts on this page:
209
+
210
+ - bind the stores with your framework's external-store primitive (Vue: `shallowRef` updated in
211
+ `subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
212
+ Solid: a signal set in `subscribe`). `createCollectionsStore(options)` per gallery,
213
+ `createCollectionProjectsStore(slug, options)` per collection page,
214
+ `createProjectDetailStore(slug, options)` per project page — `start()` when mounted, `stop()`
215
+ when unmounted. State in, exactly the hooks' contracts above;
216
+ - gallery items through `mediaElement(item)` from `portfolio-core.ts`; images through
217
+ `imgAttrs` from `media.ts`.
218
+
219
+ Routes `/portfolio`, `/portfolio/:slug` (`fetchCollectionBySlug`, null → your 404),
220
+ `/projects/:slug` (`fetchProjectBySlug`, null → your 404); dev server on 4321; a static build goes
221
+ through `npx @wix/cli@latest release` with `site.outputDirectory` pointing at the build folder, an
222
+ SSR build is hosted by you. Page titles and descriptions from the DTO (Portfolio has no SEO item type).
223
+
224
+ ### Wiring — static site (`--stack static`, no bundler)
225
+
226
+ Read the reference files listed above before writing any surface.
227
+
228
+ `deploy.mjs portfolio --stack static --out site` put the REST layer in `site/js/wix/` (browser ESM,
229
+ the `.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` — pages,
230
+ styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project root
231
+ (config, plan, seed output) is never the upload. Same function names and DTOs as the table above,
232
+ so the contracts on this page hold unchanged: `fetchCollections`, `fetchCollectionBySlug`,
233
+ `fetchProjects`, `fetchProjectBySlug`, `fetchProjectGallery` from `./js/wix/portfolio.js`. The
234
+ state machines ship too: `createCollectionsStore` from `./js/wix/collections-store.js`,
235
+ `createCollectionProjectsStore` from `./js/wix/collection-projects-store.js`,
236
+ `createProjectDetailStore` from `./js/wix/project-detail-store.js` (`start()` once the page is up;
237
+ `notFound` is the 404 signal), `mediaElement` from `./js/wix/portfolio-core.js` (the gallery item
238
+ → element rule), and `imgAttrs` from `./js/wix/media.js` (attribute names as HTML spells them —
239
+ `setAttribute` per entry). No components ship — you write the rendering in plain JS: one render
240
+ function per surface that reads `getState()`, called from `subscribe`. Wix static hosting serves
241
+ files, not directories: pages are `portfolio.html`, `collection.html?slug=…`,
242
+ `project.html?slug=…`; name the file and link to it. Set `document.title` and the meta description
243
+ from the DTO (`collection.title`/`description`, `project.title`/`description`) once the store
244
+ populates — Portfolio has no `seoData`. The visitor token persists in `localStorage` on its own;
245
+ never mint per page. `npx @wix/cli@latest release` uploads `site/`.
246
+
247
+ ### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
248
+
249
+ Read the reference files listed above before writing any surface.
250
+
251
+ Run `deploy.mjs portfolio --stack static` in the project folder anyway: `js/wix/` is the readable
252
+ spec. This vertical is reads only, so everything renders on the server: port `js/wix/portfolio.ts`
253
+ and `portfolio-core.ts` to your language — the same five functions returning the same DTO shapes
254
+ as dicts, one anonymous visitor token per process (mint and refresh per `client.ts`) — and render
255
+ gallery, collection, and project pages in your templates to the contracts above, so titles are in
256
+ the HTML; `<title>`/`<meta>` from the DTO. Carry the URLs and bodies over as they are (the list
257
+ calls are `GET` with `paging.limit`; the slug lookups `POST …/query` with
258
+ `{ query: { filter: { slug }, cursorPaging: { limit: 1 } } }`). The gallery's kind rule is
259
+ `mediaElement` — port it, don't re-derive it. Nothing here runs in the browser unless you want
260
+ client-side loading; then the static wiring above applies. Routes stay `/portfolio`,
261
+ `/portfolio/<slug>`, `/projects/<slug>`.
262
+
263
+ **Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for
264
+ the reads, run at build time with one anonymous token; the generator must emit a page for every
265
+ collection (`fetchCollections()`) and every project (`fetchProjects()`) — both return one page of
266
+ up to 100 in list order, the same as the SDK path; a portfolio larger than that walks
267
+ `pagingMetadataV2.cursors.next` on the list response (add it to your port when you hit it). Run
268
+ `deploy.mjs portfolio --stack static --out <build dir>` only if a page loads anything client-side;
269
+ point `site.outputDirectory` at the build folder, `wix release`. Pages sit at different depths
270
+ (`/`, `/portfolio/…`, `/projects/…`): give the templates one base path to any `js/wix/` import (a
271
+ template variable, or root-relative `/js/wix/…`), never a relative `./js/wix/` — it breaks one
272
+ level down. Close with the live URL, the rebuild + release command, and one line for the owner:
273
+ dashboard edits reach the site when that command runs.
274
+
89
275
  ### Wiring — React SPA (Vite etc.)
90
276
 
91
- Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
92
- plugins — deploy added the dep). Routes: `/portfolio` → your gallery; `/portfolio/:slug` →
93
- your collection page on `useCollectionProjects(slug)`; `/projects/:slug` → your detail
94
- surface on `useProjectDetail(slug)`.
277
+ Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite plugins —
278
+ deploy added the dep). Routes: `/portfolio` → your gallery on `useCollections()`; `/portfolio/:slug`
279
+ → your collection page on `useCollectionProjects(slug)`; `/projects/:slug` → your detail surface on
280
+ `useProjectDetail(slug)` (the hooks fetch client-side when no `initial*` is passed; route the 404
281
+ view off `notFound`). Deploy wrote the public client id into `wix/config.ts`; nothing else to configure.
95
282
 
96
283
  ## Hard rules
97
284
 
98
285
  - **Reads only through the shipped exports** — they filter `hidden`, sort collections by the
99
286
  owner's `sortOrder` (projects have none — list order), and resolve every `wix:image://` /
100
- `wix:video://` value. Never hand-build a `wixstatic.com` URL, never render a raw media
101
- string into `src`.
287
+ `wix:video://` value. Never hand-build a `wixstatic.com` URL, never render a raw media string
288
+ into `src`.
102
289
  - **This vertical sells nothing** — no cart, no checkout, no `@wix/ecom`/`@wix/redirects`
103
290
  imports. A "contact about this project" CTA is a link, not a purchase flow.
104
- - **Route not-found off `notFound`**, and surface `error` — a transient `null` is loading,
105
- not a 404.
291
+ - **Route not-found off `notFound`**, and surface `error` — a transient `null` is loading, not a 404.
106
292
  - **Render `details` rows as given** (text or link per row); don't invent metadata.
107
- - **Gallery items branch on `kind`** — an image never gets a `<video>` tag and vice versa;
108
- nothing renderable → skip the item, never a broken tag.
109
- - Theme via the `@theme` tokens; no parallel theme files, no hardcoded palettes.
293
+ - **Gallery items render through `GalleryMedia`** (or `mediaElement` where it doesn't deploy) —
294
+ never your own branch on `kind`.
295
+ - Where the shipped components deploy (Astro, React): theme via the `@theme` tokens, and your
296
+ markup uses Tailwind utilities on the same tokens — one design system across shipped and written
297
+ code. No parallel theme files, no hardcoded palette values. Where they don't (`lib`, `static`, a
298
+ port): style with whatever your stack does well, on one token set of your own.
110
299
  - Live data or an honest empty state — never mock collections, projects, or media.
300
+ - **Call every hook before any conditional return.** Hooks first, branches after.
111
301
 
112
302
  ## Point the user to their dashboard
113
303
 
114
304
  Give the owner the dashboard link — the deploy step's JSON printed `dashboardUrl`; append
115
- `/wix-portfolio/projects` for Portfolio management (Projects and Collections are tabs on that
116
- one page: Projects adds projects and their media galleries; Collections groups them).
305
+ `/wix-portfolio/projects` for Portfolio management (Projects and Collections are tabs on that one
306
+ page: Projects adds projects and their media galleries; Collections groups them).
117
307
 
118
308
  ## Seeding
119
309
 
120
- Per `seed/SEED.md` — plain-data `plan.json` into `seed-portfolio.mjs` from the project root.
121
- Seed collections + projects that exercise the UI (covers on everything, 2–3 gallery images
122
- per project, `details` rows on a couple of projects). A fresh install ships sample content —
123
- seeding never deletes it; ask the owner before any cleanup.
124
-
125
- ## Verify (before declaring done)
126
-
127
- - [ ] `/portfolio` renders live collections SSR (view-source shows titles) through YOUR
128
- components; empty portfolio shows your honest empty state.
129
- - [ ] A collection page lists its projects (and only its); an unknown slug 404s.
130
- - [ ] A project page renders the gallery in order, the `details` rows, and — if any project
131
- has a video item — a playing `<video>` with poster.
132
- - [ ] Card/grid/detail/home are YOUR designs on the tokens; data-layer/hook files unedited.
133
- - [ ] No `@wix/ecom` import anywhere; no mocked content.
134
- - [ ] Dashboard link handed to the owner.
310
+ Per `seed/SEED.md` — plain-data `plan.json` into `seed-portfolio.mjs` from the project root. Seed
311
+ collections + projects that exercise the UI (covers on everything, 2–3 gallery images per project,
312
+ `details` rows on a couple of projects). A fresh install ships sample content — seeding never
313
+ deletes it; ask the owner before any cleanup.
@@ -3,6 +3,7 @@
3
3
  // useCollectionProjects (ProjectCard is reusable for an all-work grid or a home strip).
4
4
  import type { ComponentType, ReactNode } from "react";
5
5
  import { useCollectionProjects } from "../../hooks/portfolio/useCollectionProjects";
6
+ import { imgAttrs } from "../../wix/media";
6
7
  import type { CollectionSummary, ProjectSummary } from "../../wix/portfolio/types";
7
8
 
8
9
  export interface LinkLikeProps {
@@ -33,9 +34,8 @@ export function ProjectCard({
33
34
  <div className="relative aspect-[4/3] overflow-hidden rounded-lg bg-secondary">
34
35
  {project.imageUrl && (
35
36
  <img
36
- src={project.imageUrl}
37
+ {...imgAttrs(project.imageUrl, "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw", 0.75)}
37
38
  alt={project.title}
38
- loading="lazy"
39
39
  className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
40
40
  />
41
41
  )}
@@ -2,6 +2,7 @@
2
2
  // Correct and complete; per the skill's model you design and build your own on useCollections.
3
3
  import type { ComponentType, ReactNode } from "react";
4
4
  import { useCollections } from "../../hooks/portfolio/useCollections";
5
+ import { imgAttrs } from "../../wix/media";
5
6
  import type { CollectionSummary } from "../../wix/portfolio/types";
6
7
 
7
8
  export interface LinkLikeProps {
@@ -32,9 +33,8 @@ export function CollectionCard({
32
33
  <div className="relative aspect-[4/3] overflow-hidden rounded-lg bg-secondary">
33
34
  {collection.imageUrl && (
34
35
  <img
35
- src={collection.imageUrl}
36
+ {...imgAttrs(collection.imageUrl, "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw", 0.75)}
36
37
  alt={collection.title}
37
- loading="lazy"
38
38
  className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
39
39
  />
40
40
  )}
@@ -0,0 +1,35 @@
1
+ // SHIPPED — wire as-is. One gallery item as its one element: a <video> with poster for a video
2
+ // item, an <img> for an image item, null when nothing is renderable (never a broken tag, never
3
+ // an <img> for a video). The branching rule is `mediaElement` in wix/portfolio/portfolio-core.ts,
4
+ // shared with every other stack; this is its React spelling. Wrapped in a link when the owner
5
+ // attached one. `className` styles the media element itself.
6
+ import type { ReactNode } from "react";
7
+ import { mediaElement } from "../../wix/portfolio/portfolio-core";
8
+ import type { GalleryItem } from "../../wix/portfolio/types";
9
+
10
+ export interface GalleryMediaProps {
11
+ item: GalleryItem;
12
+ className?: string;
13
+ }
14
+
15
+ export function GalleryMedia({ item, className }: GalleryMediaProps) {
16
+ const el = mediaElement(item);
17
+ if (!el) return null;
18
+ if (el.tag === "video") {
19
+ return <video src={el.attrs.src} poster={el.attrs.poster} controls playsInline className={className} />;
20
+ }
21
+ return <img src={el.attrs.src} alt={el.attrs.alt} loading="lazy" decoding="async" className={className} />;
22
+ }
23
+
24
+ /** The item's outbound link when the owner set one, else the children as they are. */
25
+ export function GalleryLink({ item, children }: { item: GalleryItem; children: ReactNode }) {
26
+ return item.linkUrl ? (
27
+ <a href={item.linkUrl} target={item.linkTarget ?? undefined} rel="noopener">
28
+ {children}
29
+ </a>
30
+ ) : (
31
+ <>{children}</>
32
+ );
33
+ }
34
+
35
+ export default GalleryMedia;
@@ -1,41 +1,11 @@
1
1
  // REFERENCE project detail: header, details[] rows (text or link), media gallery (image AND
2
2
  // video items) on the @theme tokens. Correct and complete; per the skill's model you design
3
- // and build your own on useProjectDetail. GalleryMedia's kind branching is the load-bearing
4
- // part — keep it (or its logic) in whatever you build.
5
- import type { ReactNode } from "react";
3
+ // and build your own on useProjectDetail. The gallery items render through the SHIPPED
4
+ // GalleryMedia (kind branching, poster, link wrap) — keep that in whatever you build.
6
5
  import { useProjectDetail } from "../../hooks/portfolio/useProjectDetail";
6
+ import { imgAttrs } from "../../wix/media";
7
7
  import type { GalleryItem, ProjectDetail } from "../../wix/portfolio/types";
8
-
9
- /** One gallery item: image, or video with poster. Nothing renderable → null (never a broken tag). */
10
- export function GalleryMedia({ item }: { item: GalleryItem }) {
11
- if (item.kind === "video" && item.videoUrl) {
12
- return (
13
- <video
14
- src={item.videoUrl}
15
- poster={item.imageUrl || undefined}
16
- controls
17
- playsInline
18
- className="block w-full rounded-lg bg-secondary"
19
- />
20
- );
21
- }
22
- if (item.imageUrl) {
23
- return (
24
- <img src={item.imageUrl} alt={item.title} loading="lazy" className="block w-full rounded-lg" />
25
- );
26
- }
27
- return null;
28
- }
29
-
30
- function MaybeLink({ item, children }: { item: GalleryItem; children: ReactNode }) {
31
- return item.linkUrl ? (
32
- <a href={item.linkUrl} target={item.linkTarget ?? undefined} rel="noopener">
33
- {children}
34
- </a>
35
- ) : (
36
- <>{children}</>
37
- );
38
- }
8
+ import { GalleryLink, GalleryMedia } from "./GalleryMedia";
39
9
 
40
10
  export interface ProjectDetailViewProps {
41
11
  slug: string;
@@ -92,7 +62,7 @@ export default function ProjectDetailView({ slug, initialProject, initialItems }
92
62
  ) : items.length === 0 ? (
93
63
  // No gallery items — the cover is the project's only real media; text-only otherwise.
94
64
  project.imageUrl ? (
95
- <img src={project.imageUrl} alt={project.title} className="block w-full rounded-lg" />
65
+ <img {...imgAttrs(project.imageUrl, "(min-width: 1024px) 64rem, 100vw", 0.75)} alt={project.title} className="block w-full rounded-lg" />
96
66
  ) : (
97
67
  <p className="py-16 text-center text-muted-foreground">No media in this project yet.</p>
98
68
  )
@@ -100,9 +70,9 @@ export default function ProjectDetailView({ slug, initialProject, initialItems }
100
70
  <div className="flex flex-col gap-6">
101
71
  {items.map((item) => (
102
72
  <figure key={item.id} className="m-0">
103
- <MaybeLink item={item}>
104
- <GalleryMedia item={item} />
105
- </MaybeLink>
73
+ <GalleryLink item={item}>
74
+ <GalleryMedia item={item} className="block w-full rounded-lg bg-secondary" />
75
+ </GalleryLink>
106
76
  {item.title && (
107
77
  <figcaption className="mt-1.5 text-sm text-muted-foreground">{item.title}</figcaption>
108
78
  )}
@@ -1,59 +1,32 @@
1
- // One collection's header + its projects, by slug. SSR-friendly: pass both `initial*` and no
2
- // client fetch happens. `notFound` is the real not-found signal (collection stays null while
3
- // loading too) — route a 404 off it, never off a transient null.
4
- import { useEffect, useState } from "react";
5
- import { fetchCollectionBySlug, fetchProjects } from "../../wix/portfolio/portfolio";
6
- import type { CollectionSummary, ProjectSummary } from "../../wix/portfolio/types";
1
+ // React binding of the collection-projects store (wix/portfolio/collection-projects-store.ts) —
2
+ // slug → collection header + its projects lives there, framework-free; this hook subscribes to
3
+ // one instance per (slug, mount) and recreates it when the slug changes. SSR-friendly: pass both
4
+ // `initial*` and no client fetch happens. `notFound` is the real not-found signal (collection
5
+ // stays null while loading too) — route a 404 off it, never off a transient null.
6
+ import { useEffect, useRef, useSyncExternalStore } from "react";
7
+ import {
8
+ createCollectionProjectsStore,
9
+ type CollectionProjectsState,
10
+ type CollectionProjectsStore,
11
+ type CollectionProjectsStoreOptions,
12
+ } from "../../wix/portfolio/collection-projects-store";
7
13
 
8
- export interface UseCollectionProjectsOptions {
9
- initialCollection?: CollectionSummary;
10
- initialProjects?: ProjectSummary[];
11
- }
14
+ export type UseCollectionProjectsOptions = CollectionProjectsStoreOptions;
12
15
 
13
- export interface UseCollectionProjects {
14
- /** null while loading AND when not found — check `notFound` to tell them apart. */
15
- collection: CollectionSummary | null;
16
- notFound: boolean;
17
- /** null while the first load is in flight — render skeletons, not an empty state. */
18
- projects: ProjectSummary[] | null;
19
- error: string | null;
20
- }
16
+ export type UseCollectionProjects = CollectionProjectsState;
21
17
 
22
18
  export function useCollectionProjects(
23
19
  slug: string,
24
20
  { initialCollection, initialProjects }: UseCollectionProjectsOptions = {},
25
21
  ): UseCollectionProjects {
26
- const [collection, setCollection] = useState<CollectionSummary | null>(initialCollection ?? null);
27
- const [notFound, setNotFound] = useState(false);
28
- const [projects, setProjects] = useState<ProjectSummary[] | null>(initialProjects ?? null);
29
- const [error, setError] = useState<string | null>(null);
30
-
22
+ const ref = useRef<{ slug: string; store: CollectionProjectsStore } | null>(null);
23
+ if (!ref.current || ref.current.slug !== slug) {
24
+ ref.current = { slug, store: createCollectionProjectsStore(slug, { initialCollection, initialProjects }) };
25
+ }
26
+ const store = ref.current.store;
31
27
  useEffect(() => {
32
- if (initialCollection && initialProjects) return;
33
- let alive = true;
34
- (async () => {
35
- try {
36
- const col = initialCollection ?? (await fetchCollectionBySlug(slug));
37
- if (!alive) return;
38
- if (!col) {
39
- setNotFound(true);
40
- setProjects([]);
41
- return;
42
- }
43
- setCollection(col);
44
- const projs = await fetchProjects({ collectionId: col.id });
45
- if (alive) setProjects(projs);
46
- } catch (e) {
47
- if (!alive) return;
48
- setProjects([]);
49
- setError(e instanceof Error ? e.message : String(e));
50
- }
51
- })();
52
- return () => {
53
- alive = false;
54
- };
55
- // eslint-disable-next-line react-hooks/exhaustive-deps
56
- }, [slug]);
57
-
58
- return { collection, notFound, projects, error };
28
+ store.start();
29
+ return () => store.stop();
30
+ }, [store]);
31
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
59
32
  }