@wix/himalaya-cli 0.1614.0 → 0.1615.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/dist/cli.mjs +1 -0
- package/package.json +2 -2
- package/template-icons/salon-spa.png +0 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/branded-lite/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/branded-lite/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/branded-lite/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-galleries/index.ts +104 -12
- package/templates/community-club/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/community-club/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/events-meetups/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/gym-fitness/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/nonprofit-cause/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/podcast-creator/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/portfolio-creative/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/restaurant/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/retail-store/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/salon-spa/README.md +53 -0
- package/templates/salon-spa/config.ts +272 -0
- package/templates/salon-spa/dev/index.ts +19893 -0
- package/templates/salon-spa/himalaya.content.json +49 -0
- package/templates/salon-spa/himalaya.template.json +14 -0
- package/templates/salon-spa/icon.png +0 -0
- package/templates/salon-spa/platforms.json +1 -0
- package/templates/salon-spa/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/salon-spa/tier5-src/_shared/booking-location.ts +147 -0
- package/templates/salon-spa/tier5-src/_shared/booking-session.ts +222 -0
- package/templates/salon-spa/tier5-src/_shared/branded-lite-hosted.ts +278 -0
- package/templates/salon-spa/tier5-src/_shared/branded-load-state.ts +109 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-copy.ts +40 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-history.ts +48 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-message-model.ts +177 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-model.ts +56 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-presenter.ts +581 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-transport.ts +148 -0
- package/templates/salon-spa/tier5-src/_shared/collection-contract.ts +230 -0
- package/templates/salon-spa/tier5-src/_shared/collection-query.ts +193 -0
- package/templates/salon-spa/tier5-src/_shared/collection-service.ts +400 -0
- package/templates/salon-spa/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/salon-spa/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/salon-spa/tier5-src/_shared/engine-intl.ts +508 -0
- package/templates/salon-spa/tier5-src/_shared/himi-account.ts +164 -0
- package/templates/salon-spa/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/salon-spa/tier5-src/_shared/himi-bookings.ts +318 -0
- package/templates/salon-spa/tier5-src/_shared/himi-copy.ts +89 -0
- package/templates/salon-spa/tier5-src/_shared/himi-store.ts +7 -0
- package/templates/salon-spa/tier5-src/_shared/himi-text.ts +24 -0
- package/templates/salon-spa/tier5-src/_shared/host-events.ts +66 -0
- package/templates/salon-spa/tier5-src/_shared/hosted-checkout.ts +70 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/salon-spa/tier5-src/_shared/iap-worker.ts +125 -0
- package/templates/salon-spa/tier5-src/_shared/local-preferences.ts +38 -0
- package/templates/salon-spa/tier5-src/_shared/member-access.ts +52 -0
- package/templates/salon-spa/tier5-src/_shared/member-account-routes.ts +52 -0
- package/templates/salon-spa/tier5-src/_shared/member-bookings.ts +94 -0
- package/templates/salon-spa/tier5-src/_shared/offset-page.ts +10 -0
- package/templates/salon-spa/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/salon-spa/tier5-src/_shared/owner-load-error.ts +348 -0
- package/templates/salon-spa/tier5-src/_shared/payment-setup.ts +6 -0
- package/templates/salon-spa/tier5-src/_shared/rich-body.ts +78 -0
- package/templates/salon-spa/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/salon-spa/tier5-src/_shared/weekly-goal.ts +107 -0
- package/templates/salon-spa/tier5-src/_shared/wix-service-price.ts +146 -0
- package/templates/salon-spa/tier5-src/business-chat/index.ts +669 -0
- package/templates/salon-spa/tier5-src/himi-booking-account/index.ts +241 -0
- package/templates/salon-spa/tier5-src/himi-booking-bookings/index.ts +466 -0
- package/templates/salon-spa/tier5-src/himi-booking-saved/index.ts +103 -0
- package/templates/salon-spa/tier5-src/himi-booking-service/index.ts +1170 -0
- package/templates/salon-spa/tier5-src/himi-booking-services/index.ts +791 -0
- package/templates/salon-spa/tier5-src/himi-booking-time/index.ts +1712 -0
- package/templates/salon-spa/tier5-src/himi-branded-lite-agenda/index.ts +950 -0
- package/templates/salon-spa/tier5-src/himi-branded-lite-galleries/index.ts +282 -0
- package/templates/salon-spa/tier5-src/himi-salon-home/index.ts +544 -0
- package/templates/salon-spa/tier5-src/himi-store-account/index.ts +925 -0
- package/templates/salon-spa/tokens.json +341 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/yoga-studio/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// himi-branded-lite-galleries — branded-lite page galleries and Wix Pro Gallery.
|
|
2
|
+
//
|
|
3
|
+
// Published Gallery page photos are carried by site analyze in the descriptor and
|
|
4
|
+
// displayed without a Pro Gallery request. Sites with no page gallery use the Wix
|
|
5
|
+
// API gateway: the list takes each gallery's first item as its cover, and opening a
|
|
6
|
+
// gateway gallery reads its items. Failed reads remain errors; sample data is explicit
|
|
7
|
+
// preview only.
|
|
8
|
+
//
|
|
9
|
+
import { defineService } from "../_shared/collection-service.js";
|
|
10
|
+
import { get as getAppInfo } from "@himalaya/app-info";
|
|
11
|
+
import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
12
|
+
import { isNotFoundError, presence, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
13
|
+
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
14
|
+
|
|
15
|
+
const GALLERIES_PATH = "/progallery/v2/galleries";
|
|
16
|
+
const LEGACY_GALLERIES_URL =
|
|
17
|
+
"https://www.wix.com/_api/photography-one-app-viewer/galleries?itemsPerGallery=1&offset=0&limit=30&withHidden=false&useNew=true";
|
|
18
|
+
const PAGE_SIZE = 10; // API maximum. Keep the prior screen's 30-gallery cap.
|
|
19
|
+
const MAX_GALLERIES = 30;
|
|
20
|
+
|
|
21
|
+
type WixApi = { call: (m: string, p: string, b?: unknown, h?: Record<string, string>) => Promise<unknown> };
|
|
22
|
+
function wixApi(): WixApi | null {
|
|
23
|
+
return (globalThis as { himalaya?: { wixApi?: WixApi } }).himalaya?.wixApi ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RawItem { id?: unknown; title?: unknown; image?: { imageInfo?: unknown } }
|
|
27
|
+
interface RawGallery { id: string; name?: string; totalItems?: number; totalItemsCount?: number; items?: RawItem[]; source?: "gateway" | "legacy" }
|
|
28
|
+
interface StaticGallery { id: string; title: string; photos: string[] }
|
|
29
|
+
/** `source`: which route listed the gallery. Only a GATEWAY gallery can be opened live by id —
|
|
30
|
+
* a legacy row's id (or a synthesized `gallery-<n>`) is unknown to the gateway. */
|
|
31
|
+
interface Row { id: string; title: string; subtitle: string; name: string; description: string; image: string; noCover: boolean; hasCover: boolean; source: "gateway" | "legacy" | "page" }
|
|
32
|
+
interface PhotoRow { id: string; url: string; title: string }
|
|
33
|
+
interface S extends LoadFlags, PresenceFlags {
|
|
34
|
+
galleries: Row[]; emptyText: string;
|
|
35
|
+
galleryTitle: string; photos: PhotoRow[]; photoUrl: string; photoTitle: string; noGallery: boolean; hasPhotos: boolean; hasPhoto: boolean; noPhoto: boolean;
|
|
36
|
+
isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
|
|
37
|
+
* shows only when the site really has none, and the unavailable one only when the
|
|
38
|
+
* query failed. A failed query must never render the sentence that claims the
|
|
39
|
+
* customer has no content. */
|
|
40
|
+
notMockEmpty: boolean; notMockUnavailable: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function input(key: string): string {
|
|
44
|
+
const value = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input?.[key];
|
|
45
|
+
return typeof value === "string" ? value : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseStaticGalleries(raw: unknown): StaticGallery[] {
|
|
49
|
+
const parsed: unknown = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
50
|
+
if (parsed == null) return [];
|
|
51
|
+
if (!Array.isArray(parsed)) throw new Error("Malformed imported galleries");
|
|
52
|
+
let remaining = 50;
|
|
53
|
+
return parsed.slice(0, 10).flatMap((item): StaticGallery[] => {
|
|
54
|
+
if (!remaining) return [];
|
|
55
|
+
if (!item || typeof item !== "object") return [];
|
|
56
|
+
const gallery = item as Partial<StaticGallery>;
|
|
57
|
+
if (typeof gallery.id !== "string" || typeof gallery.title !== "string" || !Array.isArray(gallery.photos)) return [];
|
|
58
|
+
const photos = gallery.photos.filter((url): url is string => typeof url === "string" && /^https:\/\/static\.wixstatic\.com\/media\/[A-Za-z0-9_~.-]+$/.test(url)).slice(0, remaining);
|
|
59
|
+
remaining -= photos.length;
|
|
60
|
+
return photos.length ? [{ id: gallery.id, title: gallery.title, photos }] : [];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let staticGalleries: StaticGallery[] = [];
|
|
65
|
+
/** The gateway gallery open on screen: each `open` counts up, and a read answers for its own. */
|
|
66
|
+
let openSeq = 0;
|
|
67
|
+
type PhotosRead = { seq: number; title?: string; photos?: PhotoRow[] } | { seq: number; error: unknown };
|
|
68
|
+
|
|
69
|
+
/** A Pro Gallery image item's URL. `imageInfo` is either a Wix media object (`{ id, url }`,
|
|
70
|
+
* where `url` may be the bare media id) or a `wix:image://` string; anything else is "". */
|
|
71
|
+
function itemUrl(item: RawItem | undefined): string {
|
|
72
|
+
const info = item?.image?.imageInfo;
|
|
73
|
+
if (typeof info === "string") return wixMediaUrl(info);
|
|
74
|
+
if (!info || typeof info !== "object") return "";
|
|
75
|
+
const media = info as { url?: unknown; id?: unknown };
|
|
76
|
+
return wixMediaUrl(media.url) || wixMediaUrl(media.id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function norm(g: RawGallery, i: number): Row {
|
|
80
|
+
const title = (g.name || "").trim() || `Gallery ${i + 1}`;
|
|
81
|
+
const n = g.totalItems ?? g.totalItemsCount ?? 0;
|
|
82
|
+
const subtitle = n ? (n === 1 ? "1 photo" : `${n} photos`) : "";
|
|
83
|
+
// The list asks for one item per gallery (itemLimit=1): that item is the cover.
|
|
84
|
+
const image = itemUrl(Array.isArray(g.items) ? g.items[0] : undefined);
|
|
85
|
+
return { id: g.id, title, subtitle, name: title, description: subtitle, image, noCover: !image, hasCover: !!image, source: g.source ?? "gateway" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A gateway gallery's photos, in the gallery's own order, page by page (`itemOffset` /
|
|
89
|
+
* `itemLimit`; `totalItems` says when to stop — `offset`/`limit` are ignored on this route).
|
|
90
|
+
* Capped so a huge gallery cannot stall the screen. Video and text items carry no image and
|
|
91
|
+
* are skipped. */
|
|
92
|
+
const ITEM_PAGE = 100, ITEM_MAX = 500;
|
|
93
|
+
async function readGalleryPhotos(api: WixApi, id: string): Promise<{ title: string; photos: PhotoRow[] }> {
|
|
94
|
+
let name: unknown = "";
|
|
95
|
+
const items: RawItem[] = [];
|
|
96
|
+
for (let offset = 0; offset < ITEM_MAX; offset += ITEM_PAGE) {
|
|
97
|
+
const res = await api.call("GET", `${GALLERIES_PATH}/${encodeURIComponent(id)}?itemOffset=${offset}&itemLimit=${ITEM_PAGE}`) as { gallery?: { name?: unknown; items?: unknown; totalItems?: unknown } } | null;
|
|
98
|
+
const gallery = res?.gallery;
|
|
99
|
+
if (!gallery || typeof gallery !== "object") throw new Error("Malformed Pro Gallery response");
|
|
100
|
+
if (offset === 0) name = gallery.name;
|
|
101
|
+
const page = Array.isArray(gallery.items) ? gallery.items as RawItem[] : [];
|
|
102
|
+
items.push(...page);
|
|
103
|
+
const total = typeof gallery.totalItems === "number" ? gallery.totalItems : undefined;
|
|
104
|
+
if (page.length < ITEM_PAGE || (total !== undefined && items.length >= total)) break;
|
|
105
|
+
}
|
|
106
|
+
const photos = items.flatMap((item, index): PhotoRow[] => {
|
|
107
|
+
// The original media URL, like a page gallery's: the photo viewer (`view`) accepts only
|
|
108
|
+
// that form, and the renderer sizes the image to its slot.
|
|
109
|
+
const url = itemUrl(item).replace(/\/v1\/.*$/, "");
|
|
110
|
+
if (!url) return [];
|
|
111
|
+
const title = typeof item.title === "string" && item.title.trim() ? item.title.trim() : `Photo ${index + 1}`;
|
|
112
|
+
return [{ id: typeof item.id === "string" && item.id ? item.id : `${id}-${index}`, url, title }];
|
|
113
|
+
});
|
|
114
|
+
return { title: typeof name === "string" ? name.trim() : "", photos };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readGalleries(api: WixApi): Promise<RawGallery[]> {
|
|
118
|
+
const all: RawGallery[] = [];
|
|
119
|
+
for (let offset = 0; offset < MAX_GALLERIES; offset += PAGE_SIZE) {
|
|
120
|
+
const res = await api.call("GET", `${GALLERIES_PATH}?itemLimit=1&offset=${offset}&limit=${PAGE_SIZE}`) as unknown;
|
|
121
|
+
const page = (res as { galleries?: unknown } | null)?.galleries;
|
|
122
|
+
if (!Array.isArray(page) || page.some((g) => !g || typeof g.id !== "string" || !g.id)) {
|
|
123
|
+
throw new Error("Malformed Pro Gallery list response");
|
|
124
|
+
}
|
|
125
|
+
all.push(...page as RawGallery[]);
|
|
126
|
+
const total = (res as { totalGalleries?: unknown }).totalGalleries;
|
|
127
|
+
if (page.length < PAGE_SIZE || (typeof total === "number" && all.length >= total)) break;
|
|
128
|
+
}
|
|
129
|
+
return all;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readLegacyGalleries(api: WixApi): Promise<RawGallery[]> {
|
|
133
|
+
const res = await api.call("GET", LEGACY_GALLERIES_URL) as unknown;
|
|
134
|
+
const raw = Array.isArray(res) ? res :
|
|
135
|
+
(res as { galleries?: unknown; data?: { galleries?: unknown } } | null)?.galleries ??
|
|
136
|
+
(res as { data?: { galleries?: unknown } } | null)?.data?.galleries;
|
|
137
|
+
if (!Array.isArray(raw)) throw new Error("Malformed legacy Pro Gallery list response");
|
|
138
|
+
return raw.map((gallery, index) => {
|
|
139
|
+
if (!gallery || typeof gallery !== "object") throw new Error("Malformed legacy Pro Gallery row");
|
|
140
|
+
const row = gallery as Partial<RawGallery>;
|
|
141
|
+
return { ...row, id: typeof row.id === "string" && row.id ? row.id : `gallery-${index}`, source: "legacy" as const };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readCompatibleGalleries(api: WixApi): Promise<RawGallery[]> {
|
|
146
|
+
// The old absolute host is supported by native Wix transports, but the
|
|
147
|
+
// published web bridge refuses it as webwix_owner_only.
|
|
148
|
+
const nativeLegacyAvailable = async (): Promise<boolean> => {
|
|
149
|
+
const info = await getAppInfo().catch(() => undefined);
|
|
150
|
+
return info?.platform === "ios" || info?.platform === "android" ||
|
|
151
|
+
info?.platform === "macos" || info?.platform === "react-native";
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const galleries = await readGalleries(api);
|
|
155
|
+
if (galleries.length) return galleries;
|
|
156
|
+
if (!(await nativeLegacyAvailable())) return galleries;
|
|
157
|
+
// A native site's older Pro Gallery app can have content even when the gateway
|
|
158
|
+
// reports none. Keep the gateway's honest empty result if the old route is blocked.
|
|
159
|
+
try {
|
|
160
|
+
const legacy = await readLegacyGalleries(api);
|
|
161
|
+
return legacy.length ? legacy : galleries;
|
|
162
|
+
} catch { return galleries; }
|
|
163
|
+
} catch (gatewayError) {
|
|
164
|
+
if (!(await nativeLegacyAvailable())) throw gatewayError;
|
|
165
|
+
try { return await readLegacyGalleries(api); }
|
|
166
|
+
catch { throw gatewayError; }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const MOCK: Row[] = [
|
|
171
|
+
{ id: "sample-work", title: "Our Work", subtitle: "24 photos", name: "Our Work", description: "24 photos", image: "", noCover: true, hasCover: false, source: "page" },
|
|
172
|
+
{ id: "sample-events", title: "Events", subtitle: "48 photos", name: "Events", description: "48 photos", image: "", noCover: true, hasCover: false, source: "page" },
|
|
173
|
+
{ id: "sample-team", title: "The Team", subtitle: "9 photos", name: "The Team", description: "9 photos", image: "", noCover: true, hasCover: false, source: "page" },
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
/** sample rows reach the screen ONLY in the host's explicit sample preview;
|
|
177
|
+
* a failed or empty query is an honest error / empty state (branded-load-state.ts). */
|
|
178
|
+
const COPY: LoadCopy<Row> = {
|
|
179
|
+
prefix: "galleries",
|
|
180
|
+
empty: "No galleries yet — check back soon.",
|
|
181
|
+
error: "Galleries couldn't be loaded.",
|
|
182
|
+
signIn: "Sign in to see galleries.",
|
|
183
|
+
sample: () => MOCK.map((r) => ({ ...r })),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** The newest state, for an action that answers after an await (`s` is from before it). */
|
|
187
|
+
const current = (s: S): S => (svc.getState?.() as S | undefined) ?? s;
|
|
188
|
+
|
|
189
|
+
const svc = defineService<S, {
|
|
190
|
+
load: (s: S, arg?: { staticGalleries?: unknown }) => Promise<S>;
|
|
191
|
+
open: (s: S, arg?: { staticGalleries?: unknown }) => S;
|
|
192
|
+
photosRead: (s: S, arg?: PhotosRead) => S | undefined;
|
|
193
|
+
view: (s: S) => S;
|
|
194
|
+
}>("himiBrandedGalleries", {
|
|
195
|
+
state: { ...LOADING, ...presence("loading"), galleries: [], emptyText: "", galleryTitle: "", photos: [], photoUrl: "", photoTitle: "", noGallery: true, hasPhotos: false, hasPhoto: false, noPhoto: true },
|
|
196
|
+
actions: {
|
|
197
|
+
load: async (s, arg) => {
|
|
198
|
+
// No `openSeq` bump here: the list and a gallery never share a worker (workers are pooled by
|
|
199
|
+
// screen and input), and a host's refresh runs `load` on the gallery's own worker, where a
|
|
200
|
+
// bump would drop that gallery's read in flight and leave its screen blank.
|
|
201
|
+
try {
|
|
202
|
+
if (arg && "staticGalleries" in arg) {
|
|
203
|
+
staticGalleries = [];
|
|
204
|
+
staticGalleries = parseStaticGalleries(arg.staticGalleries);
|
|
205
|
+
}
|
|
206
|
+
if (staticGalleries.length) {
|
|
207
|
+
const rows = staticGalleries.map((gallery) => {
|
|
208
|
+
const subtitle = gallery.photos.length === 1 ? "1 photo" : `${gallery.photos.length} photos`;
|
|
209
|
+
return { id: gallery.id, title: gallery.title, subtitle, name: gallery.title, description: subtitle, image: gallery.photos[0], noCover: false, hasCover: true, source: "page" as const };
|
|
210
|
+
});
|
|
211
|
+
return { ...s, ...loaded("galleries", rows, COPY) };
|
|
212
|
+
}
|
|
213
|
+
const api = wixApi();
|
|
214
|
+
if (!api) return { ...s, ...loadFailed("galleries", new Error("Wix bridge unavailable"), COPY) };
|
|
215
|
+
const raw = await readCompatibleGalleries(api);
|
|
216
|
+
const galleries = raw.map(norm);
|
|
217
|
+
// From the state as it is NOW: on a gallery's own worker, its photos may have landed
|
|
218
|
+
// (`photosRead`) during the read, and `s` is from before it.
|
|
219
|
+
return { ...current(s), ...loaded("galleries", galleries, COPY) };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return { ...current(s), ...loadFailed("galleries", err, COPY) };
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
open: (s, arg) => {
|
|
225
|
+
// Every open, live or not, makes the reads of earlier opens stale (`photosRead` drops them):
|
|
226
|
+
// a page or legacy gallery opened after a slow gateway read must not get that read's photos.
|
|
227
|
+
const seq = ++openSeq;
|
|
228
|
+
try {
|
|
229
|
+
if (arg && "staticGalleries" in arg) {
|
|
230
|
+
staticGalleries = [];
|
|
231
|
+
staticGalleries = parseStaticGalleries(arg.staticGalleries);
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return { ...s, ...loadFailed("galleries", err, COPY), ...presence("failed"), galleryTitle: "", photos: [],
|
|
235
|
+
noGallery: true, hasPhotos: false, photoUrl: "", photoTitle: "", hasPhoto: false, noPhoto: true };
|
|
236
|
+
}
|
|
237
|
+
const id = input("id");
|
|
238
|
+
const gallery = staticGalleries.find((entry) => entry.id === id);
|
|
239
|
+
// No page galleries were carried in: a gateway (Pro Gallery app) gallery is read live, but
|
|
240
|
+
// ONLY when the tap says the list row came from the gateway (`source: "gateway"`, the row's
|
|
241
|
+
// own field). A legacy row's id and a sample row's id ("sample-work") are unknown to the
|
|
242
|
+
// gateway, and a tap that passes no source (Branded Lite's) cannot tell them apart: those
|
|
243
|
+
// open as before, by their title.
|
|
244
|
+
const api = wixApi();
|
|
245
|
+
const live = input("source") === "gateway" && !/^gallery-\d+$/.test(id);
|
|
246
|
+
if (id && !staticGalleries.length && api && live) {
|
|
247
|
+
// The read lands through `photosRead`, tagged with this open: a pooled worker shows THIS
|
|
248
|
+
// gallery loading at once (never the last one's photos), and a slower read for a gallery
|
|
249
|
+
// left since is dropped instead of overwriting the one on screen.
|
|
250
|
+
const name = input("name");
|
|
251
|
+
void readGalleryPhotos(api, id).then(
|
|
252
|
+
({ title, photos }) => svc.photosRead({ seq, title: title || name, photos }),
|
|
253
|
+
(err: unknown) => svc.photosRead({ seq, error: err }),
|
|
254
|
+
);
|
|
255
|
+
return { ...s, ...LOADING, ...presence("loading"), galleryTitle: name, photos: [], noGallery: true, hasPhotos: false,
|
|
256
|
+
photoUrl: "", photoTitle: "", hasPhoto: false, noPhoto: true };
|
|
257
|
+
}
|
|
258
|
+
const missing = !id || (staticGalleries.length > 0 && !gallery);
|
|
259
|
+
return { ...s, ...loaded("galleries", s.galleries, COPY), ...presence(missing ? "missing" : "found"), galleryTitle: gallery?.title || input("name"),
|
|
260
|
+
photos: (gallery?.photos ?? []).map((url, index) => ({ id: `${id}-${index}`, url, title: `Photo ${index + 1}` })),
|
|
261
|
+
noGallery: missing, hasPhotos: !!gallery?.photos.length, noPhoto: true };
|
|
262
|
+
},
|
|
263
|
+
photosRead: (s, arg) => {
|
|
264
|
+
if (!arg || arg.seq !== openSeq) return undefined;
|
|
265
|
+
if ("error" in arg) {
|
|
266
|
+
// A 404 is the gallery being GONE — the not-found state alone, never also an error card
|
|
267
|
+
// whose Retry cannot succeed. Any other failure is the error state.
|
|
268
|
+
return { ...s, ...(isNotFoundError(arg.error) ? { ...loaded("galleries", s.galleries, COPY), ...presence("missing") } : { ...loadFailed("galleries", arg.error, COPY), ...presence("failed") }),
|
|
269
|
+
galleryTitle: "", photos: [], noGallery: true, hasPhotos: false, photoUrl: "", photoTitle: "", hasPhoto: false, noPhoto: true };
|
|
270
|
+
}
|
|
271
|
+
return { ...s, ...loaded("galleries", s.galleries, COPY), ...presence("found"), galleryTitle: arg.title ?? "",
|
|
272
|
+
photos: arg.photos ?? [], noGallery: false, hasPhotos: (arg.photos ?? []).length > 0, photoUrl: "", photoTitle: "", hasPhoto: false, noPhoto: true };
|
|
273
|
+
},
|
|
274
|
+
view: (s) => {
|
|
275
|
+
const photoUrl = input("url");
|
|
276
|
+
const valid = /^https:\/\/static\.wixstatic\.com\/media\/[A-Za-z0-9_~.-]+$/.test(photoUrl);
|
|
277
|
+
return { ...s, ...loaded("galleries", s.galleries, COPY), ...presence(valid ? "found" : "missing"),
|
|
278
|
+
photoUrl: valid ? photoUrl : "", photoTitle: valid ? input("name") || "Gallery photo" : "", hasPhoto: valid, noPhoto: !valid };
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
export { svc };
|