@wix/himalaya-cli 0.1036.0 → 0.1037.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/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/portfolio/src/index.d.ts +46 -9
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-portfolio.mjs +14 -2
- package/dist/cli.mjs +476 -109
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +51 -2
- package/guides/sdk-reference.json +16 -10
- package/guides/wix-business-context.md +13 -1
- package/guides/wix-site-to-himi-app.md +10 -1
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +14 -8
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +12 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +2 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
|
Binary file
|
package/fixtures/wix-canned.json
CHANGED
|
@@ -1057,20 +1057,69 @@
|
|
|
1057
1057
|
"title": "Roastery Rebuild",
|
|
1058
1058
|
"description": "A 12-week fit-out, start to finish.",
|
|
1059
1059
|
"slug": "roastery-rebuild",
|
|
1060
|
-
"coverImage":
|
|
1060
|
+
"coverImage": {
|
|
1061
|
+
"type": "WIX_MEDIA",
|
|
1062
|
+
"imageInfo": {
|
|
1063
|
+
"id": "example-rebuild.jpg",
|
|
1064
|
+
"url": "https://static.wixstatic.com/media/example-rebuild.jpg",
|
|
1065
|
+
"width": 2752,
|
|
1066
|
+
"height": 1536
|
|
1067
|
+
},
|
|
1068
|
+
"secure": false
|
|
1069
|
+
}
|
|
1061
1070
|
},
|
|
1062
1071
|
{
|
|
1063
1072
|
"id": "proj-0002",
|
|
1064
1073
|
"title": "Harvest Field Notes",
|
|
1065
1074
|
"description": "Photography from the 2026 trip.",
|
|
1066
1075
|
"slug": "harvest-field-notes",
|
|
1067
|
-
"coverImage":
|
|
1076
|
+
"coverImage": {
|
|
1077
|
+
"type": "WIX_MEDIA",
|
|
1078
|
+
"imageInfo": {
|
|
1079
|
+
"id": "example-fieldnotes.jpg",
|
|
1080
|
+
"url": "https://static.wixstatic.com/media/example-fieldnotes.jpg",
|
|
1081
|
+
"width": 1024,
|
|
1082
|
+
"height": 1024
|
|
1083
|
+
},
|
|
1084
|
+
"secure": false
|
|
1085
|
+
}
|
|
1068
1086
|
}
|
|
1069
1087
|
],
|
|
1070
1088
|
"pagingMetadata": {
|
|
1071
1089
|
"count": 2
|
|
1072
1090
|
}
|
|
1073
1091
|
},
|
|
1092
|
+
"POST /portfolio/v1/items/query": {
|
|
1093
|
+
"items": [
|
|
1094
|
+
{
|
|
1095
|
+
"id": "item-0001",
|
|
1096
|
+
"projectId": "proj-0001",
|
|
1097
|
+
"sortOrder": 1,
|
|
1098
|
+
"type": "IMAGE",
|
|
1099
|
+
"title": "The finished bar",
|
|
1100
|
+
"image": {
|
|
1101
|
+
"type": "WIX_MEDIA",
|
|
1102
|
+
"imageInfo": {
|
|
1103
|
+
"id": "example-rebuild-bar.jpg",
|
|
1104
|
+
"url": "https://static.wixstatic.com/media/example-rebuild-bar.jpg",
|
|
1105
|
+
"width": 1600,
|
|
1106
|
+
"height": 1067
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
"id": "item-0002",
|
|
1112
|
+
"projectId": "proj-0001",
|
|
1113
|
+
"sortOrder": 2,
|
|
1114
|
+
"type": "TEXT",
|
|
1115
|
+
"title": "Twelve weeks",
|
|
1116
|
+
"description": "Stripped to the brick, rebuilt around the roaster."
|
|
1117
|
+
}
|
|
1118
|
+
],
|
|
1119
|
+
"metadata": {
|
|
1120
|
+
"count": 2
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1074
1123
|
"POST /badges/v4/badges/query": {
|
|
1075
1124
|
"badges": [
|
|
1076
1125
|
{
|
|
@@ -19564,11 +19564,23 @@
|
|
|
19564
19564
|
"source": "stdlib/wix-sdk/portfolio/src/index.ts",
|
|
19565
19565
|
"description": "Portfolio — projects and collections",
|
|
19566
19566
|
"api": [
|
|
19567
|
+
{
|
|
19568
|
+
"name": "WixMediaImage",
|
|
19569
|
+
"kind": "interface",
|
|
19570
|
+
"jsdoc": "/** A Wix Portfolio media image — a project / collection cover or a project item's image. */",
|
|
19571
|
+
"decl": "export interface WixMediaImage {\n type?: string;\n imageInfo?: { id?: string; url?: string; width?: number; height?: number };\n}"
|
|
19572
|
+
},
|
|
19573
|
+
{
|
|
19574
|
+
"name": "portfolioCoverRef",
|
|
19575
|
+
"kind": "function",
|
|
19576
|
+
"jsdoc": "/**\n * The Wix media reference of a Portfolio cover or item image — its `imageInfo.url`, else\n * its `imageInfo.id` — for `wixMediaUrl` to resolve and size. `\"\"` when there is none.\n * A bare string is still accepted, so a cover stored as a url string keeps rendering.\n *\n * @example\n * const image = wixMediaUrl(portfolioCoverRef(project.coverImage));\n */",
|
|
19577
|
+
"decl": "export function portfolioCoverRef(cover: WixMediaImage | string | null | undefined): string;"
|
|
19578
|
+
},
|
|
19567
19579
|
{
|
|
19568
19580
|
"name": "WixPortfolioProject",
|
|
19569
19581
|
"kind": "interface",
|
|
19570
19582
|
"jsdoc": "",
|
|
19571
|
-
"decl": "export interface WixPortfolioProject {\n id: string;\n title?: string;\n description?: string;\n
|
|
19583
|
+
"decl": "export interface WixPortfolioProject {\n id: string;\n title?: string;\n description?: string;\n /**\n * Project cover. The live wire shape is a media OBJECT\n * (`{type:\"WIX_MEDIA\", imageInfo:{id,url,width,height}}`), not a url string —\n * read it with {@link portfolioCoverRef}; `wixMediaUrl` returns `\"\"` for the object.\n * A url string is still accepted, for projects constructed by hand.\n */\n coverImage?: WixMediaImage | string;\n slug?: string;\n hidden?: boolean;\n createdDate?: string;\n updatedDate?: string;\n}"
|
|
19572
19584
|
},
|
|
19573
19585
|
{
|
|
19574
19586
|
"name": "WixListProjectsResponse",
|
|
@@ -19583,12 +19595,6 @@
|
|
|
19583
19595
|
"decl": "export function listProjects(limit = 100, cursor?: string): Promise<WixListProjectsResponse>;",
|
|
19584
19596
|
"http": "GET /portfolio/v1/projects?{…}"
|
|
19585
19597
|
},
|
|
19586
|
-
{
|
|
19587
|
-
"name": "WixMediaImage",
|
|
19588
|
-
"kind": "interface",
|
|
19589
|
-
"jsdoc": "",
|
|
19590
|
-
"decl": "export interface WixMediaImage { imageInfo?: { id?: string; url?: string } }"
|
|
19591
|
-
},
|
|
19592
19598
|
{
|
|
19593
19599
|
"name": "WixPortfolioCollection",
|
|
19594
19600
|
"kind": "interface",
|
|
@@ -19618,14 +19624,14 @@
|
|
|
19618
19624
|
"name": "WixListProjectItemsResponse",
|
|
19619
19625
|
"kind": "interface",
|
|
19620
19626
|
"jsdoc": "",
|
|
19621
|
-
"decl": "export interface WixListProjectItemsResponse {\n items?: WixPortfolioProjectItem[];\n pagingMetadata?: { count?: number; cursors?: { next?: string }; hasNext?: boolean };\n}"
|
|
19627
|
+
"decl": "export interface WixListProjectItemsResponse {\n items?: WixPortfolioProjectItem[];\n /**\n * Paging, under the name this function always documented. A Wix query reports it as\n * `metadata`; {@link listProjectItems} copies that here when `pagingMetadata` is absent.\n */\n pagingMetadata?: { count?: number; cursors?: { next?: string }; hasNext?: boolean };\n /** The query endpoint's own name for the paging block. */\n metadata?: { count?: number; cursors?: { next?: string }; hasNext?: boolean };\n}"
|
|
19622
19628
|
},
|
|
19623
19629
|
{
|
|
19624
19630
|
"name": "listProjectItems",
|
|
19625
19631
|
"kind": "function",
|
|
19626
|
-
"jsdoc": "
|
|
19632
|
+
"jsdoc": "/**\n * POST /portfolio/v1/items/query — list a project's items.\n *\n * A first page filters on `projectId` with `paging.limit`. Passing a `cursor` (a previous\n * page's `pagingMetadata.cursors.next`) switches to `cursorPaging` and drops the filter,\n * because a Wix query cursor already carries it. Items come back in the order Wix returns\n * them — sort on `sortOrder` for the author's order.\n */",
|
|
19627
19633
|
"decl": "export function listProjectItems(projectId: string, limit = 100, cursor?: string): Promise<WixListProjectItemsResponse>;",
|
|
19628
|
-
"http": "
|
|
19634
|
+
"http": "POST /portfolio/v1/items/query"
|
|
19629
19635
|
}
|
|
19630
19636
|
]
|
|
19631
19637
|
},
|
|
@@ -122,9 +122,21 @@ never change. If nothing is reachable, keep the default kit.
|
|
|
122
122
|
RFC 0048 refused a **content dump** on the runtime facade. The dump lives here, at **build time**:
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
-
himi site analyze --
|
|
125
|
+
himi site analyze --url https://… --out .himi/business-book # visitor-only pass
|
|
126
|
+
himi site analyze --url https://… --client-id <id> [--site <msid>] --out .himi/business-book [--apply]
|
|
126
127
|
```
|
|
127
128
|
|
|
129
|
+
`--url` alone is the **visitor-only pass**: the published pages, their look (screenshots,
|
|
130
|
+
`--wst-*`), the Copy chapter and the Placeholders chapter (template phone/email, Wix editor text, a
|
|
131
|
+
second business name, repeated images). It needs no OAuth client, so no anonymous token is minted
|
|
132
|
+
and every vertical probe reports `visitor skipped` — the Catalog has no counts. `--client-id` (the
|
|
133
|
+
site's Headless client) adds those counts, each Bookings service's price and payment mode
|
|
134
|
+
(`pay online only`, `pay in person only`, `free`, …), and the `wix` binding `--apply` writes;
|
|
135
|
+
`--apply` without it exits 2. `--site` adds the owner-only chapters (CMS, media, roles); an
|
|
136
|
+
ambient `WIX_SITE_ID` does so only beside an explicit `--client-id` flag (an exported
|
|
137
|
+
`WIX_CLIENT_ID` does not count), so a URL-only pass is never silently upgraded to an owner read
|
|
138
|
+
of whichever site that variable names.
|
|
139
|
+
|
|
128
140
|
Same collector, three doors:
|
|
129
141
|
|
|
130
142
|
| Door | Artifact |
|
|
@@ -59,14 +59,23 @@ banner. Keep **only the first ASCII line** (`head -1`) or HTTP headers throw
|
|
|
59
59
|
### 1. Analyze — do not re-fetch Wix
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
|
+
# Visitor-only pass — the URL is all it needs (published pages, look, copy, template leftovers):
|
|
63
|
+
himi site analyze --url https://customer.wixsite.com/site --out .himi/business-book
|
|
64
|
+
|
|
65
|
+
# Full pass — --client-id adds catalog counts, service payment modes and the --apply binding;
|
|
66
|
+
# --site adds the owner chapters (CMS, media, roles):
|
|
62
67
|
himi site analyze \
|
|
63
|
-
--client-id <headless-oauth-client-id> \
|
|
64
68
|
--url https://customer.wixsite.com/site \
|
|
69
|
+
--client-id <headless-oauth-client-id> \
|
|
65
70
|
--site <msid> \
|
|
66
71
|
--out .himi/business-book
|
|
67
72
|
# screenshots + --wst-*: npx playwright install chromium
|
|
68
73
|
```
|
|
69
74
|
|
|
75
|
+
Check `book.md` → **Placeholders** before reusing any copy: a template phone (`123-456-7890`),
|
|
76
|
+
Wix editor text, a second business name or one photo repeated across a gallery is the
|
|
77
|
+
template's, not the customer's.
|
|
78
|
+
|
|
70
79
|
Read `book.json`, `book.md`, `evidence/pages/`, `media/`, `velo/` (via
|
|
71
80
|
`book.velo.files[].localPath`). Write `.himi/business-book/look.notes.md` for mood,
|
|
72
81
|
overlay tie-break, language. **Never rewrite `book.json`.** Never log
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1037.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"artifactId": "himalaya-cli"
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
|
-
"falconPackageHash": "
|
|
68
|
+
"falconPackageHash": "e35236017e826d4e25212e7448f29f8004d84cec687b348a8231366e"
|
|
69
69
|
}
|
|
@@ -11,9 +11,14 @@ const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } })
|
|
|
11
11
|
const validDate = (value: unknown): value is string => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && dayKey(new Date(`${value}T12:00:00`)) === value;
|
|
12
12
|
const views = localPreferences("booking.schedule", value => ["agenda", "day", "month"].includes(String(value)) ? String(value) : "agenda");
|
|
13
13
|
let mode = validDate(input?.date) ? "day" : views.read();
|
|
14
|
-
|
|
14
|
+
// "" until the schedule is first shown, NOT today's date: the release build hashes this worker's
|
|
15
|
+
// initial state, so a date read off the build machine's clock made every build of unchanged
|
|
16
|
+
// source a different release (#3268, the `nondeterministic-initial-state` lint). `derive()` fills
|
|
17
|
+
// in today the first time the schedule is visible, which is never at build time (no member).
|
|
18
|
+
let selectedDate = validDate(input?.date) ? input.date : "";
|
|
19
|
+
const today = () => dayKey(new Date());
|
|
15
20
|
let pageMember: MemberAccess | undefined;
|
|
16
|
-
let queryNow =
|
|
21
|
+
let queryNow = ""; // set by the first-page fetch, never at boot (see selectedDate)
|
|
17
22
|
let cancelTarget: { row: MemberBookingRow; member: MemberAccess } | undefined;
|
|
18
23
|
let cancelling = false;
|
|
19
24
|
interface S {
|
|
@@ -26,11 +31,12 @@ interface S {
|
|
|
26
31
|
nextCursor?: string; complete?: boolean; loadingMore?: boolean; pageError?: boolean; total?: number;
|
|
27
32
|
}
|
|
28
33
|
function derive(s: S): S {
|
|
29
|
-
const
|
|
34
|
+
const hideSchedule = !s.isMember || s.isLoading;
|
|
35
|
+
if (!selectedDate && !hideSchedule) selectedDate = today();
|
|
36
|
+
const dayRows = selectedDate ? s.rows.filter(row => row.day === selectedDate) : [];
|
|
30
37
|
const counts = new Map<string, number>();
|
|
31
38
|
for (const row of s.rows) if (row.day) counts.set(row.day, (counts.get(row.day) ?? 0) + 1);
|
|
32
|
-
|
|
33
|
-
return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: dayLabel(selectedDate), hideSchedule,
|
|
39
|
+
return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: selectedDate ? dayLabel(selectedDate) : "", hideSchedule,
|
|
34
40
|
hideAgenda: hideSchedule || mode !== "agenda", hideCalendar: hideSchedule || mode === "agenda", hideMonth: mode !== "month",
|
|
35
41
|
decorations: [...counts].map(([date, n]) => ({ date, dotCount: Math.min(n, 3), dotColorToken: "accentPrimary" })),
|
|
36
42
|
dayEmptyText: dayRows.length ? "" : s.complete ? copyParam("bookings.emptyDay", "No bookings on this day.") : copyParam("bookings.emptyLoadedDay", "No bookings on this day in loaded results. Load more to check later pages."),
|
|
@@ -48,7 +54,7 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
|
|
|
48
54
|
async fetch(criteria, cursor) {
|
|
49
55
|
const member = await memberAccess();
|
|
50
56
|
if (cursor && (!pageMember || pageMember.email !== member.email || pageMember.scope !== member.scope)) throw memberAccessChanged();
|
|
51
|
-
if (!cursor) queryNow = new Date().toISOString();
|
|
57
|
+
if (!cursor || !queryNow) queryNow = new Date().toISOString();
|
|
52
58
|
return fetchMemberBookings(criteria, cursor, queryNow, member);
|
|
53
59
|
},
|
|
54
60
|
},
|
|
@@ -65,8 +71,8 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
|
|
|
65
71
|
actions: {
|
|
66
72
|
scheduleView(s, arg) { const value = (arg as { selectedId?: string })?.selectedId; if (["agenda", "day", "month"].includes(value || "")) { mode = value!; views.write(mode); } return derive(s); },
|
|
67
73
|
selectDate(s, arg) { const date = (arg as { date?: unknown })?.date; if (validDate(date)) selectedDate = date; return derive(s); },
|
|
68
|
-
previousDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
|
|
69
|
-
nextDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
|
|
74
|
+
previousDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
|
|
75
|
+
nextDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
|
|
70
76
|
async rebook(s, arg) {
|
|
71
77
|
const member = pageMember, row = s.rows.find(row => row.id === (arg as { id?: string })?.id);
|
|
72
78
|
if (!member || !row?.serviceId) return s;
|
|
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
|
|
|
388
388
|
});
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
|
|
392
|
+
* the initial state, so a greeting read off the build machine's clock made every build of an
|
|
393
|
+
* unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
|
|
394
|
+
* The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
|
|
391
395
|
function welcome(): string {
|
|
392
396
|
const hour = new Date().getHours();
|
|
393
397
|
if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
|
|
@@ -395,6 +399,11 @@ function welcome(): string {
|
|
|
395
399
|
return copyParam("home.greetingEvening", "Good evening");
|
|
396
400
|
}
|
|
397
401
|
|
|
402
|
+
/** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
|
|
403
|
+
function neutralWelcome(): string {
|
|
404
|
+
return copyParam("home.greetingNeutral", "Welcome");
|
|
405
|
+
}
|
|
406
|
+
|
|
398
407
|
function feature(p: StoreProduct): ProductFeature {
|
|
399
408
|
const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
|
|
400
409
|
const image = productImage(p);
|
|
@@ -871,7 +880,9 @@ function baseState(): State {
|
|
|
871
880
|
hideHeroInitial: true,
|
|
872
881
|
hideHeroFallback: true,
|
|
873
882
|
hideFeaturedSkeleton: false,
|
|
874
|
-
|
|
883
|
+
// Not welcome(): that reads the clock, and this state is baked into the release. A neutral
|
|
884
|
+
// greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
|
|
885
|
+
welcome: neutralWelcome(),
|
|
875
886
|
refreshedAt: "",
|
|
876
887
|
effectiveLayout: resolveEffectiveLayout(undefined, 0),
|
|
877
888
|
accentColor: resolveAccent(undefined),
|
|
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
|
|
|
8
8
|
import { queryReviews, getReview } from '@himalaya/wix-reviews';
|
|
9
9
|
import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
|
|
10
10
|
import { queryForms, getForm } from '@himalaya/wix-forms';
|
|
11
|
-
import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
|
|
11
|
+
import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
|
|
12
12
|
import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
|
|
13
13
|
import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
|
|
14
14
|
import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
|
|
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
208
208
|
details = [['Namespace', subtitle]];
|
|
209
209
|
break;
|
|
210
210
|
case 'portfolio':
|
|
211
|
-
title = field(raw, 'title'); subtitle = field(raw, 'description'); image =
|
|
211
|
+
title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
|
|
212
212
|
statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
|
|
213
213
|
break;
|
|
214
214
|
case 'donationCampaigns':
|
|
@@ -311,7 +311,10 @@ function shape(s: Partial<AppointmentsState>): AppointmentsState {
|
|
|
311
311
|
// Until the owner picks a day, follow the soonest booked one, so a freshly
|
|
312
312
|
// loaded month never opens on an empty day list.
|
|
313
313
|
const soonest = all.find((r) => r._day)?._day ?? "";
|
|
314
|
-
|
|
314
|
+
// Today only once the list is READY (the month is hidden until then): the initial state is baked
|
|
315
|
+
// into the release, and a day read off the build machine's clock made every build of unchanged
|
|
316
|
+
// source a different release (#3268, the `nondeterministic-initial-state` lint).
|
|
317
|
+
const focus = (s._picked ? s.selectedDate : "") || soonest || (status === "ready" ? dayKey(new Date()) : "");
|
|
315
318
|
const dayAppointments = all.filter((r) => r._day === focus);
|
|
316
319
|
const dayEvents = eventsOf(dayAppointments);
|
|
317
320
|
const ready = status === "ready";
|
|
@@ -11,9 +11,14 @@ const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } })
|
|
|
11
11
|
const validDate = (value: unknown): value is string => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && dayKey(new Date(`${value}T12:00:00`)) === value;
|
|
12
12
|
const views = localPreferences("booking.schedule", value => ["agenda", "day", "month"].includes(String(value)) ? String(value) : "agenda");
|
|
13
13
|
let mode = validDate(input?.date) ? "day" : views.read();
|
|
14
|
-
|
|
14
|
+
// "" until the schedule is first shown, NOT today's date: the release build hashes this worker's
|
|
15
|
+
// initial state, so a date read off the build machine's clock made every build of unchanged
|
|
16
|
+
// source a different release (#3268, the `nondeterministic-initial-state` lint). `derive()` fills
|
|
17
|
+
// in today the first time the schedule is visible, which is never at build time (no member).
|
|
18
|
+
let selectedDate = validDate(input?.date) ? input.date : "";
|
|
19
|
+
const today = () => dayKey(new Date());
|
|
15
20
|
let pageMember: MemberAccess | undefined;
|
|
16
|
-
let queryNow =
|
|
21
|
+
let queryNow = ""; // set by the first-page fetch, never at boot (see selectedDate)
|
|
17
22
|
let cancelTarget: { row: MemberBookingRow; member: MemberAccess } | undefined;
|
|
18
23
|
let cancelling = false;
|
|
19
24
|
interface S {
|
|
@@ -26,11 +31,12 @@ interface S {
|
|
|
26
31
|
nextCursor?: string; complete?: boolean; loadingMore?: boolean; pageError?: boolean; total?: number;
|
|
27
32
|
}
|
|
28
33
|
function derive(s: S): S {
|
|
29
|
-
const
|
|
34
|
+
const hideSchedule = !s.isMember || s.isLoading;
|
|
35
|
+
if (!selectedDate && !hideSchedule) selectedDate = today();
|
|
36
|
+
const dayRows = selectedDate ? s.rows.filter(row => row.day === selectedDate) : [];
|
|
30
37
|
const counts = new Map<string, number>();
|
|
31
38
|
for (const row of s.rows) if (row.day) counts.set(row.day, (counts.get(row.day) ?? 0) + 1);
|
|
32
|
-
|
|
33
|
-
return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: dayLabel(selectedDate), hideSchedule,
|
|
39
|
+
return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: selectedDate ? dayLabel(selectedDate) : "", hideSchedule,
|
|
34
40
|
hideAgenda: hideSchedule || mode !== "agenda", hideCalendar: hideSchedule || mode === "agenda", hideMonth: mode !== "month",
|
|
35
41
|
decorations: [...counts].map(([date, n]) => ({ date, dotCount: Math.min(n, 3), dotColorToken: "accentPrimary" })),
|
|
36
42
|
dayEmptyText: dayRows.length ? "" : s.complete ? copyParam("bookings.emptyDay", "No bookings on this day.") : copyParam("bookings.emptyLoadedDay", "No bookings on this day in loaded results. Load more to check later pages."),
|
|
@@ -48,7 +54,7 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
|
|
|
48
54
|
async fetch(criteria, cursor) {
|
|
49
55
|
const member = await memberAccess();
|
|
50
56
|
if (cursor && (!pageMember || pageMember.email !== member.email || pageMember.scope !== member.scope)) throw memberAccessChanged();
|
|
51
|
-
if (!cursor) queryNow = new Date().toISOString();
|
|
57
|
+
if (!cursor || !queryNow) queryNow = new Date().toISOString();
|
|
52
58
|
return fetchMemberBookings(criteria, cursor, queryNow, member);
|
|
53
59
|
},
|
|
54
60
|
},
|
|
@@ -65,8 +71,8 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
|
|
|
65
71
|
actions: {
|
|
66
72
|
scheduleView(s, arg) { const value = (arg as { selectedId?: string })?.selectedId; if (["agenda", "day", "month"].includes(value || "")) { mode = value!; views.write(mode); } return derive(s); },
|
|
67
73
|
selectDate(s, arg) { const date = (arg as { date?: unknown })?.date; if (validDate(date)) selectedDate = date; return derive(s); },
|
|
68
|
-
previousDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
|
|
69
|
-
nextDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
|
|
74
|
+
previousDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
|
|
75
|
+
nextDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
|
|
70
76
|
async rebook(s, arg) {
|
|
71
77
|
const member = pageMember, row = s.rows.find(row => row.id === (arg as { id?: string })?.id);
|
|
72
78
|
if (!member || !row?.serviceId) return s;
|
|
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
|
|
|
388
388
|
});
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
|
|
392
|
+
* the initial state, so a greeting read off the build machine's clock made every build of an
|
|
393
|
+
* unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
|
|
394
|
+
* The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
|
|
391
395
|
function welcome(): string {
|
|
392
396
|
const hour = new Date().getHours();
|
|
393
397
|
if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
|
|
@@ -395,6 +399,11 @@ function welcome(): string {
|
|
|
395
399
|
return copyParam("home.greetingEvening", "Good evening");
|
|
396
400
|
}
|
|
397
401
|
|
|
402
|
+
/** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
|
|
403
|
+
function neutralWelcome(): string {
|
|
404
|
+
return copyParam("home.greetingNeutral", "Welcome");
|
|
405
|
+
}
|
|
406
|
+
|
|
398
407
|
function feature(p: StoreProduct): ProductFeature {
|
|
399
408
|
const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
|
|
400
409
|
const image = productImage(p);
|
|
@@ -871,7 +880,9 @@ function baseState(): State {
|
|
|
871
880
|
hideHeroInitial: true,
|
|
872
881
|
hideHeroFallback: true,
|
|
873
882
|
hideFeaturedSkeleton: false,
|
|
874
|
-
|
|
883
|
+
// Not welcome(): that reads the clock, and this state is baked into the release. A neutral
|
|
884
|
+
// greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
|
|
885
|
+
welcome: neutralWelcome(),
|
|
875
886
|
refreshedAt: "",
|
|
876
887
|
effectiveLayout: resolveEffectiveLayout(undefined, 0),
|
|
877
888
|
accentColor: resolveAccent(undefined),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// himi-branded-lite-portfolio — real, public Wix Portfolio projects.
|
|
2
2
|
// Empty and error states stay explicit; media IDs are normalized for every host.
|
|
3
3
|
import { defineService } from "../_shared/collection-service.js";
|
|
4
|
-
import { listProjects, type WixPortfolioProject } from "@himalaya/wix-portfolio";
|
|
4
|
+
import { listProjects, portfolioCoverRef, type WixPortfolioProject } from "@himalaya/wix-portfolio";
|
|
5
5
|
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
6
6
|
|
|
7
7
|
interface Row {
|
|
@@ -24,7 +24,7 @@ interface S {
|
|
|
24
24
|
|
|
25
25
|
function norm(project: WixPortfolioProject): Row {
|
|
26
26
|
const title = (project.title || "Project").trim() || "Project";
|
|
27
|
-
const image = wixMediaUrl(project.coverImage
|
|
27
|
+
const image = wixMediaUrl(portfolioCoverRef(project.coverImage));
|
|
28
28
|
return {
|
|
29
29
|
id: project.id || project.slug || title,
|
|
30
30
|
title,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The public project row is resolved again from Wix, then its ordered project
|
|
3
3
|
// items supply the actual image/video/text story. No sample project is invented.
|
|
4
4
|
import { defineService } from "@himalaya/state";
|
|
5
|
-
import { listProjectItems, listProjects, type WixPortfolioProjectItem } from "@himalaya/wix-portfolio";
|
|
5
|
+
import { listProjectItems, listProjects, portfolioCoverRef, type WixPortfolioProjectItem } from "@himalaya/wix-portfolio";
|
|
6
6
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
7
7
|
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
8
8
|
import { presence, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
@@ -48,7 +48,7 @@ function inputId(): string {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function imageOf(item: WixPortfolioProjectItem): string {
|
|
51
|
-
return wixMediaUrl(item.image
|
|
51
|
+
return wixMediaUrl(portfolioCoverRef(item.image), WIX_DETAIL_IMAGE);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function row(item: WixPortfolioProjectItem, index: number): MediaRow {
|
|
@@ -133,7 +133,7 @@ const svc = defineService<State, { load: (state: State, arg?: unknown) => Promis
|
|
|
133
133
|
title: (project.title || copyParam("portfolioProject.projectFallbackName", "Project")).trim(),
|
|
134
134
|
initial: ((project.title || "P").trim()[0] || "P").toUpperCase(),
|
|
135
135
|
description: (project.description ?? "").trim(),
|
|
136
|
-
coverImage: wixMediaUrl(project.coverImage
|
|
136
|
+
coverImage: wixMediaUrl(portfolioCoverRef(project.coverImage), WIX_DETAIL_IMAGE),
|
|
137
137
|
items,
|
|
138
138
|
emptyText: items.length ? "" : copyParam("portfolioProject.noMediaText", "This project has no published media yet."),
|
|
139
139
|
...presence("found"),
|
|
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
|
|
|
388
388
|
});
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
|
|
392
|
+
* the initial state, so a greeting read off the build machine's clock made every build of an
|
|
393
|
+
* unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
|
|
394
|
+
* The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
|
|
391
395
|
function welcome(): string {
|
|
392
396
|
const hour = new Date().getHours();
|
|
393
397
|
if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
|
|
@@ -395,6 +399,11 @@ function welcome(): string {
|
|
|
395
399
|
return copyParam("home.greetingEvening", "Good evening");
|
|
396
400
|
}
|
|
397
401
|
|
|
402
|
+
/** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
|
|
403
|
+
function neutralWelcome(): string {
|
|
404
|
+
return copyParam("home.greetingNeutral", "Welcome");
|
|
405
|
+
}
|
|
406
|
+
|
|
398
407
|
function feature(p: StoreProduct): ProductFeature {
|
|
399
408
|
const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
|
|
400
409
|
const image = productImage(p);
|
|
@@ -871,7 +880,9 @@ function baseState(): State {
|
|
|
871
880
|
hideHeroInitial: true,
|
|
872
881
|
hideHeroFallback: true,
|
|
873
882
|
hideFeaturedSkeleton: false,
|
|
874
|
-
|
|
883
|
+
// Not welcome(): that reads the clock, and this state is baked into the release. A neutral
|
|
884
|
+
// greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
|
|
885
|
+
welcome: neutralWelcome(),
|
|
875
886
|
refreshedAt: "",
|
|
876
887
|
effectiveLayout: resolveEffectiveLayout(undefined, 0),
|
|
877
888
|
accentColor: resolveAccent(undefined),
|
|
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
|
|
|
8
8
|
import { queryReviews, getReview } from '@himalaya/wix-reviews';
|
|
9
9
|
import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
|
|
10
10
|
import { queryForms, getForm } from '@himalaya/wix-forms';
|
|
11
|
-
import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
|
|
11
|
+
import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
|
|
12
12
|
import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
|
|
13
13
|
import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
|
|
14
14
|
import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
|
|
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
208
208
|
details = [['Namespace', subtitle]];
|
|
209
209
|
break;
|
|
210
210
|
case 'portfolio':
|
|
211
|
-
title = field(raw, 'title'); subtitle = field(raw, 'description'); image =
|
|
211
|
+
title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
|
|
212
212
|
statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
|
|
213
213
|
break;
|
|
214
214
|
case 'donationCampaigns':
|
|
@@ -388,6 +388,10 @@ function resolveEffectiveLayout(arg: unknown, productCount: number): LayoutId {
|
|
|
388
388
|
});
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/** The time-of-day greeting. Read at REFRESH time only, never at boot: the release build hashes
|
|
392
|
+
* the initial state, so a greeting read off the build machine's clock made every build of an
|
|
393
|
+
* unchanged template a different release (#3268, the `nondeterministic-initial-state` lint).
|
|
394
|
+
* The initial state carries `neutralWelcome()` instead, and the first refresh swaps this in. */
|
|
391
395
|
function welcome(): string {
|
|
392
396
|
const hour = new Date().getHours();
|
|
393
397
|
if (hour < 12) return copyParam("home.greetingMorning", "Good morning");
|
|
@@ -395,6 +399,11 @@ function welcome(): string {
|
|
|
395
399
|
return copyParam("home.greetingEvening", "Good evening");
|
|
396
400
|
}
|
|
397
401
|
|
|
402
|
+
/** The boot-time greeting: clock-free, so the baked initial state is the same on every build. */
|
|
403
|
+
function neutralWelcome(): string {
|
|
404
|
+
return copyParam("home.greetingNeutral", "Welcome");
|
|
405
|
+
}
|
|
406
|
+
|
|
398
407
|
function feature(p: StoreProduct): ProductFeature {
|
|
399
408
|
const name = cleanTitle(p.name) || copyParam("home.productFallbackName", "Featured item");
|
|
400
409
|
const image = productImage(p);
|
|
@@ -871,7 +880,9 @@ function baseState(): State {
|
|
|
871
880
|
hideHeroInitial: true,
|
|
872
881
|
hideHeroFallback: true,
|
|
873
882
|
hideFeaturedSkeleton: false,
|
|
874
|
-
|
|
883
|
+
// Not welcome(): that reads the clock, and this state is baked into the release. A neutral
|
|
884
|
+
// greeting keeps the hero pill painted (same size, no reflow) until the first refresh.
|
|
885
|
+
welcome: neutralWelcome(),
|
|
875
886
|
refreshedAt: "",
|
|
876
887
|
effectiveLayout: resolveEffectiveLayout(undefined, 0),
|
|
877
888
|
accentColor: resolveAccent(undefined),
|
|
@@ -8,7 +8,7 @@ import { queryPrograms, getProgram } from '@himalaya/wix-online-programs';
|
|
|
8
8
|
import { queryReviews, getReview } from '@himalaya/wix-reviews';
|
|
9
9
|
import { queryReservations, getReservation } from '@himalaya/wix-table-reservations';
|
|
10
10
|
import { queryForms, getForm } from '@himalaya/wix-forms';
|
|
11
|
-
import { listProjects, listProjectItems } from '@himalaya/wix-portfolio';
|
|
11
|
+
import { listProjects, listProjectItems, portfolioCoverRef, type WixMediaImage } from '@himalaya/wix-portfolio';
|
|
12
12
|
import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
|
|
13
13
|
import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
|
|
14
14
|
import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
|
|
@@ -208,7 +208,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
208
208
|
details = [['Namespace', subtitle]];
|
|
209
209
|
break;
|
|
210
210
|
case 'portfolio':
|
|
211
|
-
title = field(raw, 'title'); subtitle = field(raw, 'description'); image =
|
|
211
|
+
title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
|
|
212
212
|
statusLabel = typeof at(raw, 'hidden') === 'boolean' ? at(raw, 'hidden') ? 'Hidden' : 'Visible' : '';
|
|
213
213
|
break;
|
|
214
214
|
case 'donationCampaigns':
|
|
@@ -11,9 +11,14 @@ const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } })
|
|
|
11
11
|
const validDate = (value: unknown): value is string => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && dayKey(new Date(`${value}T12:00:00`)) === value;
|
|
12
12
|
const views = localPreferences("booking.schedule", value => ["agenda", "day", "month"].includes(String(value)) ? String(value) : "agenda");
|
|
13
13
|
let mode = validDate(input?.date) ? "day" : views.read();
|
|
14
|
-
|
|
14
|
+
// "" until the schedule is first shown, NOT today's date: the release build hashes this worker's
|
|
15
|
+
// initial state, so a date read off the build machine's clock made every build of unchanged
|
|
16
|
+
// source a different release (#3268, the `nondeterministic-initial-state` lint). `derive()` fills
|
|
17
|
+
// in today the first time the schedule is visible, which is never at build time (no member).
|
|
18
|
+
let selectedDate = validDate(input?.date) ? input.date : "";
|
|
19
|
+
const today = () => dayKey(new Date());
|
|
15
20
|
let pageMember: MemberAccess | undefined;
|
|
16
|
-
let queryNow =
|
|
21
|
+
let queryNow = ""; // set by the first-page fetch, never at boot (see selectedDate)
|
|
17
22
|
let cancelTarget: { row: MemberBookingRow; member: MemberAccess } | undefined;
|
|
18
23
|
let cancelling = false;
|
|
19
24
|
interface S {
|
|
@@ -26,11 +31,12 @@ interface S {
|
|
|
26
31
|
nextCursor?: string; complete?: boolean; loadingMore?: boolean; pageError?: boolean; total?: number;
|
|
27
32
|
}
|
|
28
33
|
function derive(s: S): S {
|
|
29
|
-
const
|
|
34
|
+
const hideSchedule = !s.isMember || s.isLoading;
|
|
35
|
+
if (!selectedDate && !hideSchedule) selectedDate = today();
|
|
36
|
+
const dayRows = selectedDate ? s.rows.filter(row => row.day === selectedDate) : [];
|
|
30
37
|
const counts = new Map<string, number>();
|
|
31
38
|
for (const row of s.rows) if (row.day) counts.set(row.day, (counts.get(row.day) ?? 0) + 1);
|
|
32
|
-
|
|
33
|
-
return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: dayLabel(selectedDate), hideSchedule,
|
|
39
|
+
return { ...s, scheduleMode: mode, selectedDate, dayRows, dayHeading: selectedDate ? dayLabel(selectedDate) : "", hideSchedule,
|
|
34
40
|
hideAgenda: hideSchedule || mode !== "agenda", hideCalendar: hideSchedule || mode === "agenda", hideMonth: mode !== "month",
|
|
35
41
|
decorations: [...counts].map(([date, n]) => ({ date, dotCount: Math.min(n, 3), dotColorToken: "accentPrimary" })),
|
|
36
42
|
dayEmptyText: dayRows.length ? "" : s.complete ? copyParam("bookings.emptyDay", "No bookings on this day.") : copyParam("bookings.emptyLoadedDay", "No bookings on this day in loaded results. Load more to check later pages."),
|
|
@@ -48,7 +54,7 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
|
|
|
48
54
|
async fetch(criteria, cursor) {
|
|
49
55
|
const member = await memberAccess();
|
|
50
56
|
if (cursor && (!pageMember || pageMember.email !== member.email || pageMember.scope !== member.scope)) throw memberAccessChanged();
|
|
51
|
-
if (!cursor) queryNow = new Date().toISOString();
|
|
57
|
+
if (!cursor || !queryNow) queryNow = new Date().toISOString();
|
|
52
58
|
return fetchMemberBookings(criteria, cursor, queryNow, member);
|
|
53
59
|
},
|
|
54
60
|
},
|
|
@@ -65,8 +71,8 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
|
|
|
65
71
|
actions: {
|
|
66
72
|
scheduleView(s, arg) { const value = (arg as { selectedId?: string })?.selectedId; if (["agenda", "day", "month"].includes(value || "")) { mode = value!; views.write(mode); } return derive(s); },
|
|
67
73
|
selectDate(s, arg) { const date = (arg as { date?: unknown })?.date; if (validDate(date)) selectedDate = date; return derive(s); },
|
|
68
|
-
previousDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
|
|
69
|
-
nextDay(s) { const date = new Date(`${selectedDate}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
|
|
74
|
+
previousDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() - 1); selectedDate = dayKey(date); return derive(s); },
|
|
75
|
+
nextDay(s) { const date = new Date(`${selectedDate || today()}T12:00:00`); date.setDate(date.getDate() + 1); selectedDate = dayKey(date); return derive(s); },
|
|
70
76
|
async rebook(s, arg) {
|
|
71
77
|
const member = pageMember, row = s.rows.find(row => row.id === (arg as { id?: string })?.id);
|
|
72
78
|
if (!member || !row?.serviceId) return s;
|