const col = initialCollection ?? (await fetchCollectionBySlug(slug));
\n
if (!started)
\n
return;
\n
if (!col) {
\n
notFound = true;
\n
projects = [];
\n
emit();
\n
return;
\n
}
\n
collection = col;
\n
emit();
\n
const list = await fetchProjects({ collectionId: col.id });
\n
if (started) {
\n
projects = list;
\n
emit();
\n
}
\n
}
\n
catch (e) {
\n
if (!started)
\n
return;
\n
projects = [];
\n
error = e instanceof Error ? e.message : String(e);
\n
emit();
\n
}
\n
})();
\n
},
\n
stop() { started = false; },
\n
};
\n
}
\n
"
,
14 "js/wix/collections-store.js": "// Declarations of collections-store.ts: what this file exports, their arguments and what they return.\n// import type { CollectionSummary } from \"./portfolio-types.js\";\n// export interface CollectionsStoreOptions {\n// initialCollections?: CollectionSummary[];\n// }\n// export interface CollectionsState {\n// collections: CollectionSummary[] | null;\n// error: string | null;\n// }\n// export interface CollectionsStore {\n// getState(): CollectionsState;\n// subscribe(listener: (state: CollectionsState) => void): () => void;\n// start(): void;\n// stop(): void;\n// }\n// export declare function createCollectionsStore({ initialCollections }?: CollectionsStoreOptions): CollectionsStore;\n// The collections gallery as a framework-free store — the logic behind useCollections, usable\n// from React (useCollections wraps it with useSyncExternalStore), from a static page or\n// Vue/Svelte (subscribe and render), or as the specification for a port. State, subscribe/getState,\n// start()/stop(), emit after every change.\n//\n// SSR-friendly: seed with `initialCollections` and nothing is fetched; without a seed `start()`\n// fetches once. One store per mounted gallery: createCollectionsStore(), not a singleton.\nimport { fetchCollections } from \"./portfolio.js\";\nexport function createCollectionsStore({ initialCollections } = {}) {\n let collections = initialCollections ?? null;\n let error = null;\n let started = false;\n const listeners = new Set();\n let snapshot = null;\n const emit = () => { snapshot = null; for (const fn of listeners)\n fn(); };\n function getState() {\n return (snapshot ??= { collections, error });\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"collections-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (initialCollections)\n return;\n fetchCollections()\n .then((c) => { if (started) {\n collections = c;\n emit();\n } })\n .catch((e) => {\n if (!started)\n return;\n collections = [];\n error = e instanceof Error ? e.message : String(e);\n emit();\n });\n },\n stop() { started = false; },\n };\n}\n",
15 "js/wix/portfolio-core.js": "// Declarations of portfolio-core.ts: what this file exports, their arguments and what they return.\n// import type { CollectionSummary, GalleryItem, ProjectDetail, ProjectDetailRow, ProjectSummary } from \"./portfolio-types.js\";\n// export type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export type VideoSrc = (wixVideoRef: string) => {\n// url: string;\n// thumbnail: string;\n// } | null;\n// export declare function imageRef(value: unknown): string;\n// export declare function toVideo(value: unknown, imgSrc: ImgSrc, videoSrc: VideoSrc): {\n// videoUrl: string | null;\n// posterUrl: string;\n// };\n// export declare function toCollection(raw: Raw, imgSrc: ImgSrc): CollectionSummary;\n// export declare function projectCover(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): string;\n// export declare function toProject(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectSummary;\n// export declare function toDetailRow(raw: Raw): ProjectDetailRow;\n// export declare function toDetail(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectDetail;\n// export declare function toGalleryItem(raw: Raw, imgSrc: ImgSrc, videoSrc: VideoSrc): GalleryItem | null;\n// export declare function visibleCollections(raws: Raw[], imgSrc: ImgSrc): CollectionSummary[];\n// export declare function visibleCollection(raw: Raw | undefined | null, imgSrc: ImgSrc): CollectionSummary | null;\n// export declare function visibleProjects(raws: Raw[], collectionId: string | undefined, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectSummary[];\n// export declare function visibleProject(raw: Raw | undefined | null, imgSrc: ImgSrc, videoSrc: VideoSrc): ProjectDetail | null;\n// export declare function galleryItems(raws: Raw[], imgSrc: ImgSrc, videoSrc: VideoSrc): GalleryItem[];\n// export type MediaElement = {\n// tag: \"img\";\n// attrs: {\n// src: string;\n// alt: string;\n// loading: \"lazy\";\n// decoding: \"async\";\n// };\n// } | {\n// tag: \"video\";\n// attrs: {\n// src: string;\n// poster?: string;\n// controls: \"\";\n// playsinline: \"\";\n// };\n// };\n// export declare function mediaElement(item: GalleryItem): MediaElement | null;\nconst id = (raw) => raw?._id ?? raw?.id ?? \"\";\n/**\n * An image reference as either transport carries it — the SDK's bare `wix:image://` string, or\n * REST's `imageInfo` object `{ id, url, filename }` — normalized to the `wix:image://` form so\n * imgSrc scales both to the SAME URL (REST's `url` is a fixed 1024×1024 rendition; the id is the\n * media file). \"\" when there is no image.\n */\nexport function imageRef(value) {\n if (!value)\n return \"\";\n if (typeof value === \"string\")\n return value;\n const o = value;\n if (typeof o.id === \"string\" && o.id)\n return `wix:image://v1/${o.id}/${o.filename ?? o.id}`;\n return typeof o.url === \"string\" ? o.url : \"\";\n}\n/**\n * A video as either transport carries it: the SDK's bare `wix:video://` string (resolved by the\n * injected videoSrc — 720p is the standard transcode), or REST's videoInfo object\n * `{ resolutions: [{ url }], posters: [{ id, url }] }` (optimal resolution first).\n */\nexport function toVideo(value, imgSrc, videoSrc) {\n if (typeof value === \"string\") {\n const v = value.startsWith(\"wix:video://\") ? videoSrc(value) : null;\n return v ? { videoUrl: v.url, posterUrl: imgSrc(imageRef(v.thumbnail), 1200, 675) } : { videoUrl: null, posterUrl: \"\" };\n }\n const raw = value;\n const url = raw?.resolutions?.[0]?.url;\n return {\n videoUrl: typeof url === \"string\" ? url : null,\n posterUrl: imgSrc(imageRef(raw?.posters?.[0]), 1200, 675),\n };\n}\n// ---- DTO mappers -----------------------------------------------------------------------------------\nexport function toCollection(raw, imgSrc) {\n return {\n id: id(raw),\n slug: raw.slug ?? \"\",\n title: raw.title ?? \"\",\n description: raw.description ?? \"\",\n imageUrl: imgSrc(imageRef(raw.coverImage?.imageInfo), 1200, 900),\n };\n}\n/** coverImage is ONE-OF with coverVideo — a video-covered project falls back to its poster. */\nexport function projectCover(raw, imgSrc, videoSrc) {\n return imgSrc(imageRef(raw.coverImage?.imageInfo), 1200, 900) || toVideo(raw.coverVideo?.videoInfo, imgSrc, videoSrc).posterUrl;\n}\nexport function toProject(raw, imgSrc, videoSrc) {\n return {\n id: id(raw),\n slug: raw.slug ?? \"\",\n title: raw.title ?? \"\",\n description: raw.description ?? \"\",\n imageUrl: projectCover(raw, imgSrc, videoSrc),\n collectionIds: raw.collectionIds ?? [],\n };\n}\n/** A details row is a one-of: { label, text } or { label, link: { text, url, target } }. */\nexport function toDetailRow(raw) {\n return {\n label: raw.label ?? \"\",\n text: raw.link?.text ?? raw.text ?? \"\",\n url: raw.link?.url ?? null,\n target: raw.link?.target ?? null,\n };\n}\nexport function toDetail(raw, imgSrc, videoSrc) {\n return { ...toProject(raw, imgSrc, videoSrc), details: (raw.details ?? []).map(toDetailRow) };\n}\n/** IMAGE → image item, VIDEO → video item; UNDEFINED or unresolvable media → null (nothing renderable). */\nexport function toGalleryItem(raw, imgSrc, videoSrc) {\n const base = {\n id: id(raw),\n title: raw.title ?? \"\",\n description: raw.description ?? \"\",\n linkUrl: raw.link?.url ?? null,\n linkTarget: raw.link?.target ?? null,\n };\n if (raw.type === \"IMAGE\") {\n const imageUrl = imgSrc(imageRef(raw.image?.imageInfo), 1600, 1200);\n return imageUrl ? { ...base, kind: \"image\", imageUrl, videoUrl: null } : null;\n }\n if (raw.type === \"VIDEO\") {\n const { videoUrl, posterUrl } = toVideo(raw.video?.videoInfo, imgSrc, videoSrc);\n return videoUrl ? { ...base, kind: \"video\", imageUrl: posterUrl, videoUrl } : null;\n }\n return null;\n}\n// ---- list rules -----------------------------------------------------------------------------------\n/**\n * Visible collections in the owner's dashboard order (sortOrder — collections have one, projects\n * don't). An omitted `hidden` comes back ABSENT (proto3): test falsy, never === false.\n */\nexport function visibleCollections(raws, imgSrc) {\n return raws\n .filter((c) => !c.hidden)\n .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))\n .map((c) => toCollection(c, imgSrc));\n}\n/** One visible collection, or null when missing or hidden (a real 404, never a fallback). */\nexport function visibleCollection(raw, imgSrc) {\n return raw && !raw.hidden ? toCollection(raw, imgSrc) : null;\n}\n/**\n * Visible projects in list order (Project exposes no sortOrder), optionally only one collection's.\n * The collection filter is client-side over the one list (portfolios are small; a project links\n * to collections via collectionIds).\n */\nexport function visibleProjects(raws, collectionId, imgSrc, videoSrc) {\n return raws\n .filter((p) => !p.hidden)\n .filter((p) => !collectionId || (p.collectionIds ?? []).includes(collectionId))\n .map((p) => toProject(p, imgSrc, videoSrc));\n}\n/** One visible project with its details rows, or null when missing or hidden. */\nexport function visibleProject(raw, imgSrc, videoSrc) {\n return raw && !raw.hidden ? toDetail(raw, imgSrc, videoSrc) : null;\n}\n/** A project's gallery in dashboard order (item sortOrder); UNDEFINED and unresolvable items dropped. */\nexport function galleryItems(raws, imgSrc, videoSrc) {\n return raws\n .slice()\n .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))\n .map((it) => toGalleryItem(it, imgSrc, videoSrc))\n .filter((g) => g !== null);\n}\n/**\n * Branch on `kind` once, here: a video item is a <video> with its poster, an image item an <img>,\n * anything else null — never a broken tag, never an <img> for a video. The shipped GalleryMedia.tsx\n * renders this; a static page or a port creates the element from it.\n */\nexport function mediaElement(item) {\n if (item.kind === \"video\" && item.videoUrl) {\n return { tag: \"video\", attrs: { src: item.videoUrl, ...(item.imageUrl ? { poster: item.imageUrl } : {}), controls: \"\", playsinline: \"\" } };\n }\n if (item.imageUrl)\n return { tag: \"img\", attrs: { src: item.imageUrl, alt: item.title, loading: \"lazy\", decoding: \"async\" } };\n return null;\n}\n",
16 "js/wix/portfolio-types.js": "// Declarations of portfolio-types.ts: what this file exports, their arguments and what they return.\n// /** A collection as the gallery grid needs it. */\n// export interface CollectionSummary {\n// id: string;\n// slug: string;\n// title: string;\n// description: string;\n// /** Resolved https URL (\"\" when the collection has no cover). */\n// imageUrl: string;\n// }\n// /** A project as a grid tile needs it. */\n// export interface ProjectSummary {\n// id: string;\n// slug: string;\n// title: string;\n// description: string;\n// /** Cover image; a video-covered project falls back to its poster frame (\"\" when neither). */\n// imageUrl: string;\n// /** The collections this project belongs to (a project can be in several). */\n// collectionIds: string[];\n// }\n// /** One row of a project's metadata (Role, Year, Client…): plain text or an outbound link. */\n// export interface ProjectDetailRow {\n// label: string;\n// text: string;\n// /** Non-null → render the row's text as a link. */\n// url: string | null;\n// /** Link rows only: \"_blank\" | \"_self\". */\n// target: string | null;\n// }\n// /** A project as the detail page needs it. */\n// export interface ProjectDetail extends ProjectSummary {\n// details: ProjectDetailRow[];\n// }\n// /** One media item of a project's gallery (dashboard order). */\n// export interface GalleryItem {\n// id: string;\n// kind: \"image\" | \"video\";\n// title: string;\n// description: string;\n// /** image: the resolved image. video: the poster frame (\"\" when the video has none). */\n// imageUrl: string;\n// /** video only: playable mp4 URL. */\n// videoUrl: string | null;\n// /** Optional owner-attached outbound link for this item. */\n// linkUrl: string | null;\n// linkTarget: string | null;\n// }\n// Portfolio DTOs — the serializable shapes every hook, component, and page consumes.\n// Plain JSON: safe as Astro island props or across server/client boundaries. Images are\n// resolved https URLs; videos are playable mp4 URLs with a poster frame.\nexport {};\n",
17 "js/wix/portfolio.js": "// Declarations of portfolio.ts: what this file exports, their arguments and what they return.\n// import type { CollectionSummary, GalleryItem, ProjectDetail, ProjectSummary } from \"./portfolio-types.js\";\n// export declare function fetchCollections({ limit }?: {\n// limit?: number | undefined;\n// }): Promise<CollectionSummary[]>;\n// export declare function fetchCollectionBySlug(slug: string): Promise<CollectionSummary | null>;\n// export declare function fetchProjects({ collectionId, limit, }?: {\n// collectionId?: string;\n// limit?: number;\n// }): Promise<ProjectSummary[]>;\n// export declare function fetchProjectBySlug(slug: string): Promise<ProjectDetail | null>;\n// export declare function fetchProjectGallery(projectId: string): Promise<GalleryItem[]>;\n// Portfolio reads over REST — the twin of app/wix/portfolio/portfolio.ts. Same exports, same DTOs;\n// the rules and mappers come from portfolio-core (the SAME file the SDK transport uses, deployed\n// flat next to this one by deploy.mjs --stack static), so this file is only the transport: one\n// fetch with a literal URL and body per function. Every call here is a public read, safe from a\n// browser with a visitor token. Porting: keep the paths, keep the bodies, port the core once.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/collections/list-collections.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/collections/query-collections.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/projects/list-projects.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/projects/query-projects.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/project-items/list-project-items.md\nimport { wixRequest } from \"./client.js\";\nimport { imgSrc } from \"./media.js\";\nimport { galleryItems, visibleCollection, visibleCollections, visibleProject, visibleProjects, } from \"./portfolio-core.js\";\n// REST never returns the bare wix:video:// string — videoInfo arrives as { resolutions, posters },\n// which the core reads directly. A string here has no resolver on this transport → dropped.\nconst videoSrc = () => null;\n/** Visible collections in dashboard order. GET /portfolio/v1/collections?paging.limit=N → { collections } */\nexport async function fetchCollections({ limit = 100 } = {}) {\n const res = await wixRequest(\"/portfolio/v1/collections\", { method: \"GET\", query: { \"paging.limit\": String(limit) } });\n return visibleCollections((res?.collections ?? []), imgSrc);\n}\n/**\n * One visible collection by URL slug; null when missing or hidden → a real 404.\n * POST /portfolio/v1/collections/query { query: { filter: { slug }, cursorPaging: { limit: 1 } } } → { collections }\n */\nexport async function fetchCollectionBySlug(slug) {\n const res = await wixRequest(\"/portfolio/v1/collections/query\", {\n body: { query: { filter: { slug }, cursorPaging: { limit: 1 } } },\n });\n return visibleCollection(res?.collections?.[0], imgSrc);\n}\n/** Visible projects in list order, optionally one collection's. GET /portfolio/v1/projects?paging.limit=N → { projects } */\nexport async function fetchProjects({ collectionId, limit = 100, } = {}) {\n const res = await wixRequest(\"/portfolio/v1/projects\", { method: \"GET\", query: { \"paging.limit\": String(limit) } });\n return visibleProjects((res?.projects ?? []), collectionId, imgSrc, videoSrc);\n}\n/**\n * One visible project with its details rows by URL slug; null when missing or hidden.\n * POST /portfolio/v1/projects/query { query: { filter: { slug }, cursorPaging: { limit: 1 } } } → { projects }\n */\nexport async function fetchProjectBySlug(slug) {\n const res = await wixRequest(\"/portfolio/v1/projects/query\", {\n body: { query: { filter: { slug }, cursorPaging: { limit: 1 } } },\n });\n return visibleProject(res?.projects?.[0], imgSrc, videoSrc);\n}\n/**\n * A project's gallery in dashboard order; IMAGE and VIDEO items only.\n * GET /portfolio/v1/projectItems/{projectId}/items?paging.limit=100 → { items }\n */\nexport async function fetchProjectGallery(projectId) {\n const res = await wixRequest(`/portfolio/v1/projectItems/${encodeURIComponent(projectId)}/items`, {\n method: \"GET\",\n query: { \"paging.limit\": \"100\" },\n });\n return galleryItems((res?.items ?? []), imgSrc, videoSrc);\n}\n",
18 "js/wix/project-detail-store.js": "// Declarations of project-detail-store.ts: what this file exports, their arguments and what they return.\n// import type { GalleryItem, ProjectDetail } from \"./portfolio-types.js\";\n// export interface ProjectDetailStoreOptions {\n// initialProject?: ProjectDetail;\n// initialItems?: GalleryItem[];\n// }\n// export interface ProjectDetailState {\n// project: ProjectDetail | null;\n// notFound: boolean;\n// items: GalleryItem[] | null;\n// error: string | null;\n// }\n// export interface ProjectDetailStore {\n// getState(): ProjectDetailState;\n// subscribe(listener: (state: ProjectDetailState) => void): () => void;\n// start(): void;\n// stop(): void;\n// }\n// export declare function createProjectDetailStore(slug: string, { initialProject, initialItems }?: ProjectDetailStoreOptions): ProjectDetailStore;\n// Project detail + media gallery, by slug, as a framework-free store — the logic behind\n// useProjectDetail, usable from React (the hook wraps it), from a static page's project view,\n// from Vue/Svelte, or as the specification for a port.\n//\n// SSR-friendly: pass BOTH `initialProject` and `initialItems` and nothing is fetched. With only\n// the project seeded, `start()` fetches its gallery; with nothing seeded it resolves the slug\n// first. `notFound` is the real not-found signal (project stays null while loading). One store\n// per (slug, mount): createProjectDetailStore(slug, options), not a singleton.\nimport { fetchProjectBySlug, fetchProjectGallery } from \"./portfolio.js\";\nexport function createProjectDetailStore(slug, { initialProject, initialItems } = {}) {\n let project = initialProject ?? null;\n let notFound = false;\n let items = initialItems ?? null;\n let error = null;\n let started = false;\n const listeners = new Set();\n let snapshot = null;\n const emit = () => { snapshot = null; for (const fn of listeners)\n fn(); };\n function getState() {\n return (snapshot ??= { project, notFound, items, error });\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"project-detail-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (initialProject && initialItems)\n return;\n (async () => {\n try {\n const proj = initialProject ?? (await fetchProjectBySlug(slug));\n if (!started)\n return;\n if (!proj) {\n notFound = true;\n items = [];\n emit();\n return;\n }\n project = proj;\n emit();\n const gallery = await fetchProjectGallery(proj.id);\n if (started) {\n items = gallery;\n emit();\n }\n }\n catch (e) {\n if (!started)\n return;\n items = [];\n error = e instanceof Error ? e.message : String(e);\n emit();\n }\n })();\n },\n stop() { started = false; },\n };\n}\n"