@joystyk/catalog 0.0.0 → 0.2.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 +24 -0
- package/README.md +29 -2
- package/dist/advertise.d.ts +82 -0
- package/dist/advertise.js +203 -0
- package/dist/advertise.js.map +1 -0
- package/dist/categories.d.ts +12 -0
- package/dist/categories.js +45 -0
- package/dist/categories.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +9 -0
- package/dist/client.js.map +1 -0
- package/dist/components/advertise.d.ts +4 -0
- package/dist/components/advertise.js +1304 -0
- package/dist/components/advertise.js.map +1 -0
- package/dist/components/app.d.ts +25 -0
- package/dist/components/app.js +106 -0
- package/dist/components/app.js.map +1 -0
- package/dist/components/leaving.d.ts +2 -0
- package/dist/components/leaving.js +104 -0
- package/dist/components/leaving.js.map +1 -0
- package/dist/components/views.d.ts +13 -0
- package/dist/components/views.js +1410 -0
- package/dist/components/views.js.map +1 -0
- package/dist/head.d.ts +6 -0
- package/dist/head.js +33 -0
- package/dist/head.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/labels.d.ts +10 -0
- package/dist/labels.js +27 -0
- package/dist/labels.js.map +1 -0
- package/dist/placeholders.d.ts +3 -0
- package/dist/placeholders.js +7 -0
- package/dist/placeholders.js.map +1 -0
- package/dist/registry.d.ts +46 -0
- package/dist/registry.js +106 -0
- package/dist/registry.js.map +1 -0
- package/dist/routes.d.ts +56 -0
- package/dist/routes.js +89 -0
- package/dist/routes.js.map +1 -0
- package/dist/seo.d.ts +31 -0
- package/dist/seo.js +174 -0
- package/dist/seo.js.map +1 -0
- package/dist/server.d.ts +50 -0
- package/dist/server.js +162 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +202 -0
- package/dist/store.js +136 -0
- package/dist/store.js.map +1 -0
- package/dist/styles.css +1121 -0
- package/dist/wire.d.ts +108 -0
- package/dist/wire.js +74 -0
- package/dist/wire.js.map +1 -0
- package/package.json +63 -11
package/dist/seo.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { playersLabel } from "./labels.js";
|
|
2
|
+
import { ADVERTISE_PATH, ADVERTISE_POLICY_PATH, categoryPath, gamePath } from "./routes.js";
|
|
3
|
+
//#region src/seo.ts
|
|
4
|
+
var SITE_NAME = "Joystyk";
|
|
5
|
+
var SINGLE = [
|
|
6
|
+
"solo",
|
|
7
|
+
"ai",
|
|
8
|
+
"daily"
|
|
9
|
+
];
|
|
10
|
+
var MULTI = [
|
|
11
|
+
"pass-and-play",
|
|
12
|
+
"p2p",
|
|
13
|
+
"server"
|
|
14
|
+
];
|
|
15
|
+
/** schema.org playMode values for a game's modes. */
|
|
16
|
+
function playModes(modes) {
|
|
17
|
+
const out = [];
|
|
18
|
+
if (modes.some((m) => SINGLE.includes(m))) out.push("SinglePlayer");
|
|
19
|
+
if (modes.some((m) => MULTI.includes(m))) out.push("MultiPlayer");
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What a game page calls the game in its title, heading and structured data: the manifest's
|
|
24
|
+
* `pageTitle` ("Klondike Solitaire") when it has one, else the display name. Links, cards
|
|
25
|
+
* and breadcrumbs keep the display name.
|
|
26
|
+
*/
|
|
27
|
+
var pageTitleOf = (game) => game.pageTitle ?? game.name;
|
|
28
|
+
var crumb = (position, name, item) => ({
|
|
29
|
+
"@type": "ListItem",
|
|
30
|
+
position,
|
|
31
|
+
name,
|
|
32
|
+
item
|
|
33
|
+
});
|
|
34
|
+
function gameJsonLd(game, category, origin) {
|
|
35
|
+
const url = `${origin}${gamePath(game.id)}`;
|
|
36
|
+
const crumbs = [crumb(1, "Games", `${origin}/`)];
|
|
37
|
+
if (category !== void 0) crumbs.push(crumb(2, category.name, `${origin}${categoryPath(category.id)}`));
|
|
38
|
+
crumbs.push(crumb(crumbs.length + 1, game.name, url));
|
|
39
|
+
return {
|
|
40
|
+
"@context": "https://schema.org",
|
|
41
|
+
"@graph": [{
|
|
42
|
+
"@type": "VideoGame",
|
|
43
|
+
"@id": `${url}#game`,
|
|
44
|
+
name: pageTitleOf(game),
|
|
45
|
+
...pageTitleOf(game) === game.name ? {} : { alternateName: game.name },
|
|
46
|
+
description: game.description,
|
|
47
|
+
url,
|
|
48
|
+
...category === void 0 ? {} : { genre: category.name },
|
|
49
|
+
keywords: game.tags.join(", "),
|
|
50
|
+
gamePlatform: "Web browser",
|
|
51
|
+
applicationCategory: "Game",
|
|
52
|
+
operatingSystem: "Any",
|
|
53
|
+
playMode: playModes(game.modes),
|
|
54
|
+
numberOfPlayers: {
|
|
55
|
+
"@type": "QuantitativeValue",
|
|
56
|
+
minValue: game.players.min,
|
|
57
|
+
maxValue: game.players.max
|
|
58
|
+
},
|
|
59
|
+
isAccessibleForFree: true,
|
|
60
|
+
publisher: {
|
|
61
|
+
"@type": "Organization",
|
|
62
|
+
name: SITE_NAME,
|
|
63
|
+
url: `${origin}/`
|
|
64
|
+
}
|
|
65
|
+
}, {
|
|
66
|
+
"@type": "BreadcrumbList",
|
|
67
|
+
itemListElement: crumbs
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function categoryJsonLd(category, origin) {
|
|
72
|
+
return {
|
|
73
|
+
"@context": "https://schema.org",
|
|
74
|
+
"@type": "BreadcrumbList",
|
|
75
|
+
itemListElement: [crumb(1, "Games", `${origin}/`), crumb(2, category.name, `${origin}${categoryPath(category.id)}`)]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var NOT_FOUND_HEAD = {
|
|
79
|
+
title: `Page not found | ${SITE_NAME}`,
|
|
80
|
+
description: "There is no page at this address.",
|
|
81
|
+
canonical: null,
|
|
82
|
+
robots: "noindex",
|
|
83
|
+
ogType: "article",
|
|
84
|
+
jsonLd: null
|
|
85
|
+
};
|
|
86
|
+
var loadingHead = {
|
|
87
|
+
...NOT_FOUND_HEAD,
|
|
88
|
+
title: `Loading… | ${SITE_NAME}`,
|
|
89
|
+
description: ""
|
|
90
|
+
};
|
|
91
|
+
var findCategory = (c, id) => c.categories.find((k) => k.id === id);
|
|
92
|
+
/** A game page's description: the summary, then the facts a searcher wants. */
|
|
93
|
+
var gameDescription = (g) => `${g.summary} Free in your browser, ${playersLabel(g.players)}.`;
|
|
94
|
+
function headFor(page, c) {
|
|
95
|
+
switch (page._tag) {
|
|
96
|
+
case "Home": return {
|
|
97
|
+
title: `${SITE_NAME}: free arcade, card and dice games in your browser`,
|
|
98
|
+
description: "Free arcade, card and dice games for every age. Play in your browser with no sign-up and no download: alone, against the computer, or with friends.",
|
|
99
|
+
canonical: `${c.origin}/`,
|
|
100
|
+
robots: "index",
|
|
101
|
+
ogType: "website",
|
|
102
|
+
jsonLd: {
|
|
103
|
+
"@context": "https://schema.org",
|
|
104
|
+
"@type": "WebSite",
|
|
105
|
+
name: SITE_NAME,
|
|
106
|
+
url: `${c.origin}/`
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
case "Category": {
|
|
110
|
+
const category = findCategory(c, page.id);
|
|
111
|
+
if (category === void 0) return NOT_FOUND_HEAD;
|
|
112
|
+
return {
|
|
113
|
+
title: `${category.name} games: play free in your browser | ${SITE_NAME}`,
|
|
114
|
+
description: category.description,
|
|
115
|
+
canonical: `${c.origin}${categoryPath(category.id)}`,
|
|
116
|
+
robots: "index",
|
|
117
|
+
ogType: "article",
|
|
118
|
+
jsonLd: categoryJsonLd(category, c.origin)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
case "Game": {
|
|
122
|
+
const game = c.games[page.id];
|
|
123
|
+
if (game === void 0) return c.missing.includes(page.id) ? NOT_FOUND_HEAD : loadingHead;
|
|
124
|
+
const category = findCategory(c, game.category);
|
|
125
|
+
return {
|
|
126
|
+
title: `${pageTitleOf(game)}: free online ${category?.noun ?? "game"} | ${SITE_NAME}`,
|
|
127
|
+
description: gameDescription(game),
|
|
128
|
+
canonical: `${c.origin}${gamePath(game.id)}`,
|
|
129
|
+
robots: "index",
|
|
130
|
+
ogType: "article",
|
|
131
|
+
jsonLd: gameJsonLd(game, category, c.origin)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "Licenses": return {
|
|
135
|
+
title: `Source licenses and credits | ${SITE_NAME}`,
|
|
136
|
+
description: "Credits and source-license notices for Joystyk’s map and word data.",
|
|
137
|
+
canonical: `${c.origin}/licenses`,
|
|
138
|
+
robots: "index",
|
|
139
|
+
ogType: "article",
|
|
140
|
+
jsonLd: null
|
|
141
|
+
};
|
|
142
|
+
case "Advertise": return {
|
|
143
|
+
title: `Advertise with us: ad slots, audience and rules | ${SITE_NAME}`,
|
|
144
|
+
description: "Put your brand beside free games played by every age. See the ad slots on Joystyk and their sizes, who sees them and the rules every ad meets, then send us a message.",
|
|
145
|
+
canonical: `${c.origin}${ADVERTISE_PATH}`,
|
|
146
|
+
robots: "index",
|
|
147
|
+
ogType: "article",
|
|
148
|
+
jsonLd: null
|
|
149
|
+
};
|
|
150
|
+
case "AdvertisePolicy": return {
|
|
151
|
+
title: `Creative policy for advertisers | ${SITE_NAME}`,
|
|
152
|
+
description: "The rules every ad on Joystyk meets before it runs: products we never advertise, honest claims, clear labels, safe landing pages and the image specs for each slot.",
|
|
153
|
+
canonical: `${c.origin}${ADVERTISE_POLICY_PATH}`,
|
|
154
|
+
robots: "index",
|
|
155
|
+
ogType: "article",
|
|
156
|
+
jsonLd: null
|
|
157
|
+
};
|
|
158
|
+
case "AdvertiseSent": return {
|
|
159
|
+
...NOT_FOUND_HEAD,
|
|
160
|
+
title: `Message sent | ${SITE_NAME}`,
|
|
161
|
+
description: "Thanks for getting in touch about advertising on Joystyk."
|
|
162
|
+
};
|
|
163
|
+
case "LeavingSite": return c.leaving?.id === page.id ? {
|
|
164
|
+
...NOT_FOUND_HEAD,
|
|
165
|
+
title: `You’re leaving ${SITE_NAME}`,
|
|
166
|
+
description: `This ad goes to ${c.leaving.domain}, a website ${SITE_NAME} doesn’t run.`
|
|
167
|
+
} : NOT_FOUND_HEAD;
|
|
168
|
+
case "NotFound": return NOT_FOUND_HEAD;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { NOT_FOUND_HEAD, SITE_NAME, categoryJsonLd, gameDescription, gameJsonLd, headFor, pageTitleOf, playModes };
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=seo.js.map
|
package/dist/seo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.js","names":[],"sources":["../src/seo.ts"],"sourcesContent":["// Head metadata as a pure function of the page and the catalog state. The server writes it\n// into the document head; after client navigation the shell sends the same value to the head\n// model through Gyral's shared head API, so both sinks read one source.\nimport type { JsonValue } from '@gyral/core';\nimport type { Mode } from '@joystyk/contracts';\nimport { playersLabel } from './labels.ts';\nimport { ADVERTISE_PATH, ADVERTISE_POLICY_PATH, categoryPath, gamePath, type Page } from './routes.ts';\nimport type { CatalogState } from './store.ts';\nimport type { CategoryInfo, GameDetail } from './wire.ts';\n\nexport const SITE_NAME = 'Joystyk';\n\nexport type JsonLd = Readonly<Record<string, JsonValue>>;\n\nexport interface Head {\n readonly title: string;\n readonly description: string;\n /** Absolute canonical URL, or null for pages that must not be indexed. */\n readonly canonical: string | null;\n readonly robots: 'index' | 'noindex';\n /** Open Graph type: the index is a website; everything else an article-like page. */\n readonly ogType: 'website' | 'article';\n readonly jsonLd: JsonLd | null;\n}\n\nconst SINGLE: readonly Mode[] = ['solo', 'ai', 'daily'];\nconst MULTI: readonly Mode[] = ['pass-and-play', 'p2p', 'server'];\n\n/** schema.org playMode values for a game's modes. */\nexport function playModes(modes: readonly Mode[]): readonly string[] {\n const out: string[] = [];\n if (modes.some((m) => SINGLE.includes(m))) out.push('SinglePlayer');\n if (modes.some((m) => MULTI.includes(m))) out.push('MultiPlayer');\n return out;\n}\n\n/**\n * What a game page calls the game in its title, heading and structured data: the manifest's\n * `pageTitle` (\"Klondike Solitaire\") when it has one, else the display name. Links, cards\n * and breadcrumbs keep the display name.\n */\nexport const pageTitleOf = (game: Pick<GameDetail, 'name' | 'pageTitle'>): string => game.pageTitle ?? game.name;\n\nconst crumb = (position: number, name: string, item: string) => ({ '@type': 'ListItem', position, name, item });\n\nexport function gameJsonLd(game: GameDetail, category: CategoryInfo | undefined, origin: string): JsonLd {\n const url = `${origin}${gamePath(game.id)}`;\n const crumbs = [crumb(1, 'Games', `${origin}/`)];\n if (category !== undefined) crumbs.push(crumb(2, category.name, `${origin}${categoryPath(category.id)}`));\n crumbs.push(crumb(crumbs.length + 1, game.name, url));\n return {\n '@context': 'https://schema.org',\n '@graph': [\n {\n '@type': 'VideoGame',\n '@id': `${url}#game`,\n name: pageTitleOf(game),\n ...(pageTitleOf(game) === game.name ? {} : { alternateName: game.name }),\n description: game.description,\n url,\n ...(category === undefined ? {} : { genre: category.name }),\n keywords: game.tags.join(', '),\n gamePlatform: 'Web browser',\n applicationCategory: 'Game',\n operatingSystem: 'Any',\n playMode: playModes(game.modes),\n numberOfPlayers: { '@type': 'QuantitativeValue', minValue: game.players.min, maxValue: game.players.max },\n isAccessibleForFree: true,\n publisher: { '@type': 'Organization', name: SITE_NAME, url: `${origin}/` },\n },\n { '@type': 'BreadcrumbList', itemListElement: crumbs },\n ],\n };\n}\n\nexport function categoryJsonLd(category: CategoryInfo, origin: string): JsonLd {\n return {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: [\n crumb(1, 'Games', `${origin}/`),\n crumb(2, category.name, `${origin}${categoryPath(category.id)}`),\n ],\n };\n}\n\nexport const NOT_FOUND_HEAD: Head = {\n title: `Page not found | ${SITE_NAME}`,\n description: 'There is no page at this address.',\n canonical: null,\n robots: 'noindex',\n ogType: 'article',\n jsonLd: null,\n};\n\nconst loadingHead: Head = { ...NOT_FOUND_HEAD, title: `Loading… | ${SITE_NAME}`, description: '' };\n\nconst findCategory = (c: CatalogState, id: string): CategoryInfo | undefined => c.categories.find((k) => k.id === id);\n\n/** A game page's description: the summary, then the facts a searcher wants. */\nexport const gameDescription = (g: GameDetail): string =>\n `${g.summary} Free in your browser, ${playersLabel(g.players)}.`;\n\nexport function headFor(page: Page, c: CatalogState): Head {\n switch (page._tag) {\n case 'Home':\n return {\n title: `${SITE_NAME}: free arcade, card and dice games in your browser`,\n description:\n 'Free arcade, card and dice games for every age. Play in your browser with no sign-up and no download: alone, against the computer, or with friends.',\n canonical: `${c.origin}/`,\n robots: 'index',\n ogType: 'website',\n jsonLd: { '@context': 'https://schema.org', '@type': 'WebSite', name: SITE_NAME, url: `${c.origin}/` },\n };\n case 'Category': {\n const category = findCategory(c, page.id);\n if (category === undefined) return NOT_FOUND_HEAD;\n return {\n title: `${category.name} games: play free in your browser | ${SITE_NAME}`,\n description: category.description,\n canonical: `${c.origin}${categoryPath(category.id)}`,\n robots: 'index',\n ogType: 'article',\n jsonLd: categoryJsonLd(category, c.origin),\n };\n }\n case 'Game': {\n const game = c.games[page.id];\n if (game === undefined) return c.missing.includes(page.id) ? NOT_FOUND_HEAD : loadingHead;\n const category = findCategory(c, game.category);\n return {\n title: `${pageTitleOf(game)}: free online ${category?.noun ?? 'game'} | ${SITE_NAME}`,\n description: gameDescription(game),\n canonical: `${c.origin}${gamePath(game.id)}`,\n robots: 'index',\n ogType: 'article',\n jsonLd: gameJsonLd(game, category, c.origin),\n };\n }\n case 'Licenses':\n return {\n title: `Source licenses and credits | ${SITE_NAME}`,\n description: 'Credits and source-license notices for Joystyk’s map and word data.',\n canonical: `${c.origin}/licenses`,\n robots: 'index',\n ogType: 'article',\n jsonLd: null,\n };\n case 'Advertise':\n return {\n title: `Advertise with us: ad slots, audience and rules | ${SITE_NAME}`,\n description:\n 'Put your brand beside free games played by every age. See the ad slots on Joystyk and their sizes, who sees them and the rules every ad meets, then send us a message.',\n canonical: `${c.origin}${ADVERTISE_PATH}`,\n robots: 'index',\n ogType: 'article',\n jsonLd: null,\n };\n case 'AdvertisePolicy':\n return {\n title: `Creative policy for advertisers | ${SITE_NAME}`,\n description:\n 'The rules every ad on Joystyk meets before it runs: products we never advertise, honest claims, clear labels, safe landing pages and the image specs for each slot.',\n canonical: `${c.origin}${ADVERTISE_POLICY_PATH}`,\n robots: 'index',\n ogType: 'article',\n jsonLd: null,\n };\n case 'AdvertiseSent':\n return {\n ...NOT_FOUND_HEAD,\n title: `Message sent | ${SITE_NAME}`,\n description: 'Thanks for getting in touch about advertising on Joystyk.',\n };\n // A step between an ad and the advertiser's site: never indexed.\n case 'LeavingSite':\n return c.leaving?.id === page.id\n ? {\n ...NOT_FOUND_HEAD,\n title: `You’re leaving ${SITE_NAME}`,\n description: `This ad goes to ${c.leaving.domain}, a website ${SITE_NAME} doesn’t run.`,\n }\n : NOT_FOUND_HEAD;\n case 'NotFound':\n return NOT_FOUND_HEAD;\n }\n}\n"],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ChildValue } from '@gyral/core';
|
|
2
|
+
import { type CspOptions } from '@gyral/ssr';
|
|
3
|
+
import './components/app.ts';
|
|
4
|
+
import type { ContactFormState } from './advertise.js';
|
|
5
|
+
import type { Registry } from './registry.js';
|
|
6
|
+
import { type Page } from './routes.js';
|
|
7
|
+
import { type CatalogState, type Leaving } from './store.js';
|
|
8
|
+
export interface CatalogRendererOptions {
|
|
9
|
+
/** Public origin for canonical URLs and JSON-LD, e.g. https://joystyk.com. */
|
|
10
|
+
readonly origin: string;
|
|
11
|
+
readonly registry: Registry;
|
|
12
|
+
/** The built client entry's URL (`/assets/client-<hash>.js`), or the source path in dev. */
|
|
13
|
+
readonly clientEntry: string;
|
|
14
|
+
/** Chunks to preload with the entry, including Gyral's hydration chunk. */
|
|
15
|
+
readonly modulepreload?: readonly string[];
|
|
16
|
+
/** Document CSS, in cascade order: the ui tokens, then the catalog stylesheet. */
|
|
17
|
+
readonly styles: readonly string[];
|
|
18
|
+
/** Hashed CSS URLs from the client manifest, in cascade order. */
|
|
19
|
+
readonly css?: readonly string[];
|
|
20
|
+
/** Extra head markup every page gets (favicon, theme colour). */
|
|
21
|
+
readonly head?: ChildValue;
|
|
22
|
+
}
|
|
23
|
+
/** Headers on every HTML response. HTML always revalidates (offline-and-updates.md, Rule 1). */
|
|
24
|
+
export declare const HTML_HEADERS: Readonly<Record<string, string>>;
|
|
25
|
+
export type { Leaving } from './store.js';
|
|
26
|
+
export declare const CSP: CspOptions;
|
|
27
|
+
/** A rejected contact form post, re-rendered on the advertise page with this status. */
|
|
28
|
+
export interface ContactRender {
|
|
29
|
+
readonly form: ContactFormState;
|
|
30
|
+
readonly status: number;
|
|
31
|
+
}
|
|
32
|
+
/** What the site passes for the pages that show server-only data. */
|
|
33
|
+
export interface PageData {
|
|
34
|
+
/** The advertise page after a rejected post. */
|
|
35
|
+
readonly contact?: ContactRender;
|
|
36
|
+
/** The "You're leaving Joystyk" page: an approved creative's destination. */
|
|
37
|
+
readonly leaving?: Leaving;
|
|
38
|
+
}
|
|
39
|
+
/** What `page` renders, loaded before rendering (the render is synchronous), and its status. */
|
|
40
|
+
export declare function seedFor(page: Page, registry: Registry, origin: string, data?: PageData): {
|
|
41
|
+
readonly state: CatalogState;
|
|
42
|
+
readonly status: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Renders the page at a URL. `data.contact` re-renders the advertise page after a rejected
|
|
46
|
+
* post, with the visitor's values and the errors; the site's contact handler passes it.
|
|
47
|
+
* `data.leaving` renders an ad click's "You're leaving Joystyk" page; the ad routes pass it.
|
|
48
|
+
*/
|
|
49
|
+
export type CatalogRenderer = (url: URL, data?: PageData) => Response;
|
|
50
|
+
export declare function createCatalogRenderer(options: CatalogRendererOptions): CatalogRenderer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { pageFor, site } from "./routes.js";
|
|
2
|
+
import { headFor } from "./seo.js";
|
|
3
|
+
import { toGyralHead } from "./head.js";
|
|
4
|
+
import { catalog, emptyCatalog } from "./store.js";
|
|
5
|
+
import "./components/app.js";
|
|
6
|
+
import { compiled } from "@gyral/core/compiled";
|
|
7
|
+
import "@gyral/core";
|
|
8
|
+
import { renderPage } from "@gyral/ssr";
|
|
9
|
+
//#region src/server.ts
|
|
10
|
+
var _gyral$t0 = {
|
|
11
|
+
"id": "tg7w4s30nl",
|
|
12
|
+
"html": "<jy-app></jy-app>",
|
|
13
|
+
"parts": [[
|
|
14
|
+
3,
|
|
15
|
+
[0],
|
|
16
|
+
"path"
|
|
17
|
+
]],
|
|
18
|
+
"segments": [
|
|
19
|
+
{
|
|
20
|
+
"k": "open",
|
|
21
|
+
"tag": "jy-app",
|
|
22
|
+
"html": "<jy-app",
|
|
23
|
+
"attrs": []
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"k": "attr",
|
|
27
|
+
"name": "path"
|
|
28
|
+
},
|
|
29
|
+
{ "k": "openEnd" },
|
|
30
|
+
{
|
|
31
|
+
"k": "close",
|
|
32
|
+
"tag": "jy-app"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
/** Headers on every HTML response. HTML always revalidates (offline-and-updates.md, Rule 1). */
|
|
37
|
+
var HTML_HEADERS = {
|
|
38
|
+
"cache-control": "no-cache",
|
|
39
|
+
"x-content-type-options": "nosniff",
|
|
40
|
+
"referrer-policy": "strict-origin-when-cross-origin"
|
|
41
|
+
};
|
|
42
|
+
var CSP = { directives: {
|
|
43
|
+
"default-src": "'self'",
|
|
44
|
+
"script-src": "'self'",
|
|
45
|
+
"connect-src": "'self'",
|
|
46
|
+
"img-src": "'self' data:",
|
|
47
|
+
"object-src": "'none'",
|
|
48
|
+
"base-uri": "'none'",
|
|
49
|
+
"form-action": "'self'",
|
|
50
|
+
"frame-ancestors": "'none'"
|
|
51
|
+
} };
|
|
52
|
+
var copyForm = (form) => ({
|
|
53
|
+
values: Object.fromEntries(Object.entries(form.values).map(([k, v]) => [k, typeof v === "string" ? v : [...v]])),
|
|
54
|
+
errors: Object.fromEntries(Object.entries(form.errors).map(([k, v]) => [k, [...v]]))
|
|
55
|
+
});
|
|
56
|
+
/** What `page` renders, loaded before rendering (the render is synchronous), and its status. */
|
|
57
|
+
function seedFor(page, registry, origin, data = {}) {
|
|
58
|
+
const { contact, leaving } = data;
|
|
59
|
+
const empty = emptyCatalog(origin, registry.categories());
|
|
60
|
+
const summaries = [...registry.summaries()];
|
|
61
|
+
switch (page._tag) {
|
|
62
|
+
case "Home": return {
|
|
63
|
+
state: {
|
|
64
|
+
...empty,
|
|
65
|
+
summaries
|
|
66
|
+
},
|
|
67
|
+
status: 200
|
|
68
|
+
};
|
|
69
|
+
case "Category": return registry.categories().some((k) => k.id === page.id) ? {
|
|
70
|
+
state: {
|
|
71
|
+
...empty,
|
|
72
|
+
summaries
|
|
73
|
+
},
|
|
74
|
+
status: 200
|
|
75
|
+
} : {
|
|
76
|
+
state: empty,
|
|
77
|
+
status: 404
|
|
78
|
+
};
|
|
79
|
+
case "Game": {
|
|
80
|
+
const game = registry.detail(page.id);
|
|
81
|
+
return game === void 0 ? {
|
|
82
|
+
state: {
|
|
83
|
+
...empty,
|
|
84
|
+
missing: [page.id]
|
|
85
|
+
},
|
|
86
|
+
status: 404
|
|
87
|
+
} : {
|
|
88
|
+
state: {
|
|
89
|
+
...empty,
|
|
90
|
+
summaries,
|
|
91
|
+
games: { [game.id]: game }
|
|
92
|
+
},
|
|
93
|
+
status: 200
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
case "Advertise": return contact === void 0 ? {
|
|
97
|
+
state: empty,
|
|
98
|
+
status: 200
|
|
99
|
+
} : {
|
|
100
|
+
state: {
|
|
101
|
+
...empty,
|
|
102
|
+
contactForm: copyForm(contact.form)
|
|
103
|
+
},
|
|
104
|
+
status: contact.status
|
|
105
|
+
};
|
|
106
|
+
case "Licenses":
|
|
107
|
+
case "AdvertisePolicy":
|
|
108
|
+
case "AdvertiseSent": return {
|
|
109
|
+
state: empty,
|
|
110
|
+
status: 200
|
|
111
|
+
};
|
|
112
|
+
case "LeavingSite": return leaving?.id === page.id ? {
|
|
113
|
+
state: {
|
|
114
|
+
...empty,
|
|
115
|
+
leaving: { ...leaving }
|
|
116
|
+
},
|
|
117
|
+
status: 200
|
|
118
|
+
} : {
|
|
119
|
+
state: empty,
|
|
120
|
+
status: 404
|
|
121
|
+
};
|
|
122
|
+
case "NotFound": return {
|
|
123
|
+
state: empty,
|
|
124
|
+
status: 404
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function createCatalogRenderer(options) {
|
|
129
|
+
const { styles } = options;
|
|
130
|
+
return (url, data = {}) => {
|
|
131
|
+
const page = pageFor(url.pathname);
|
|
132
|
+
const canonicalPath = page._tag === "NotFound" ? url.pathname : site.match(url)?.path ?? url.pathname;
|
|
133
|
+
if (canonicalPath !== url.pathname) return new Response(null, {
|
|
134
|
+
status: 301,
|
|
135
|
+
headers: {
|
|
136
|
+
location: `${canonicalPath}${url.search}`,
|
|
137
|
+
"cache-control": "no-cache"
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const seed = seedFor(page, options.registry, options.origin, data);
|
|
141
|
+
const head = headFor(page, seed.state);
|
|
142
|
+
return renderPage({
|
|
143
|
+
...toGyralHead(head),
|
|
144
|
+
lang: "en",
|
|
145
|
+
styles,
|
|
146
|
+
stylesheets: options.css ?? [],
|
|
147
|
+
extraHead: options.head,
|
|
148
|
+
stores: [catalog.instance(seed.state)],
|
|
149
|
+
body: compiled(_gyral$t0, [url.pathname]),
|
|
150
|
+
scripts: [options.clientEntry],
|
|
151
|
+
modulepreload: options.modulepreload ?? [],
|
|
152
|
+
csp: CSP
|
|
153
|
+
}, {
|
|
154
|
+
status: seed.status,
|
|
155
|
+
headers: HTML_HEADERS
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { CSP, HTML_HEADERS, createCatalogRenderer, seedFor };
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","names":["_gyral$compiled"],"sources":["../src/server.ts"],"sourcesContent":["// The catalog's request-time renderer. It resolves the URL to a page, redirects\n// non-canonical paths, loads exactly the data that page shows into a fresh store instance,\n// picks the status from that data, and renders the document with @gyral/ssr. It does no\n// I/O: the app passes in the registry, the built asset URLs and the stylesheet text.\nimport { html, type ChildValue } from '@gyral/core';\nimport { renderPage, type CspOptions } from '@gyral/ssr';\nimport './components/app.ts';\nimport type { ContactFormState } from './advertise.ts';\nimport { toGyralHead } from './head.ts';\nimport type { Registry } from './registry.ts';\nimport { pageFor, site, type Page } from './routes.ts';\nimport { headFor } from './seo.ts';\nimport { catalog, emptyCatalog, type CatalogState, type Leaving } from './store.ts';\n\nexport interface CatalogRendererOptions {\n /** Public origin for canonical URLs and JSON-LD, e.g. https://joystyk.com. */\n readonly origin: string;\n readonly registry: Registry;\n /** The built client entry's URL (`/assets/client-<hash>.js`), or the source path in dev. */\n readonly clientEntry: string;\n /** Chunks to preload with the entry, including Gyral's hydration chunk. */\n readonly modulepreload?: readonly string[];\n /** Document CSS, in cascade order: the ui tokens, then the catalog stylesheet. */\n readonly styles: readonly string[];\n /** Hashed CSS URLs from the client manifest, in cascade order. */\n readonly css?: readonly string[];\n /** Extra head markup every page gets (favicon, theme colour). */\n readonly head?: ChildValue;\n}\n\n/** Headers on every HTML response. HTML always revalidates (offline-and-updates.md, Rule 1). */\nexport const HTML_HEADERS: Readonly<Record<string, string>> = {\n 'cache-control': 'no-cache',\n 'x-content-type-options': 'nosniff',\n 'referrer-policy': 'strict-origin-when-cross-origin',\n};\n\nexport type { Leaving } from './store.ts';\n\nexport const CSP: CspOptions = {\n directives: {\n 'default-src': \"'self'\",\n 'script-src': \"'self'\",\n 'connect-src': \"'self'\",\n 'img-src': \"'self' data:\",\n 'object-src': \"'none'\",\n 'base-uri': \"'none'\",\n 'form-action': \"'self'\",\n 'frame-ancestors': \"'none'\",\n },\n};\n\n/** A rejected contact form post, re-rendered on the advertise page with this status. */\nexport interface ContactRender {\n readonly form: ContactFormState;\n readonly status: number;\n}\n\nconst copyForm = (form: ContactFormState): CatalogState['contactForm'] => ({\n values: Object.fromEntries(Object.entries(form.values).map(([k, v]) => [k, typeof v === 'string' ? v : [...v]])),\n errors: Object.fromEntries(Object.entries(form.errors).map(([k, v]) => [k, [...v]])),\n});\n\n/** What the site passes for the pages that show server-only data. */\nexport interface PageData {\n /** The advertise page after a rejected post. */\n readonly contact?: ContactRender;\n /** The \"You're leaving Joystyk\" page: an approved creative's destination. */\n readonly leaving?: Leaving;\n}\n\n/** What `page` renders, loaded before rendering (the render is synchronous), and its status. */\nexport function seedFor(\n page: Page,\n registry: Registry,\n origin: string,\n data: PageData = {},\n): { readonly state: CatalogState; readonly status: number } {\n const { contact, leaving } = data;\n const empty = emptyCatalog(origin, registry.categories());\n const summaries = [...registry.summaries()];\n switch (page._tag) {\n case 'Home':\n return { state: { ...empty, summaries }, status: 200 };\n case 'Category':\n return registry.categories().some((k) => k.id === page.id)\n ? { state: { ...empty, summaries }, status: 200 }\n : { state: empty, status: 404 };\n case 'Game': {\n const game = registry.detail(page.id);\n return game === undefined\n ? { state: { ...empty, missing: [page.id] }, status: 404 }\n : { state: { ...empty, summaries, games: { [game.id]: game } }, status: 200 };\n }\n case 'Advertise':\n return contact === undefined\n ? { state: empty, status: 200 }\n : { state: { ...empty, contactForm: copyForm(contact.form) }, status: contact.status };\n case 'Licenses':\n case 'AdvertisePolicy':\n case 'AdvertiseSent':\n return { state: empty, status: 200 };\n // Only with a destination for this creative from the ad routes; without one, not found.\n case 'LeavingSite':\n return leaving?.id === page.id\n ? { state: { ...empty, leaving: { ...leaving } }, status: 200 }\n : { state: empty, status: 404 };\n case 'NotFound':\n return { state: empty, status: 404 };\n }\n}\n\n/**\n * Renders the page at a URL. `data.contact` re-renders the advertise page after a rejected\n * post, with the visitor's values and the errors; the site's contact handler passes it.\n * `data.leaving` renders an ad click's \"You're leaving Joystyk\" page; the ad routes pass it.\n */\nexport type CatalogRenderer = (url: URL, data?: PageData) => Response;\n\nexport function createCatalogRenderer(options: CatalogRendererOptions): CatalogRenderer {\n const { styles } = options;\n return (url, data = {}) => {\n const page = pageFor(url.pathname);\n // One URL per page: /games/x/ answers 301 to /games/x, keeping the query.\n const canonicalPath = page._tag === 'NotFound' ? url.pathname : (site.match(url)?.path ?? url.pathname);\n if (canonicalPath !== url.pathname) {\n return new Response(null, {\n status: 301,\n headers: { location: `${canonicalPath}${url.search}`, 'cache-control': 'no-cache' },\n });\n }\n const seed = seedFor(page, options.registry, options.origin, data);\n const head = headFor(page, seed.state);\n return renderPage(\n {\n ...toGyralHead(head),\n lang: 'en',\n styles,\n stylesheets: options.css ?? [],\n extraHead: options.head,\n stores: [catalog.instance(seed.state)],\n body: html`<jy-app path=${url.pathname}></jy-app>`,\n scripts: [options.clientEntry],\n modulepreload: options.modulepreload ?? [],\n csp: CSP,\n },\n { status: seed.status, headers: HTML_HEADERS },\n );\n };\n}\n"],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import * as z from 'zod/mini';
|
|
2
|
+
import { type CategoryInfo, type GameDetail, type GameSummary } from './wire.js';
|
|
3
|
+
declare const LoadSchema: z.ZodMiniEnum<{
|
|
4
|
+
failed: "failed";
|
|
5
|
+
idle: "idle";
|
|
6
|
+
loading: "loading";
|
|
7
|
+
}>;
|
|
8
|
+
export type Load = z.infer<typeof LoadSchema>;
|
|
9
|
+
/**
|
|
10
|
+
* Where an ad click leads: the creative's id, its approved link, that link's domain as the page
|
|
11
|
+
* names it, and the same-site path "Back to Joystyk" returns to.
|
|
12
|
+
*/
|
|
13
|
+
declare const LeavingSchema: z.ZodMiniObject<{
|
|
14
|
+
id: z.ZodMiniString<string>;
|
|
15
|
+
href: z.ZodMiniString<string>;
|
|
16
|
+
domain: z.ZodMiniString<string>;
|
|
17
|
+
back: z.ZodMiniString<string>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export type Leaving = z.infer<typeof LeavingSchema>;
|
|
20
|
+
export declare const CatalogStateSchema: z.ZodMiniObject<{
|
|
21
|
+
origin: z.ZodMiniString<string>;
|
|
22
|
+
categories: z.ZodMiniArray<z.ZodMiniObject<{
|
|
23
|
+
id: z.ZodMiniString<string>;
|
|
24
|
+
name: z.ZodMiniString<string>;
|
|
25
|
+
noun: z.ZodMiniString<string>;
|
|
26
|
+
description: z.ZodMiniString<string>;
|
|
27
|
+
games: z.ZodMiniInt;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
summaries: z.ZodMiniNullable<z.ZodMiniArray<z.ZodMiniObject<{
|
|
30
|
+
id: z.ZodMiniString<string>;
|
|
31
|
+
name: z.ZodMiniString<string>;
|
|
32
|
+
summary: z.ZodMiniString<string>;
|
|
33
|
+
category: z.ZodMiniString<string>;
|
|
34
|
+
tags: z.ZodMiniArray<z.ZodMiniString<string>>;
|
|
35
|
+
modes: z.ZodMiniArray<z.ZodMiniEnum<{
|
|
36
|
+
daily: "daily";
|
|
37
|
+
ai: "ai";
|
|
38
|
+
server: "server";
|
|
39
|
+
solo: "solo";
|
|
40
|
+
"pass-and-play": "pass-and-play";
|
|
41
|
+
p2p: "p2p";
|
|
42
|
+
}>>;
|
|
43
|
+
players: z.ZodMiniObject<{
|
|
44
|
+
min: z.ZodMiniInt;
|
|
45
|
+
max: z.ZodMiniInt;
|
|
46
|
+
default: z.ZodMiniOptional<z.ZodMiniInt>;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
status: z.ZodMiniEnum<{
|
|
49
|
+
"coming-soon": "coming-soon";
|
|
50
|
+
available: "available";
|
|
51
|
+
}>;
|
|
52
|
+
placeholder: z.ZodMiniBoolean<boolean>;
|
|
53
|
+
featured: z.ZodMiniBoolean<boolean>;
|
|
54
|
+
}, z.core.$strip>>>;
|
|
55
|
+
games: z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniObject<{
|
|
56
|
+
id: z.ZodMiniString<string>;
|
|
57
|
+
name: z.ZodMiniString<string>;
|
|
58
|
+
summary: z.ZodMiniString<string>;
|
|
59
|
+
category: z.ZodMiniString<string>;
|
|
60
|
+
tags: z.ZodMiniArray<z.ZodMiniString<string>>;
|
|
61
|
+
modes: z.ZodMiniArray<z.ZodMiniEnum<{
|
|
62
|
+
daily: "daily";
|
|
63
|
+
ai: "ai";
|
|
64
|
+
server: "server";
|
|
65
|
+
solo: "solo";
|
|
66
|
+
"pass-and-play": "pass-and-play";
|
|
67
|
+
p2p: "p2p";
|
|
68
|
+
}>>;
|
|
69
|
+
players: z.ZodMiniObject<{
|
|
70
|
+
min: z.ZodMiniInt;
|
|
71
|
+
max: z.ZodMiniInt;
|
|
72
|
+
default: z.ZodMiniOptional<z.ZodMiniInt>;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
status: z.ZodMiniEnum<{
|
|
75
|
+
"coming-soon": "coming-soon";
|
|
76
|
+
available: "available";
|
|
77
|
+
}>;
|
|
78
|
+
placeholder: z.ZodMiniBoolean<boolean>;
|
|
79
|
+
featured: z.ZodMiniBoolean<boolean>;
|
|
80
|
+
description: z.ZodMiniString<string>;
|
|
81
|
+
pageTitle: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
82
|
+
rules: z.ZodMiniArray<z.ZodMiniString<string>>;
|
|
83
|
+
aiLevels: z.ZodMiniArray<z.ZodMiniString<string>>;
|
|
84
|
+
essentialMotion: z.ZodMiniBoolean<boolean>;
|
|
85
|
+
aiContent: z.ZodMiniBoolean<boolean>;
|
|
86
|
+
assets: z.ZodMiniString<string>;
|
|
87
|
+
options: z.ZodMiniOptional<z.ZodMiniCustom<import("@joystyk/contracts").GameOption[], import("@joystyk/contracts").GameOption[]>>;
|
|
88
|
+
attribution: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
89
|
+
text: z.ZodMiniString<string>;
|
|
90
|
+
href: z.ZodMiniString<string>;
|
|
91
|
+
}, z.core.$strip>>;
|
|
92
|
+
}, z.core.$strip>>;
|
|
93
|
+
missing: z.ZodMiniArray<z.ZodMiniString<string>>;
|
|
94
|
+
summariesLoad: z.ZodMiniEnum<{
|
|
95
|
+
failed: "failed";
|
|
96
|
+
idle: "idle";
|
|
97
|
+
loading: "loading";
|
|
98
|
+
}>;
|
|
99
|
+
gameLoads: z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniEnum<{
|
|
100
|
+
failed: "failed";
|
|
101
|
+
idle: "idle";
|
|
102
|
+
loading: "loading";
|
|
103
|
+
}>>;
|
|
104
|
+
contactForm: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
105
|
+
values: z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniArray<z.ZodMiniString<string>>]>>;
|
|
106
|
+
errors: z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniArray<z.ZodMiniString<string>>>;
|
|
107
|
+
}, z.core.$strip>>;
|
|
108
|
+
leaving: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
109
|
+
id: z.ZodMiniString<string>;
|
|
110
|
+
href: z.ZodMiniString<string>;
|
|
111
|
+
domain: z.ZodMiniString<string>;
|
|
112
|
+
back: z.ZodMiniString<string>;
|
|
113
|
+
}, z.core.$strip>>;
|
|
114
|
+
}, z.core.$strip>;
|
|
115
|
+
export type CatalogState = z.infer<typeof CatalogStateSchema>;
|
|
116
|
+
export type CatalogMsg = {
|
|
117
|
+
readonly _tag: 'LoadSummaries';
|
|
118
|
+
} | {
|
|
119
|
+
readonly _tag: 'LoadGame';
|
|
120
|
+
readonly id: string;
|
|
121
|
+
} | {
|
|
122
|
+
readonly _tag: 'SummariesLoaded';
|
|
123
|
+
readonly summaries: GameSummary[];
|
|
124
|
+
} | {
|
|
125
|
+
readonly _tag: 'GameLoaded';
|
|
126
|
+
readonly game: GameDetail;
|
|
127
|
+
} | {
|
|
128
|
+
readonly _tag: 'GameMissing';
|
|
129
|
+
readonly id: string;
|
|
130
|
+
} | {
|
|
131
|
+
readonly _tag: 'LoadFailed';
|
|
132
|
+
readonly id: string | null;
|
|
133
|
+
};
|
|
134
|
+
export declare const emptyCatalog: (origin: string, categories?: readonly CategoryInfo[]) => CatalogState;
|
|
135
|
+
export declare const catalog: import("@gyral/core").Store<{
|
|
136
|
+
origin: string;
|
|
137
|
+
categories: {
|
|
138
|
+
id: string;
|
|
139
|
+
name: string;
|
|
140
|
+
noun: string;
|
|
141
|
+
description: string;
|
|
142
|
+
games: number;
|
|
143
|
+
}[];
|
|
144
|
+
summaries: {
|
|
145
|
+
id: string;
|
|
146
|
+
name: string;
|
|
147
|
+
summary: string;
|
|
148
|
+
category: string;
|
|
149
|
+
tags: string[];
|
|
150
|
+
modes: ("daily" | "ai" | "server" | "solo" | "pass-and-play" | "p2p")[];
|
|
151
|
+
players: {
|
|
152
|
+
min: number;
|
|
153
|
+
max: number;
|
|
154
|
+
default?: number | undefined;
|
|
155
|
+
};
|
|
156
|
+
status: "coming-soon" | "available";
|
|
157
|
+
placeholder: boolean;
|
|
158
|
+
featured: boolean;
|
|
159
|
+
}[] | null;
|
|
160
|
+
games: Record<string, {
|
|
161
|
+
id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
summary: string;
|
|
164
|
+
category: string;
|
|
165
|
+
tags: string[];
|
|
166
|
+
modes: ("daily" | "ai" | "server" | "solo" | "pass-and-play" | "p2p")[];
|
|
167
|
+
players: {
|
|
168
|
+
min: number;
|
|
169
|
+
max: number;
|
|
170
|
+
default?: number | undefined;
|
|
171
|
+
};
|
|
172
|
+
status: "coming-soon" | "available";
|
|
173
|
+
placeholder: boolean;
|
|
174
|
+
featured: boolean;
|
|
175
|
+
description: string;
|
|
176
|
+
rules: string[];
|
|
177
|
+
aiLevels: string[];
|
|
178
|
+
essentialMotion: boolean;
|
|
179
|
+
aiContent: boolean;
|
|
180
|
+
assets: string;
|
|
181
|
+
pageTitle?: string | undefined;
|
|
182
|
+
options?: import("@joystyk/contracts").GameOption[] | undefined;
|
|
183
|
+
attribution?: {
|
|
184
|
+
text: string;
|
|
185
|
+
href: string;
|
|
186
|
+
} | undefined;
|
|
187
|
+
}>;
|
|
188
|
+
missing: string[];
|
|
189
|
+
summariesLoad: "failed" | "idle" | "loading";
|
|
190
|
+
gameLoads: Record<string, "failed" | "idle" | "loading">;
|
|
191
|
+
contactForm: {
|
|
192
|
+
values: Record<string, string | string[]>;
|
|
193
|
+
errors: Record<string, string[]>;
|
|
194
|
+
} | null;
|
|
195
|
+
leaving: {
|
|
196
|
+
id: string;
|
|
197
|
+
href: string;
|
|
198
|
+
domain: string;
|
|
199
|
+
back: string;
|
|
200
|
+
} | null;
|
|
201
|
+
}, CatalogMsg>;
|
|
202
|
+
export {};
|