@wix/agent-skills 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +14 -112
- package/skills/wix-app/references/APP_TOOLS.md +2 -1
- package/skills/wix-app/references/BACKEND_EVENT.md +3 -3
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +41 -152
- package/skills/wix-app/references/DASHBOARD_MODAL.md +4 -4
- package/skills/wix-app/references/SDK_MODULE_MAP.md +44 -0
- package/skills/wix-app/references/SITE_PLUGIN.md +36 -190
- package/skills/wix-app/references/app-tools/TOOLS.md +3 -1
- package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +21 -0
- package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +17 -0
- package/skills/wix-app/references/dashboard-plugin/SLOTS.md +4 -18
- package/skills/wix-app/references/site-plugin/EXAMPLES.md +1 -1
- package/skills/wix-headless-fast/SKILL.md +302 -51
- package/skills/wix-headless-fast/entry/skill.md +11 -2
- package/skills/wix-headless-fast/install/AGENTS.md +32 -0
- package/skills/wix-headless-fast/install/agents-md.mjs +39 -0
- package/skills/wix-headless-fast/install/attach.mjs +360 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/setup.mjs +239 -0
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +226 -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/blog/seed/read-site.mjs +22 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +181 -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/bookings/seed/read-site.mjs +28 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +238 -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/cms/seed/read-site.mjs +20 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +241 -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/events/seed/read-site.mjs +26 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +216 -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/read-site.mjs +20 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +316 -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/members/seed/read-site.mjs +15 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +240 -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/portfolio/seed/read-site.mjs +20 -0
- package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +19 -21
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +214 -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/pricing-plans/seed/read-site.mjs +21 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +242 -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/restaurants/seed/read-site.mjs +25 -0
- package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +25 -5
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
- 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/shared/seed/read-site.mjs +99 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +186 -63
- 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 +29 -58
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
- 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 +49 -162
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +314 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
- 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 +2 -0
- 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 +31 -9
- package/skills/wix-headless-fast/references/storefront/seed/read-site.mjs +38 -0
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +57 -50
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-coupon.md +10 -19
- package/skills/wix-manage/references/events/create-wix-event.md +5 -1
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
- 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/fast-path.mjs +0 -186
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// build your own on usePost. Chips display .label; the body renders ONLY via RichContent.
|
|
4
4
|
import type { ComponentType } from "react";
|
|
5
5
|
import { usePost } from "../../hooks/blog/usePost";
|
|
6
|
+
import { postMetaLine } from "../../wix/blog/posts";
|
|
6
7
|
import type { BlogCategory, BlogTag, PostDetail } from "../../wix/blog/types";
|
|
7
8
|
import RichContent from "./RichContent";
|
|
8
9
|
import type { LinkLikeProps } from "./BlogFeedView";
|
|
@@ -81,9 +82,7 @@ export default function PostView({
|
|
|
81
82
|
)}
|
|
82
83
|
<h1 className="mt-2 text-3xl font-semibold tracking-tight">{post.title}</h1>
|
|
83
84
|
<p className="mt-3 text-sm text-muted-foreground">
|
|
84
|
-
|
|
85
|
-
{post.dateLabel && post.minutesToRead > 0 ? " · " : ""}
|
|
86
|
-
{post.minutesToRead > 0 ? `${post.minutesToRead} min read` : ""}
|
|
85
|
+
<time dateTime={post.dateISO}>{postMetaLine(post)}</time>
|
|
87
86
|
</p>
|
|
88
87
|
{post.coverUrl && (
|
|
89
88
|
<div className="mt-6 aspect-[16/9] overflow-hidden rounded-xl bg-secondary">
|
|
@@ -1,123 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// React binding of the blog feed store (wix/blog/blog-feed-store.ts) — the feed state machine
|
|
2
|
+
// (taxonomy filter, cursor paging) lives there, framework-free; this hook subscribes to one
|
|
3
|
+
// instance per mounted feed and exposes its state and actions under one name. Astro islands and
|
|
4
|
+
// React SPAs use this; a static page, Vue, or Svelte uses the store directly.
|
|
5
|
+
//
|
|
6
|
+
// SSR-friendly: pass server-fetched data as `initial*` (Astro frontmatter / server component) and
|
|
7
|
+
// no client fetch happens; a SPA passes nothing. Category/tag filtering fetches live
|
|
8
|
+
// (server-side-filtered); the two filters are mutually exclusive — setting one clears the other.
|
|
9
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
10
|
+
import { createBlogFeedStore, type BlogFeedState, type BlogFeedStore, type BlogFeedStoreOptions } from "../../wix/blog/blog-feed-store";
|
|
9
11
|
|
|
10
|
-
export
|
|
11
|
-
initialPage?: PostPage;
|
|
12
|
-
initialCategories?: BlogCategory[];
|
|
13
|
-
initialTags?: BlogTag[];
|
|
14
|
-
pageSize?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface UseBlogFeed {
|
|
18
|
-
/** null while the first load is in flight — render skeletons, not an empty state. */
|
|
19
|
-
posts: PostSummary[] | null;
|
|
20
|
-
categories: BlogCategory[];
|
|
21
|
-
tags: BlogTag[];
|
|
22
|
-
activeCategoryId: string | null;
|
|
23
|
-
setActiveCategoryId: (id: string | null) => void;
|
|
24
|
-
activeTagId: string | null;
|
|
25
|
-
setActiveTagId: (id: string | null) => void;
|
|
26
|
-
/** True when another page exists — render a "load more" control on it. */
|
|
27
|
-
hasMore: boolean;
|
|
28
|
-
loadMore: () => void;
|
|
29
|
-
loadingMore: boolean;
|
|
30
|
-
error: string | null;
|
|
31
|
-
}
|
|
12
|
+
export type UseBlogFeedOptions = BlogFeedStoreOptions;
|
|
32
13
|
|
|
33
|
-
export
|
|
34
|
-
initialPage,
|
|
35
|
-
initialCategories,
|
|
36
|
-
initialTags,
|
|
37
|
-
pageSize = 20,
|
|
38
|
-
}: UseBlogFeedOptions = {}): UseBlogFeed {
|
|
39
|
-
const [posts, setPosts] = useState<PostSummary[] | null>(initialPage?.posts ?? null);
|
|
40
|
-
const [nextCursor, setNextCursor] = useState<string | null>(initialPage?.nextCursor ?? null);
|
|
41
|
-
const [categories, setCategories] = useState<BlogCategory[]>(initialCategories ?? []);
|
|
42
|
-
const [tags, setTags] = useState<BlogTag[]>(initialTags ?? []);
|
|
43
|
-
const [activeCategoryId, setCategoryId] = useState<string | null>(null);
|
|
44
|
-
const [activeTagId, setTagId] = useState<string | null>(null);
|
|
45
|
-
const [loadingMore, setLoadingMore] = useState(false);
|
|
46
|
-
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
export type UseBlogFeed = BlogFeedState & Pick<BlogFeedStore, "setActiveCategoryId" | "setActiveTagId" | "loadMore">;
|
|
47
15
|
|
|
16
|
+
export function useBlogFeed(options: UseBlogFeedOptions = {}): UseBlogFeed {
|
|
17
|
+
const ref = useRef<BlogFeedStore | null>(null);
|
|
18
|
+
if (!ref.current) ref.current = createBlogFeedStore(options);
|
|
19
|
+
const store = ref.current;
|
|
48
20
|
useEffect(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!initialTags) {
|
|
54
|
-
fetchBlogTags().then((t) => alive && setTags(t));
|
|
55
|
-
}
|
|
56
|
-
return () => {
|
|
57
|
-
alive = false;
|
|
58
|
-
};
|
|
59
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
-
}, []);
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
// The unfiltered first page may have come from the server; only fetch when it didn't,
|
|
64
|
-
// or when a filter is active (filters are server-side).
|
|
65
|
-
if (activeCategoryId === null && activeTagId === null && initialPage) {
|
|
66
|
-
setPosts(initialPage.posts);
|
|
67
|
-
setNextCursor(initialPage.nextCursor);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
let alive = true;
|
|
71
|
-
setPosts(null);
|
|
72
|
-
setError(null);
|
|
73
|
-
fetchPosts({ limit: pageSize, categoryId: activeCategoryId, tagId: activeTagId })
|
|
74
|
-
.then((page) => {
|
|
75
|
-
if (!alive) return;
|
|
76
|
-
setPosts(page.posts);
|
|
77
|
-
setNextCursor(page.nextCursor);
|
|
78
|
-
})
|
|
79
|
-
.catch((e) => {
|
|
80
|
-
if (!alive) return;
|
|
81
|
-
setPosts([]);
|
|
82
|
-
setNextCursor(null);
|
|
83
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
84
|
-
});
|
|
85
|
-
return () => {
|
|
86
|
-
alive = false;
|
|
87
|
-
};
|
|
88
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
|
-
}, [activeCategoryId, activeTagId, pageSize]);
|
|
90
|
-
|
|
91
|
-
function loadMore(): void {
|
|
92
|
-
if (!nextCursor || loadingMore) return;
|
|
93
|
-
setLoadingMore(true);
|
|
94
|
-
setError(null);
|
|
95
|
-
fetchPosts({ limit: pageSize, cursor: nextCursor })
|
|
96
|
-
.then((page) => {
|
|
97
|
-
setPosts((prev) => [...(prev ?? []), ...page.posts]);
|
|
98
|
-
setNextCursor(page.nextCursor);
|
|
99
|
-
})
|
|
100
|
-
.catch((e) => setError(e instanceof Error ? e.message : String(e)))
|
|
101
|
-
.finally(() => setLoadingMore(false));
|
|
102
|
-
}
|
|
103
|
-
|
|
21
|
+
store.start();
|
|
22
|
+
return () => store.stop();
|
|
23
|
+
}, [store]);
|
|
24
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
104
25
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
setActiveCategoryId: (id) => {
|
|
110
|
-
setCategoryId(id);
|
|
111
|
-
setTagId(null);
|
|
112
|
-
},
|
|
113
|
-
activeTagId,
|
|
114
|
-
setActiveTagId: (id) => {
|
|
115
|
-
setTagId(id);
|
|
116
|
-
setCategoryId(null);
|
|
117
|
-
},
|
|
118
|
-
hasMore: nextCursor !== null,
|
|
119
|
-
loadMore,
|
|
120
|
-
loadingMore,
|
|
121
|
-
error,
|
|
26
|
+
...state,
|
|
27
|
+
setActiveCategoryId: store.setActiveCategoryId,
|
|
28
|
+
setActiveTagId: store.setActiveTagId,
|
|
29
|
+
loadMore: store.loadMore,
|
|
122
30
|
};
|
|
123
31
|
}
|
|
@@ -1,70 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import type { BlogCategory, BlogTag, PostDetail } from "../../wix/blog/types";
|
|
1
|
+
// React binding of the post store (wix/blog/post-store.ts) — post-by-slug loading and the resolved
|
|
2
|
+
// category/tag chips live there, framework-free; this hook subscribes to one instance per slug.
|
|
3
|
+
//
|
|
4
|
+
// SSR-friendly: pass the server-fetched post (and taxonomy) as `initial*` and no client fetch
|
|
5
|
+
// happens; a SPA passes only the slug. A slug change creates a fresh store and refetches.
|
|
6
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
7
|
+
import { createPostStore, type PostState, type PostStore, type PostStoreOptions } from "../../wix/blog/post-store";
|
|
9
8
|
|
|
10
|
-
export
|
|
11
|
-
slug: string;
|
|
12
|
-
initialPost?: PostDetail;
|
|
13
|
-
initialCategories?: BlogCategory[];
|
|
14
|
-
initialTags?: BlogTag[];
|
|
15
|
-
}
|
|
9
|
+
export type UsePostOptions = PostStoreOptions;
|
|
16
10
|
|
|
17
|
-
export
|
|
18
|
-
/** null while loading (render a skeleton) AND when not found — disambiguate via notFound. */
|
|
19
|
-
post: PostDetail | null;
|
|
20
|
-
/** True once the slug definitively resolved to nothing — render a not-found state. */
|
|
21
|
-
notFound: boolean;
|
|
22
|
-
/** This post's categories/tags, resolved to full objects (display .label, route by .slug). */
|
|
23
|
-
categories: BlogCategory[];
|
|
24
|
-
tags: BlogTag[];
|
|
25
|
-
error: string | null;
|
|
26
|
-
}
|
|
11
|
+
export type UsePost = PostState;
|
|
27
12
|
|
|
28
13
|
export function usePost({ slug, initialPost, initialCategories, initialTags }: UsePostOptions): UsePost {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const [allTags, setAllTags] = useState<BlogTag[]>(initialTags ?? []);
|
|
33
|
-
const [error, setError] = useState<string | null>(null);
|
|
34
|
-
|
|
14
|
+
const ref = useRef<{ slug: string; store: PostStore } | null>(null);
|
|
15
|
+
if (!ref.current || ref.current.slug !== slug) ref.current = { slug, store: createPostStore({ slug, initialPost, initialCategories, initialTags }) };
|
|
16
|
+
const store = ref.current.store;
|
|
35
17
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
fetchPostBySlug(slug)
|
|
41
|
-
.then((p) => {
|
|
42
|
-
if (!alive) return;
|
|
43
|
-
if (p) setPost(p);
|
|
44
|
-
else setNotFound(true);
|
|
45
|
-
})
|
|
46
|
-
.catch((e) => alive && setError(e instanceof Error ? e.message : String(e)));
|
|
47
|
-
}
|
|
48
|
-
if (!initialCategories) {
|
|
49
|
-
fetchBlogCategories().then((c) => alive && setAllCategories(c));
|
|
50
|
-
}
|
|
51
|
-
if (!initialTags) {
|
|
52
|
-
fetchBlogTags().then((t) => alive && setAllTags(t));
|
|
53
|
-
}
|
|
54
|
-
return () => {
|
|
55
|
-
alive = false;
|
|
56
|
-
};
|
|
57
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
-
}, [slug]);
|
|
59
|
-
|
|
60
|
-
const categories = useMemo(
|
|
61
|
-
() => (post ? allCategories.filter((c) => post.categoryIds.includes(c.id)) : []),
|
|
62
|
-
[post, allCategories],
|
|
63
|
-
);
|
|
64
|
-
const tags = useMemo(
|
|
65
|
-
() => (post ? allTags.filter((t) => post.tagIds.includes(t.id)) : []),
|
|
66
|
-
[post, allTags],
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
return { post, notFound, categories, tags, error };
|
|
18
|
+
store.start();
|
|
19
|
+
return () => store.stop();
|
|
20
|
+
}, [store]);
|
|
21
|
+
return useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
70
22
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// The blog feed as a framework-free store — the logic behind useBlogFeed, usable from React
|
|
2
|
+
// (useBlogFeed wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and
|
|
3
|
+
// render), or as the specification for a port. State, actions, subscribe/getState, emit after every
|
|
4
|
+
// change — the same shape as storefront's shop-store.
|
|
5
|
+
//
|
|
6
|
+
// Category and tag filters are applied by Wix (fetchPosts), never to a page already in hand; the
|
|
7
|
+
// two are mutually exclusive — setting one clears the other. A changed filter starts a fresh cursor
|
|
8
|
+
// chain; a late response from a superseded query is dropped. Paging appends by cursor.
|
|
9
|
+
//
|
|
10
|
+
// SSR-friendly: seed with `initialPage` (and `initialCategories`/`initialTags`) and the first,
|
|
11
|
+
// unfiltered page renders at once with no client fetch; a SPA passes nothing and `start()` fetches.
|
|
12
|
+
// One store per mounted feed (a page can hold a feed and a "latest posts" rail): createBlogFeedStore().
|
|
13
|
+
import { fetchPosts } from "./posts";
|
|
14
|
+
import { fetchBlogCategories, fetchBlogTags } from "./taxonomy";
|
|
15
|
+
import type { BlogCategory, BlogTag, PostPage, PostSummary } from "./types";
|
|
16
|
+
|
|
17
|
+
export interface BlogFeedStoreOptions {
|
|
18
|
+
initialPage?: PostPage;
|
|
19
|
+
initialCategories?: BlogCategory[];
|
|
20
|
+
initialTags?: BlogTag[];
|
|
21
|
+
pageSize?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Everything a feed surface renders from. Read it with getState() or through a subscription. */
|
|
25
|
+
export interface BlogFeedState {
|
|
26
|
+
/** null while the first load is in flight — render skeletons, not an empty state. */
|
|
27
|
+
posts: PostSummary[] | null;
|
|
28
|
+
categories: BlogCategory[];
|
|
29
|
+
tags: BlogTag[];
|
|
30
|
+
activeCategoryId: string | null;
|
|
31
|
+
activeTagId: string | null;
|
|
32
|
+
/** True when another page exists — render a "load more" control on it. */
|
|
33
|
+
hasMore: boolean;
|
|
34
|
+
loadingMore: boolean;
|
|
35
|
+
error: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BlogFeedStore {
|
|
39
|
+
getState(): BlogFeedState;
|
|
40
|
+
subscribe(listener: () => void): () => void;
|
|
41
|
+
/** Fetch the taxonomy (unless seeded) and the first page (unless seeded and unfiltered). Call once when mounted. */
|
|
42
|
+
start(): void;
|
|
43
|
+
/** Stop reacting; drop late responses. */
|
|
44
|
+
stop(): void;
|
|
45
|
+
/** Server-side filter; clears the tag filter. */
|
|
46
|
+
setActiveCategoryId(id: string | null): void;
|
|
47
|
+
/** Server-side filter; clears the category filter. */
|
|
48
|
+
setActiveTagId(id: string | null): void;
|
|
49
|
+
loadMore(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createBlogFeedStore({ initialPage, initialCategories, initialTags, pageSize = 20 }: BlogFeedStoreOptions = {}): BlogFeedStore {
|
|
53
|
+
let posts: PostSummary[] | null = initialPage?.posts ?? null;
|
|
54
|
+
let cursor: string | null = initialPage?.nextCursor ?? null;
|
|
55
|
+
let categories: BlogCategory[] = initialCategories ?? [];
|
|
56
|
+
let tags: BlogTag[] = initialTags ?? [];
|
|
57
|
+
let activeCategoryId: string | null = null;
|
|
58
|
+
let activeTagId: string | null = null;
|
|
59
|
+
let loadingMore = false;
|
|
60
|
+
let error: string | null = null;
|
|
61
|
+
let started = false;
|
|
62
|
+
let generation = 0; // bumped by every filter change and stop(); a response from an older generation is dropped
|
|
63
|
+
const listeners = new Set<() => void>();
|
|
64
|
+
let snapshot: BlogFeedState | null = null;
|
|
65
|
+
const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
|
|
66
|
+
|
|
67
|
+
function getState(): BlogFeedState {
|
|
68
|
+
if (snapshot) return snapshot;
|
|
69
|
+
snapshot = { posts, categories, tags, activeCategoryId, activeTagId, hasMore: cursor !== null, loadingMore, error };
|
|
70
|
+
return snapshot;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The first page for the current filter. The unfiltered page may have come from the server —
|
|
74
|
+
// fetch only when it didn't, or when a filter is active (filters are server-side).
|
|
75
|
+
function query(): void {
|
|
76
|
+
if (!started) return;
|
|
77
|
+
const id = ++generation;
|
|
78
|
+
loadingMore = false;
|
|
79
|
+
if (activeCategoryId === null && activeTagId === null && initialPage) {
|
|
80
|
+
posts = initialPage.posts; cursor = initialPage.nextCursor; error = null;
|
|
81
|
+
emit();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
posts = null; cursor = null; error = null;
|
|
85
|
+
emit();
|
|
86
|
+
fetchPosts({ limit: pageSize, categoryId: activeCategoryId, tagId: activeTagId })
|
|
87
|
+
.then((page) => {
|
|
88
|
+
if (generation !== id) return; // superseded — drop it
|
|
89
|
+
posts = page.posts; cursor = page.nextCursor;
|
|
90
|
+
emit();
|
|
91
|
+
})
|
|
92
|
+
.catch((e) => {
|
|
93
|
+
if (generation !== id) return;
|
|
94
|
+
posts = []; cursor = null;
|
|
95
|
+
error = e instanceof Error ? e.message : String(e);
|
|
96
|
+
emit();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
getState,
|
|
102
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
|
|
103
|
+
start() {
|
|
104
|
+
if (started) return;
|
|
105
|
+
started = true;
|
|
106
|
+
if (!initialCategories) fetchBlogCategories().then((c) => { if (started) { categories = c; emit(); } });
|
|
107
|
+
if (!initialTags) fetchBlogTags().then((t) => { if (started) { tags = t; emit(); } });
|
|
108
|
+
query();
|
|
109
|
+
},
|
|
110
|
+
stop() { started = false; generation++; },
|
|
111
|
+
setActiveCategoryId(id) {
|
|
112
|
+
if (id === activeCategoryId && activeTagId === null) return;
|
|
113
|
+
activeCategoryId = id; activeTagId = null;
|
|
114
|
+
query();
|
|
115
|
+
},
|
|
116
|
+
setActiveTagId(id) {
|
|
117
|
+
if (id === activeTagId && activeCategoryId === null) return;
|
|
118
|
+
activeTagId = id; activeCategoryId = null;
|
|
119
|
+
query();
|
|
120
|
+
},
|
|
121
|
+
async loadMore() {
|
|
122
|
+
if (!cursor || loadingMore) return;
|
|
123
|
+
const id = generation;
|
|
124
|
+
loadingMore = true; error = null; emit();
|
|
125
|
+
try {
|
|
126
|
+
const page = await fetchPosts({ limit: pageSize, cursor });
|
|
127
|
+
if (generation !== id) return;
|
|
128
|
+
posts = [...(posts ?? []), ...page.posts]; cursor = page.nextCursor;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (generation !== id) return;
|
|
131
|
+
error = e instanceof Error ? e.message : String(e);
|
|
132
|
+
} finally {
|
|
133
|
+
if (generation === id) { loadingMore = false; emit(); }
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// One post as a framework-free store — the logic behind usePost, usable from React (usePost wraps
|
|
2
|
+
// it), from a static page's post view, from Vue/Svelte, or as the specification for a port. Loads
|
|
3
|
+
// the post by slug (unless seeded) and the FULL taxonomy lists (blog taxonomies are small, fetched
|
|
4
|
+
// once), and resolves the post's category/tag chips from them — never per-id lookups in a render.
|
|
5
|
+
//
|
|
6
|
+
// SSR-friendly: pass the server-fetched PostDetail as `initialPost` (and the taxonomy) and no client
|
|
7
|
+
// fetch happens; a SPA passes only the slug. One store per post surface: createPostStore().
|
|
8
|
+
import { fetchPostBySlug } from "./posts";
|
|
9
|
+
import { fetchBlogCategories, fetchBlogTags } from "./taxonomy";
|
|
10
|
+
import type { BlogCategory, BlogTag, PostDetail } from "./types";
|
|
11
|
+
|
|
12
|
+
export interface PostStoreOptions {
|
|
13
|
+
slug: string;
|
|
14
|
+
initialPost?: PostDetail;
|
|
15
|
+
initialCategories?: BlogCategory[];
|
|
16
|
+
initialTags?: BlogTag[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PostState {
|
|
20
|
+
/** null while loading (render a skeleton) AND when not found — disambiguate via notFound. */
|
|
21
|
+
post: PostDetail | null;
|
|
22
|
+
/** True once the slug definitively resolved to nothing — render a not-found state. */
|
|
23
|
+
notFound: boolean;
|
|
24
|
+
/** This post's categories/tags, resolved to full objects (display .label, route by .slug). */
|
|
25
|
+
categories: BlogCategory[];
|
|
26
|
+
tags: BlogTag[];
|
|
27
|
+
error: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PostStore {
|
|
31
|
+
getState(): PostState;
|
|
32
|
+
subscribe(listener: () => void): () => void;
|
|
33
|
+
/** Fetch the post (unless `initialPost` matches the slug) and the taxonomy (unless seeded). Call once when mounted. */
|
|
34
|
+
start(): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createPostStore({ slug, initialPost, initialCategories, initialTags }: PostStoreOptions): PostStore {
|
|
39
|
+
let post: PostDetail | null = initialPost ?? null;
|
|
40
|
+
let notFound = false;
|
|
41
|
+
let allCategories: BlogCategory[] = initialCategories ?? [];
|
|
42
|
+
let allTags: BlogTag[] = initialTags ?? [];
|
|
43
|
+
let error: string | null = null;
|
|
44
|
+
let started = false;
|
|
45
|
+
const listeners = new Set<() => void>();
|
|
46
|
+
let snapshot: PostState | null = null;
|
|
47
|
+
const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
|
|
48
|
+
|
|
49
|
+
function getState(): PostState {
|
|
50
|
+
if (snapshot) return snapshot;
|
|
51
|
+
snapshot = {
|
|
52
|
+
post,
|
|
53
|
+
notFound,
|
|
54
|
+
categories: post ? allCategories.filter((c) => post!.categoryIds.includes(c.id)) : [],
|
|
55
|
+
tags: post ? allTags.filter((t) => post!.tagIds.includes(t.id)) : [],
|
|
56
|
+
error,
|
|
57
|
+
};
|
|
58
|
+
return snapshot;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
getState,
|
|
63
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
|
|
64
|
+
start() {
|
|
65
|
+
if (started) return;
|
|
66
|
+
started = true;
|
|
67
|
+
if (!initialPost || initialPost.slug !== slug) {
|
|
68
|
+
post = null; notFound = false; emit();
|
|
69
|
+
fetchPostBySlug(slug)
|
|
70
|
+
.then((p) => { if (!started) return; if (p) post = p; else notFound = true; emit(); })
|
|
71
|
+
.catch((e) => { if (started) { error = e instanceof Error ? e.message : String(e); emit(); } });
|
|
72
|
+
}
|
|
73
|
+
if (!initialCategories) fetchBlogCategories().then((c) => { if (started) { allCategories = c; emit(); } });
|
|
74
|
+
if (!initialTags) fetchBlogTags().then((t) => { if (started) { allTags = t; emit(); } });
|
|
75
|
+
},
|
|
76
|
+
stop() { started = false; },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Post rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./posts.ts (the
|
|
2
|
+
// SDK, managed Astro and React) and the REST twin in references/blog/rest/posts.ts (fetch, a static
|
|
3
|
+
// site or a port to another language). Every rule about dates, covers, the feed's sort and filters,
|
|
4
|
+
// and the body fieldsets lives HERE, once. A raw post may come from the SDK (`_id`, media as a
|
|
5
|
+
// `wix:image://` string, dates as Date) or from REST (`id`, media as an { id, url } object, dates
|
|
6
|
+
// as ISO strings); the mappers accept both and produce the same DTO. Imports are type-only so a
|
|
7
|
+
// strip to JS emits no imports.
|
|
8
|
+
import type { PostDetail, PostSummary } from "./types";
|
|
9
|
+
|
|
10
|
+
/** A raw Blog V3 entity as either transport returns it. */
|
|
11
|
+
export type Raw = Record<string, any>;
|
|
12
|
+
/** Media value + size → https URL. Injected: the SDK transport scales through @wix/sdk, REST through the URL form. */
|
|
13
|
+
export type ImgSrc = (value: any, width: number, height: number) => string;
|
|
14
|
+
|
|
15
|
+
/** The Wix Blog app id (Astro item-page routing; comments use it as appId). */
|
|
16
|
+
export const BLOG_APP_ID = "14bcded7-0066-7c35-14d7-466cb3f09103";
|
|
17
|
+
|
|
18
|
+
/** The body fieldsets — without them richContent and contentText come back undefined. */
|
|
19
|
+
export const DETAIL_FIELDSETS = ["RICH_CONTENT", "CONTENT_TEXT"] as const;
|
|
20
|
+
|
|
21
|
+
/** The feed's order: newest first; Wix places pinned posts first on its own. */
|
|
22
|
+
export const FEED_SORT = [{ fieldName: "firstPublishedDate", order: "DESC" }] as const;
|
|
23
|
+
|
|
24
|
+
/** Cover size every card and post header gets — 16:9, one scaled URL per post. */
|
|
25
|
+
export const COVER_WIDTH = 1200;
|
|
26
|
+
export const COVER_HEIGHT = 675;
|
|
27
|
+
|
|
28
|
+
export const rawId = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A media value in the form imgSrc scales. The SDK hands over the `wix:image://v1/<file>/<name>#…`
|
|
32
|
+
* string; REST hands over the Image object { id, url, width, height, filename }. Rebuilding the
|
|
33
|
+
* wix:image form from the object makes both transports scale to the SAME URL — passing the object's
|
|
34
|
+
* `url` through would skip scaling and the two paths would disagree on coverUrl.
|
|
35
|
+
*/
|
|
36
|
+
export function mediaValue(value: unknown): string {
|
|
37
|
+
if (!value) return "";
|
|
38
|
+
if (typeof value === "string") return value;
|
|
39
|
+
const o = value as Raw;
|
|
40
|
+
if (o.id && !String(o.id).startsWith("http")) {
|
|
41
|
+
const size = o.width && o.height ? `#originWidth=${o.width}&originHeight=${o.height}` : "";
|
|
42
|
+
return `wix:image://v1/${o.id}/${o.filename ?? ""}${size}`;
|
|
43
|
+
}
|
|
44
|
+
return o.url ?? o.image ?? "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Display date + ISO date from either a Date (SDK) or an ISO string (REST); both "" when missing or invalid. */
|
|
48
|
+
export function dateParts(value: unknown): { dateLabel: string; dateISO: string } {
|
|
49
|
+
if (!value) return { dateLabel: "", dateISO: "" };
|
|
50
|
+
const d = value instanceof Date ? value : new Date(String(value));
|
|
51
|
+
if (Number.isNaN(d.getTime())) return { dateLabel: "", dateISO: "" };
|
|
52
|
+
return {
|
|
53
|
+
dateLabel: d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }),
|
|
54
|
+
dateISO: d.toISOString(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** "Aug 27, 2026 · 4 min read" — the card's and header's meta line; either half alone, "" when neither is known. */
|
|
59
|
+
export function postMetaLine(post: Pick<PostSummary, "dateLabel" | "minutesToRead">): string {
|
|
60
|
+
return [post.dateLabel, post.minutesToRead > 0 ? `${post.minutesToRead} min read` : ""].filter(Boolean).join(" · ");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function toSummary(raw: Raw, imgSrc: ImgSrc): PostSummary {
|
|
64
|
+
return {
|
|
65
|
+
id: rawId(raw),
|
|
66
|
+
slug: raw.slug ?? "",
|
|
67
|
+
title: raw.title ?? "",
|
|
68
|
+
excerpt: raw.excerpt ?? "",
|
|
69
|
+
...dateParts(raw.firstPublishedDate),
|
|
70
|
+
minutesToRead: raw.minutesToRead ?? 0,
|
|
71
|
+
featured: raw.featured === true,
|
|
72
|
+
pinned: raw.pinned === true,
|
|
73
|
+
// The cover lives at media.wixMedia.image — a media string (SDK) or an Image object (REST).
|
|
74
|
+
coverUrl: imgSrc(mediaValue(raw.media?.wixMedia?.image), COVER_WIDTH, COVER_HEIGHT),
|
|
75
|
+
categoryIds: raw.categoryIds ?? [],
|
|
76
|
+
tagIds: raw.tagIds ?? [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function toDetail(raw: Raw, imgSrc: ImgSrc): PostDetail {
|
|
81
|
+
return {
|
|
82
|
+
...toSummary(raw, imgSrc),
|
|
83
|
+
richContent: raw.richContent ?? null,
|
|
84
|
+
// contentText is plain text — split on newlines for the fallback body.
|
|
85
|
+
paragraphs: String(raw.contentText ?? "")
|
|
86
|
+
.split("\n")
|
|
87
|
+
.map((s: string) => s.trim())
|
|
88
|
+
.filter(Boolean),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface FetchPostsOptions {
|
|
93
|
+
limit?: number;
|
|
94
|
+
/** `nextCursor` from a previous page. */
|
|
95
|
+
cursor?: string | null;
|
|
96
|
+
/** Server-side filters (first page only — the cursor carries them on later pages). */
|
|
97
|
+
categoryId?: string | null;
|
|
98
|
+
tagId?: string | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The feed query as one object — the REST body's `query`, and the rule the SDK builder in ./posts.ts
|
|
103
|
+
* spells with .descending()/.hasSome()/.skipTo(). A cursor encodes the original filter+sort, so a
|
|
104
|
+
* cursor request carries ONLY cursorPaging; the first page carries the sort and the filters.
|
|
105
|
+
*/
|
|
106
|
+
export function feedQuery({ limit = 20, cursor, categoryId, tagId }: FetchPostsOptions = {}): Raw {
|
|
107
|
+
if (cursor) return { cursorPaging: { limit, cursor } };
|
|
108
|
+
const filter: Raw = {};
|
|
109
|
+
if (categoryId) filter.categoryIds = { $hasSome: [categoryId] };
|
|
110
|
+
if (tagId) filter.tagIds = { $hasSome: [tagId] };
|
|
111
|
+
return { ...(Object.keys(filter).length ? { filter } : {}), sort: FEED_SORT, cursorPaging: { limit } };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The by-slug query (REST body's `query`): exact slug, one row. */
|
|
115
|
+
export function slugQuery(slug: string): Raw {
|
|
116
|
+
return { filter: { slug: { $eq: slug } }, cursorPaging: { limit: 1 } };
|
|
117
|
+
}
|