@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.
- package/package.json +1 -1
- package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
- package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
- package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
- package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
- package/skills/wix-headless/references/CAPABILITIES.md +2 -2
- package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
- package/skills/wix-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +28 -31
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +55 -1
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +371 -81
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +136 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +53 -136
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +73 -230
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -5
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
- package/skills/wix-manage/SKILL.md +4 -1
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-manage/references/events/create-wix-event.md +46 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
- package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
- package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +19 -4
- package/skills/wix-vibe-headless/platforms/generic.md +6 -0
- package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
- package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
- package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
|
@@ -1,39 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
}
|
package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts
CHANGED
|
@@ -1,58 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
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
|
|
8
|
-
initialProject?: ProjectDetail;
|
|
9
|
-
initialItems?: GalleryItem[];
|
|
10
|
-
}
|
|
13
|
+
export type UseProjectDetailOptions = ProjectDetailStoreOptions;
|
|
11
14
|
|
|
12
|
-
export
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
}
|
package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts
ADDED
|
@@ -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
|
|
2
|
-
// Read-only vertical: no cart, no checkout, no @wix/ecom. Everything
|
|
3
|
-
// DTO from ./types.
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|