@escape-game-over/atlas 0.1.101 → 0.1.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/analytics/storage.ts +53 -3
- package/src/astro/FourEscape.astro +67 -0
- package/src/booking/four-escape.ts +81 -0
- package/src/hours.ts +1 -0
- package/src/index.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escape-game-over/atlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.103",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./astro/consent-banner": "./src/astro/ConsentBanner.astro",
|
|
22
22
|
"./astro/document": "./src/astro/Document.astro",
|
|
23
23
|
"./astro/element": "./src/astro/AtlasElement.astro",
|
|
24
|
+
"./astro/four-escape": "./src/astro/FourEscape.astro",
|
|
24
25
|
"./astro/google-map": "./src/astro/GoogleMap.astro",
|
|
25
26
|
"./astro/image": "./src/astro/Image.astro",
|
|
26
27
|
"./astro/phone": "./src/astro/Phone.astro",
|
package/src/analytics/storage.ts
CHANGED
|
@@ -35,6 +35,13 @@ export type StorageVendor =
|
|
|
35
35
|
| "drip"
|
|
36
36
|
| "clarity";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* A booking widget a site embeds, which keeps its own storage on the site's
|
|
40
|
+
* domain: rows the analytics settings cannot know about, so the site names
|
|
41
|
+
* the widgets it embeds.
|
|
42
|
+
*/
|
|
43
|
+
export type BookingWidget = "4escape";
|
|
44
|
+
|
|
38
45
|
/**
|
|
39
46
|
* Each vendor's product as it calls itself, for a table's "set by" column.
|
|
40
47
|
* Names, not copy: the same in every language, so a project translates only
|
|
@@ -50,7 +57,8 @@ export const STORAGE_VENDOR_NAMES = {
|
|
|
50
57
|
axon: "AppLovin Axon",
|
|
51
58
|
drip: "Drip",
|
|
52
59
|
clarity: "Microsoft Clarity",
|
|
53
|
-
|
|
60
|
+
"4escape": "4escape",
|
|
61
|
+
} as const satisfies Record<StorageVendor | BookingWidget, string>;
|
|
54
62
|
|
|
55
63
|
/** A span `Intl.NumberFormat` can word in any locale: `{ 2, "year" }`. */
|
|
56
64
|
export interface Lifetime {
|
|
@@ -64,15 +72,18 @@ type OwnRow = `${VendorWithOwn}-own`;
|
|
|
64
72
|
/** The keys Atlas itself sets. */
|
|
65
73
|
export type AtlasStoredId = "atlas-locale" | "atlas-campaign" | "atlas-consent";
|
|
66
74
|
|
|
75
|
+
/** A booking widget's rows: what it keeps, and what it keeps for the tab. */
|
|
76
|
+
type BookingRow = BookingWidget | `${BookingWidget}-tab`;
|
|
77
|
+
|
|
67
78
|
/** One row's id: what a project keys the row's wording by. */
|
|
68
|
-
export type StoredId = AtlasStoredId | StorageVendor | OwnRow;
|
|
79
|
+
export type StoredId = AtlasStoredId | StorageVendor | OwnRow | BookingRow;
|
|
69
80
|
|
|
70
81
|
export interface StoredItem {
|
|
71
82
|
readonly id: StoredId;
|
|
72
83
|
/** As the browser shows them; a pattern like `_ga_<id>` where the vendor appends one. */
|
|
73
84
|
readonly names: readonly string[];
|
|
74
85
|
/** Who sets it: this site, or the vendor. */
|
|
75
|
-
readonly provider: "site" | StorageVendor;
|
|
86
|
+
readonly provider: "site" | StorageVendor | BookingWidget;
|
|
76
87
|
/** Set on the vendor's own domains rather than the site's; absent for the site's. */
|
|
77
88
|
readonly domains?: readonly string[];
|
|
78
89
|
/** A span; `tab` for session storage; `kept` until the visitor clears it. */
|
|
@@ -86,6 +97,8 @@ export interface StoredItemsOptions {
|
|
|
86
97
|
readonly rememberLocale?: boolean;
|
|
87
98
|
/** As passed to `Document`, and with its default: `false`. */
|
|
88
99
|
readonly rememberCampaign?: boolean;
|
|
100
|
+
/** The booking widgets the site embeds anywhere. */
|
|
101
|
+
readonly booking?: readonly BookingWidget[];
|
|
89
102
|
/**
|
|
90
103
|
* Rows the site sets first, then by what has to be granted — nothing,
|
|
91
104
|
* analytics, marketing — and otherwise in catalog order. Off, the rows
|
|
@@ -256,6 +269,24 @@ const VENDOR_ORDER: readonly StorageVendor[] = [
|
|
|
256
269
|
];
|
|
257
270
|
|
|
258
271
|
/** The vendors a deployment's settings load, in catalog order. */
|
|
272
|
+
/**
|
|
273
|
+
* Each booking widget's storage, all on the site's domain and none of it
|
|
274
|
+
* cookies: what its script left after rendering, seen live 2026-11-11, since
|
|
275
|
+
* the vendor documents none. Functional — the language, the cart — so nothing
|
|
276
|
+
* has to be granted first.
|
|
277
|
+
*/
|
|
278
|
+
const BOOKING = {
|
|
279
|
+
// `forescapeAnalytics` stayed empty, with no tracker loaded; listed because
|
|
280
|
+
// it is set.
|
|
281
|
+
"4escape": {
|
|
282
|
+
kept: ["forescapeLayout", "forescapeStoreOrder", "forescapeAnalytics"],
|
|
283
|
+
tab: ["forescape"],
|
|
284
|
+
},
|
|
285
|
+
} as const satisfies Record<
|
|
286
|
+
BookingWidget,
|
|
287
|
+
{ readonly kept: readonly string[]; readonly tab: readonly string[] }
|
|
288
|
+
>;
|
|
289
|
+
|
|
259
290
|
function loadedVendors(
|
|
260
291
|
analytics: AnalyticsSettings | undefined
|
|
261
292
|
): readonly StorageVendor[] {
|
|
@@ -327,6 +358,7 @@ export function storedItems(
|
|
|
327
358
|
const {
|
|
328
359
|
rememberLocale = true,
|
|
329
360
|
rememberCampaign = false,
|
|
361
|
+
booking = [],
|
|
330
362
|
sort = false,
|
|
331
363
|
} = options;
|
|
332
364
|
const asked = consentVendors(analytics).length > 0;
|
|
@@ -382,5 +414,23 @@ export function storedItems(
|
|
|
382
414
|
}
|
|
383
415
|
}
|
|
384
416
|
|
|
417
|
+
for (const widget of new Set(booking)) {
|
|
418
|
+
const entry = BOOKING[widget];
|
|
419
|
+
rows.push({
|
|
420
|
+
id: widget,
|
|
421
|
+
names: entry.kept,
|
|
422
|
+
provider: widget,
|
|
423
|
+
lasts: "kept",
|
|
424
|
+
consent: "none",
|
|
425
|
+
});
|
|
426
|
+
rows.push({
|
|
427
|
+
id: `${widget}-tab`,
|
|
428
|
+
names: entry.tab,
|
|
429
|
+
provider: widget,
|
|
430
|
+
lasts: "tab",
|
|
431
|
+
consent: "none",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
385
435
|
return sort ? rows.toSorted(compareStoredItems) : rows;
|
|
386
436
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A 4escape catalog, and its cart below it unless `cart={false}`: the store's
|
|
4
|
+
* games and free slots, booked without leaving the page. See `four-escape.ts`
|
|
5
|
+
* for what the widgets are and where their contract comes from.
|
|
6
|
+
*
|
|
7
|
+
* `locale` is the page's; the widgets speak it when they can, and otherwise
|
|
8
|
+
* the account's language as 4escape sets it. The cart is one per page: give
|
|
9
|
+
* the second catalog on a page `cart={false}`.
|
|
10
|
+
*
|
|
11
|
+
* The widgets draw themselves, in 4escape's style. Attributes go on the
|
|
12
|
+
* element around the catalog, which is the place to give it room; the cart
|
|
13
|
+
* follows it.
|
|
14
|
+
*
|
|
15
|
+
* ```astro
|
|
16
|
+
* <FourEscape widgetId={id} domain="acme.4escape.io" locale={locale} class="my-8" />
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import type { HTMLAttributes } from "astro/types";
|
|
20
|
+
import {
|
|
21
|
+
checkFourEscape,
|
|
22
|
+
FOUR_ESCAPE_SCRIPT,
|
|
23
|
+
FOUR_ESCAPE_STYLESHEET,
|
|
24
|
+
type FourEscapeWidget,
|
|
25
|
+
fourEscapeLanguage,
|
|
26
|
+
fourEscapeSettings,
|
|
27
|
+
} from "../booking/four-escape.ts";
|
|
28
|
+
|
|
29
|
+
type Props = HTMLAttributes<"div"> &
|
|
30
|
+
FourEscapeWidget & {
|
|
31
|
+
readonly locale: string;
|
|
32
|
+
readonly cart?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const { widgetId, domain, locale, cart = true, ...attrs } = Astro.props;
|
|
36
|
+
|
|
37
|
+
checkFourEscape({ widgetId, domain }, Astro.url.pathname);
|
|
38
|
+
|
|
39
|
+
const settings = fourEscapeSettings(domain);
|
|
40
|
+
const language = fourEscapeLanguage(locale);
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<link
|
|
44
|
+
rel="stylesheet"
|
|
45
|
+
href={FOUR_ESCAPE_STYLESHEET}
|
|
46
|
+
crossorigin="anonymous"
|
|
47
|
+
>
|
|
48
|
+
<div {...attrs}>
|
|
49
|
+
<div
|
|
50
|
+
class="forescape-catalog"
|
|
51
|
+
data-widget-id={widgetId}
|
|
52
|
+
data-settings={settings}
|
|
53
|
+
data-language={language}
|
|
54
|
+
></div>
|
|
55
|
+
</div>
|
|
56
|
+
{cart && (
|
|
57
|
+
<div
|
|
58
|
+
class='forescape-cart'
|
|
59
|
+
data-settings={settings}
|
|
60
|
+
data-language={language}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
<script
|
|
64
|
+
is:inline
|
|
65
|
+
type="module"
|
|
66
|
+
src={FOUR_ESCAPE_SCRIPT}
|
|
67
|
+
></script>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { invalidId, UUID, type Uuid } from "../analytics/ids.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 4escape's booking widgets: a catalog of a store's games and slots, and the
|
|
5
|
+
* cart a booking goes through. Not 4escape's older iframe, which cannot share
|
|
6
|
+
* a page with them; the widgets are what 4escape builds for an account, so
|
|
7
|
+
* the catalog's id comes from 4escape.
|
|
8
|
+
*
|
|
9
|
+
* Plain markup and one module script, which the browser runs once however
|
|
10
|
+
* many widgets a page has: there is nothing to load on demand. They set no
|
|
11
|
+
* cookies and need no consent; what they keep is in `storedItems`.
|
|
12
|
+
*
|
|
13
|
+
* Their help shows the embed only as screenshots, so the contract here is the
|
|
14
|
+
* widget script's own, read 2026-11-11: it looks for `.forescape-<type>`
|
|
15
|
+
* elements and reads `data-settings` as `b64.` plus base64 JSON with the
|
|
16
|
+
* `domain`. The language is `data-language`, as the help gives it for the
|
|
17
|
+
* iframe; the widgets read it the same way.
|
|
18
|
+
*
|
|
19
|
+
* @see https://help.4escape.io/en/articles/259019-how-to-integrate-the-reservation-widget-on-your-website
|
|
20
|
+
*/
|
|
21
|
+
export interface FourEscapeWidget {
|
|
22
|
+
/** The catalog's id, from 4escape. */
|
|
23
|
+
readonly widgetId: Uuid;
|
|
24
|
+
/** The account's 4escape address: `acme.4escape.io`. */
|
|
25
|
+
readonly domain: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The languages the widgets are translated into, as `data-language` takes them. */
|
|
29
|
+
export const FOUR_ESCAPE_LANGUAGES = [
|
|
30
|
+
"fr",
|
|
31
|
+
"en",
|
|
32
|
+
"es",
|
|
33
|
+
"nl",
|
|
34
|
+
"de",
|
|
35
|
+
"it",
|
|
36
|
+
"pt",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export type FourEscapeLanguage = (typeof FOUR_ESCAPE_LANGUAGES)[number];
|
|
40
|
+
|
|
41
|
+
/** The stylesheet and script the widgets need, as 4escape serves them. */
|
|
42
|
+
export const FOUR_ESCAPE_STYLESHEET = "https://widgets.4escape.app/main.css";
|
|
43
|
+
export const FOUR_ESCAPE_SCRIPT = "https://widgets-mirror.4escape.app/main.js";
|
|
44
|
+
|
|
45
|
+
/** An account's address on 4escape. */
|
|
46
|
+
const FOUR_ESCAPE_DOMAIN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.4escape\.io$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fails the build on a widget that would render nothing: a catalog id that is
|
|
50
|
+
* not a UUID, or a domain that is not an account's on 4escape.
|
|
51
|
+
*/
|
|
52
|
+
export function checkFourEscape(widget: FourEscapeWidget, at: string): void {
|
|
53
|
+
if (!UUID.test(widget.widgetId)) {
|
|
54
|
+
invalidId(
|
|
55
|
+
at,
|
|
56
|
+
`4escape widget id "${widget.widgetId}" is not a UUID, so the catalog would render nothing. 4escape gives it with the widget's embed code.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (!FOUR_ESCAPE_DOMAIN.test(widget.domain)) {
|
|
60
|
+
invalidId(
|
|
61
|
+
at,
|
|
62
|
+
`4escape domain "${widget.domain}" is not an account's address on 4escape, like "acme.4escape.io".`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `data-settings` for an account: `b64.` and the settings as base64 JSON. */
|
|
68
|
+
export function fourEscapeSettings(domain: string): string {
|
|
69
|
+
return `b64.${btoa(JSON.stringify({ domain }))}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The widgets' language for a page's locale, or `undefined` for one they are
|
|
74
|
+
* not translated into: the account's own language then, as 4escape sets it.
|
|
75
|
+
*/
|
|
76
|
+
export function fourEscapeLanguage(
|
|
77
|
+
locale: string
|
|
78
|
+
): FourEscapeLanguage | undefined {
|
|
79
|
+
const { language } = new Intl.Locale(locale);
|
|
80
|
+
return FOUR_ESCAPE_LANGUAGES.find((supported) => supported === language);
|
|
81
|
+
}
|
package/src/hours.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ export {
|
|
|
45
45
|
} from "./analytics/index.ts";
|
|
46
46
|
export {
|
|
47
47
|
type AtlasStoredId,
|
|
48
|
+
type BookingWidget,
|
|
48
49
|
compareStoredItems,
|
|
49
50
|
type Lifetime,
|
|
50
51
|
STORAGE_VENDOR_NAMES,
|
|
@@ -55,6 +56,16 @@ export {
|
|
|
55
56
|
storedItems,
|
|
56
57
|
withPurposes,
|
|
57
58
|
} from "./analytics/storage.ts";
|
|
59
|
+
export {
|
|
60
|
+
checkFourEscape,
|
|
61
|
+
FOUR_ESCAPE_LANGUAGES,
|
|
62
|
+
FOUR_ESCAPE_SCRIPT,
|
|
63
|
+
FOUR_ESCAPE_STYLESHEET,
|
|
64
|
+
type FourEscapeLanguage,
|
|
65
|
+
type FourEscapeWidget,
|
|
66
|
+
fourEscapeLanguage,
|
|
67
|
+
fourEscapeSettings,
|
|
68
|
+
} from "./booking/four-escape.ts";
|
|
58
69
|
export type {
|
|
59
70
|
LanguageTag,
|
|
60
71
|
LocaleMeta,
|