@bygghammar/cms-platform 0.96.0 → 0.97.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/CHANGELOG.md +49 -0
- package/docs/PAGES.md +45 -2
- package/package.json +3 -1
- package/src/admin/describe-log.ts +2 -0
- package/src/backoffice/BookingLandingSection.tsx +672 -0
- package/src/backoffice/BookingSettingsCard.tsx +34 -4
- package/src/backoffice/routes/host.ts +7 -0
- package/src/backoffice/routes/index.tsx +22 -3
- package/src/blocks/components/CountdownBlock.tsx +3 -1
- package/src/blocks/components/CtaBannerBlock.tsx +8 -2
- package/src/blocks/components/HeroFullBlock.tsx +7 -1
- package/src/blocks/components/IconGridBlock.tsx +5 -1
- package/src/blocks/components/InstagramBlock.tsx +5 -1
- package/src/blocks/components/InstagramTiles.tsx +3 -1
- package/src/blocks/components/LinkTilesBlock.tsx +8 -1
- package/src/blocks/components/MediaSplitBlock.tsx +8 -3
- package/src/blocks/components/PriceTableBlock.tsx +6 -5
- package/src/blocks/components/TripListBlock.tsx +5 -1
- package/src/cms/booking-landing.ts +359 -0
- package/src/cms/booking-links.ts +24 -0
- package/src/cms/page-visibility.ts +67 -1
- package/src/navigation/visibility.ts +65 -3
- package/src/popups/runtime.tsx +75 -32
- package/src/server/page-visibility-core.ts +231 -5
- package/src/server/schemas.ts +45 -0
- package/src/site/BookingOffNotice.tsx +73 -0
- package/src/site/PageRenderer.tsx +59 -3
- package/src/site/link-visibility.tsx +21 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* a published page or an unsaved draft. The registry is passed in, so a site
|
|
6
6
|
* only supplies its own block definitions.
|
|
7
7
|
*/
|
|
8
|
-
import { useMemo } from "react";
|
|
8
|
+
import { Fragment, useMemo, type ReactNode } from "react";
|
|
9
9
|
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
|
|
10
10
|
|
|
11
11
|
import { publishedPageStaleTime } from "../cms/page-publication";
|
|
@@ -116,6 +116,32 @@ function RenderBlock({
|
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** Block types that open a page full-bleed, under the header. */
|
|
120
|
+
export const DEFAULT_HERO_BLOCK_TYPES: readonly string[] = Object.freeze(["heroFull"]);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Where a notice goes among a page's top-level blocks: right after a leading
|
|
124
|
+
* hero (which sits under the header and must stay first), otherwise first. A
|
|
125
|
+
* global block counts by its own first visible block.
|
|
126
|
+
*/
|
|
127
|
+
export function noticeIndex(
|
|
128
|
+
blocks: readonly BlockInstance[],
|
|
129
|
+
globals: Record<string, BlockInstance[]>,
|
|
130
|
+
heroTypes: readonly string[] = DEFAULT_HERO_BLOCK_TYPES,
|
|
131
|
+
): number {
|
|
132
|
+
const leadOf = (block: BlockInstance): BlockInstance | null => {
|
|
133
|
+
if (block.visible === false) return null;
|
|
134
|
+
if (block.type !== "globalRef") return block;
|
|
135
|
+
const key = String((block.props as BlockProps | undefined)?.sectionKey ?? "");
|
|
136
|
+
return (globals[key] ?? []).find((inner) => inner.visible !== false) ?? null;
|
|
137
|
+
};
|
|
138
|
+
for (let i = 0; i < blocks.length; i += 1) {
|
|
139
|
+
const lead = leadOf(blocks[i]!);
|
|
140
|
+
if (lead) return heroTypes.includes(lead.type) ? i + 1 : 0;
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
119
145
|
/** Render an in-memory block tree — used by the builder preview and drafts. */
|
|
120
146
|
export function DraftPageRendererView({
|
|
121
147
|
blocks,
|
|
@@ -123,12 +149,15 @@ export function DraftPageRendererView({
|
|
|
123
149
|
registry,
|
|
124
150
|
edit,
|
|
125
151
|
selectedId,
|
|
152
|
+
insert,
|
|
126
153
|
}: {
|
|
127
154
|
blocks: BlockInstance[];
|
|
128
155
|
globals: Record<string, BlockInstance[]>;
|
|
129
156
|
registry: BlockRegistry;
|
|
130
157
|
edit?: boolean;
|
|
131
158
|
selectedId?: string | null;
|
|
159
|
+
/** Something that is not a block, rendered among the top-level blocks at `at`. */
|
|
160
|
+
insert?: { at: number; node: ReactNode };
|
|
132
161
|
}) {
|
|
133
162
|
// Container blocks (`layout`) render their children through this, so nesting
|
|
134
163
|
// uses exactly the same code path — and the same globals, selection
|
|
@@ -154,9 +183,17 @@ export function DraftPageRendererView({
|
|
|
154
183
|
// Every page render gets its own priority scope, so the first first-screen
|
|
155
184
|
// image loads eagerly and the rest stay lazy. In the builder (`edit`) the
|
|
156
185
|
// editor jumps around the page, so no image is treated as the hero.
|
|
186
|
+
const rendered = children.render(blocks);
|
|
187
|
+
if (insert) {
|
|
188
|
+
rendered.splice(
|
|
189
|
+
Math.max(0, Math.min(insert.at, rendered.length)),
|
|
190
|
+
0,
|
|
191
|
+
<Fragment key="__inserted">{insert.node}</Fragment>,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
157
194
|
return (
|
|
158
195
|
<PriorityMediaProvider enabled={!edit}>
|
|
159
|
-
<BlockChildrenProvider value={children}>{
|
|
196
|
+
<BlockChildrenProvider value={children}>{rendered}</BlockChildrenProvider>
|
|
160
197
|
</PriorityMediaProvider>
|
|
161
198
|
);
|
|
162
199
|
}
|
|
@@ -212,14 +249,24 @@ export function createPageRenderer(
|
|
|
212
249
|
* fetched. The cached original is not changed, so a later policy can show
|
|
213
250
|
* it again. Pass a stable function, or none.
|
|
214
251
|
*/
|
|
252
|
+
/*
|
|
253
|
+
* `notice` is shown among the page's blocks — after a leading hero (one of
|
|
254
|
+
* `heroBlockTypes`), otherwise first — without changing the page itself: a
|
|
255
|
+
* booking-off landing page's notice, say. It is decided by the caller from
|
|
256
|
+
* the current policy, like `transform`.
|
|
257
|
+
*/
|
|
215
258
|
function PageRenderer({
|
|
216
259
|
pageKey,
|
|
217
260
|
variant,
|
|
218
261
|
transform,
|
|
262
|
+
notice,
|
|
263
|
+
heroBlockTypes,
|
|
219
264
|
}: {
|
|
220
265
|
pageKey: string;
|
|
221
266
|
variant?: string;
|
|
222
267
|
transform?: PageTransform;
|
|
268
|
+
notice?: ReactNode;
|
|
269
|
+
heroBlockTypes?: readonly string[];
|
|
223
270
|
}) {
|
|
224
271
|
const { data } = useSuspenseQuery(pageQueryOptions(pageKey, variant));
|
|
225
272
|
const fetched = data as RenderedPage | null;
|
|
@@ -228,8 +275,17 @@ export function createPageRenderer(
|
|
|
228
275
|
[fetched, transform],
|
|
229
276
|
);
|
|
230
277
|
if (!page) return null;
|
|
278
|
+
const insert =
|
|
279
|
+
notice != null && notice !== false
|
|
280
|
+
? { at: noticeIndex(page.blocks, page.globals, heroBlockTypes), node: notice }
|
|
281
|
+
: undefined;
|
|
231
282
|
return (
|
|
232
|
-
<DraftPageRendererView
|
|
283
|
+
<DraftPageRendererView
|
|
284
|
+
blocks={page.blocks}
|
|
285
|
+
globals={page.globals}
|
|
286
|
+
registry={registry}
|
|
287
|
+
insert={insert}
|
|
288
|
+
/>
|
|
233
289
|
);
|
|
234
290
|
}
|
|
235
291
|
|
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
* provider nothing is filtered, which is what the backoffice and any screen
|
|
8
8
|
* that renders the chrome outside the public site get.
|
|
9
9
|
*/
|
|
10
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
10
|
+
import { createContext, useCallback, useContext, type ReactNode } from "react";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
OPEN_LINK_POLICY,
|
|
14
|
+
isHiddenFromDiscovery,
|
|
15
|
+
type LinkVisibilityPolicy,
|
|
16
|
+
} from "../navigation/visibility";
|
|
13
17
|
|
|
14
18
|
const LinkVisibilityContext = createContext<LinkVisibilityPolicy>(OPEN_LINK_POLICY);
|
|
15
19
|
|
|
@@ -26,3 +30,18 @@ export function LinkVisibilityProvider({
|
|
|
26
30
|
export function useLinkVisibility(): LinkVisibilityPolicy {
|
|
27
31
|
return useContext(LinkVisibilityContext);
|
|
28
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The one rule every structured link on a page follows — buttons, cards,
|
|
36
|
+
* tiles, slide links: may it be offered right now? False only for a link to a
|
|
37
|
+
* page hidden from the website while booking is hidden (see
|
|
38
|
+
* `isHiddenFromDiscovery`); external links, anchors, `mailto:`/`tel:` and
|
|
39
|
+
* every other page are always offered. Read from the policy the server
|
|
40
|
+
* rendered with and every client navigation refreshes, so the server's HTML
|
|
41
|
+
* and the browser agree. Without a provider (the builder, the backoffice)
|
|
42
|
+
* everything is offered. Running text is not filtered: see docs/PAGES.md.
|
|
43
|
+
*/
|
|
44
|
+
export function useDiscoverableLink(): (href: string | null | undefined) => boolean {
|
|
45
|
+
const policy = useLinkVisibility();
|
|
46
|
+
return useCallback((href) => !isHiddenFromDiscovery(href, policy), [policy]);
|
|
47
|
+
}
|