@zthun/romulator-web 1.16.0 → 1.17.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 +9 -0
- package/dist/assets/{index-DscDEeqF.js → index-CKd74YMu.js} +165 -165
- package/dist/index.html +1 -1
- package/package.json +3 -3
- package/src/games/game-page.cm.mts +33 -0
- package/src/games/game-page.spec.tsx +72 -0
- package/src/games/game-page.tsx +94 -10
- package/src/media/media-card.tsx +8 -8
package/dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Romulator: Organize your Games</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CKd74YMu.js"></script>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="zthunworks-romulator"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zthun/romulator-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "Romulator frontend",
|
|
5
5
|
"author": "Anthony Bonta",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@zthun/helpful-query": "^9.10.0",
|
|
29
29
|
"@zthun/helpful-react": "^9.10.0",
|
|
30
30
|
"@zthun/janitor-build-config": "^19.4.1",
|
|
31
|
-
"@zthun/romulator-client": "^1.
|
|
31
|
+
"@zthun/romulator-client": "^1.17.0",
|
|
32
32
|
"@zthun/webigail-http": "^5.0.0",
|
|
33
33
|
"@zthun/webigail-rest": "^5.0.0",
|
|
34
34
|
"@zthun/webigail-url": "^5.0.0",
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"vitest": "^4.0.4",
|
|
44
44
|
"vitest-mock-extended": "^3.1.0"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "9597c19fa02f71076d84059043213cb7eaf893de"
|
|
47
47
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ZCircusBy, ZCircusComponentModel } from "@zthun/cirque";
|
|
2
2
|
import {
|
|
3
3
|
ZAlertComponentModel,
|
|
4
|
+
ZCardComponentModel,
|
|
4
5
|
ZSuspenseComponentModel,
|
|
5
6
|
} from "@zthun/fashion-boutique";
|
|
7
|
+
import { firstDefined } from "@zthun/helpful-fn";
|
|
6
8
|
import { ZRomulatorGameMediaType } from "@zthun/romulator-client";
|
|
9
|
+
import { kebabCase } from "lodash-es";
|
|
7
10
|
import { ZRomulatorMediaCardComponentModel } from "../media/media-card.cm.mjs";
|
|
8
11
|
|
|
9
12
|
const {
|
|
@@ -40,6 +43,14 @@ export class ZRomulatorGamePageComponentModel extends ZCircusComponentModel {
|
|
|
40
43
|
return ZCircusBy.optional(this.driver, ZAlertComponentModel);
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
public info(): Promise<ZCardComponentModel | null> {
|
|
47
|
+
return ZCircusBy.optional(this.driver, ZCardComponentModel, "info");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public synopsis(): Promise<ZCardComponentModel | null> {
|
|
51
|
+
return ZCircusBy.optional(this.driver, ZCardComponentModel, "synopsis");
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
private media(
|
|
44
55
|
type: ZRomulatorGameMediaType,
|
|
45
56
|
): Promise<ZRomulatorMediaCardComponentModel | null> {
|
|
@@ -58,4 +69,26 @@ export class ZRomulatorGamePageComponentModel extends ZCircusComponentModel {
|
|
|
58
69
|
public physicalMedia = this.media.bind(this, PhysicalMedia);
|
|
59
70
|
public screenshot = this.media.bind(this, Screenshot);
|
|
60
71
|
public title = this.media.bind(this, Title);
|
|
72
|
+
|
|
73
|
+
private async field(key: string): Promise<string> {
|
|
74
|
+
const info = await this.info();
|
|
75
|
+
const klass = `.ZRomulatorGamePage-${kebabCase(key)}`;
|
|
76
|
+
const element = await info?.driver.select(klass);
|
|
77
|
+
const value = await element?.attribute("data-value", "");
|
|
78
|
+
return firstDefined("", value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public name = this.field.bind(this, "Name");
|
|
82
|
+
public file = this.field.bind(this, "File");
|
|
83
|
+
public players = this.field.bind(this, "Players");
|
|
84
|
+
public release = this.field.bind(this, "Release Date");
|
|
85
|
+
public developer = this.field.bind(this, "Developer");
|
|
86
|
+
public publisher = this.field.bind(this, "Publisher");
|
|
87
|
+
|
|
88
|
+
public async description(): Promise<string> {
|
|
89
|
+
const synopsis = await this.synopsis();
|
|
90
|
+
const content = await synopsis?.content();
|
|
91
|
+
|
|
92
|
+
return firstDefined("", await content?.text());
|
|
93
|
+
}
|
|
61
94
|
}
|
|
@@ -20,6 +20,7 @@ import type { ZRomulatorMediaType } from "@zthun/romulator-client";
|
|
|
20
20
|
import {
|
|
21
21
|
ZRomulatorGameBuilder,
|
|
22
22
|
ZRomulatorGameMediaType,
|
|
23
|
+
ZRomulatorPlayersBuilder,
|
|
23
24
|
ZRomulatorSystemId,
|
|
24
25
|
} from "@zthun/romulator-client";
|
|
25
26
|
import type { History } from "history";
|
|
@@ -44,6 +45,26 @@ describe("ZGamePage", () => {
|
|
|
44
45
|
.id("nes-mario")
|
|
45
46
|
.file("/path/to/games/nes/mario.zip")
|
|
46
47
|
.system(ZRomulatorSystemId.Nintendo)
|
|
48
|
+
.developer("Nintendo")
|
|
49
|
+
.publisher("Nintendo")
|
|
50
|
+
.release("1985-11-17")
|
|
51
|
+
.players(new ZRomulatorPlayersBuilder().twoPlayer().build())
|
|
52
|
+
.description(
|
|
53
|
+
"The Princess has been kidnapped by the evil Bowser, " +
|
|
54
|
+
"and it is up to Mario and brother Luigi to save the day.\n\n" +
|
|
55
|
+
"The first ever platform adventure for the Mario Brothers " +
|
|
56
|
+
"has the player exploring level after level, with Bowser to " +
|
|
57
|
+
"contend with as the end of level boss. Power-ups include " +
|
|
58
|
+
"the Super Mushroom, which increases Mario's size and power, " +
|
|
59
|
+
"the fire flower, allowing him to shoot fireballs at enemies, " +
|
|
60
|
+
"and the ever important star man for a short burst of " +
|
|
61
|
+
"invincibility.\n\nEach level includes a bonus section " +
|
|
62
|
+
"filled with coins plus a shortcut through the level, plenty " +
|
|
63
|
+
"of bad buys and obstacles to get past, and an end of " +
|
|
64
|
+
"level flag, in which the higher the player grabs it, " +
|
|
65
|
+
"the more points are awarded to them. Certain levels " +
|
|
66
|
+
"also include warp points, which takes the player to higher levels.",
|
|
67
|
+
)
|
|
47
68
|
.build();
|
|
48
69
|
|
|
49
70
|
let _driver: IZCircusDriver | undefined;
|
|
@@ -186,4 +207,55 @@ describe("ZGamePage", () => {
|
|
|
186
207
|
await shouldRenderMedia(ZRomulatorGameMediaType.Title, (t) => t.title());
|
|
187
208
|
});
|
|
188
209
|
});
|
|
210
|
+
|
|
211
|
+
describe("Info", () => {
|
|
212
|
+
async function shouldRenderInformation(
|
|
213
|
+
expected: string,
|
|
214
|
+
fieldFn: (target: ZRomulatorGamePageComponentModel) => Promise<string>,
|
|
215
|
+
) {
|
|
216
|
+
// Arrange.
|
|
217
|
+
const target = await createTestTarget();
|
|
218
|
+
await target.load();
|
|
219
|
+
|
|
220
|
+
// Act.
|
|
221
|
+
const actual = await fieldFn(target);
|
|
222
|
+
|
|
223
|
+
// Assert.
|
|
224
|
+
expect(actual).toEqual(expected);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
it("should render the game name", async () => {
|
|
228
|
+
await shouldRenderInformation(mario.name, (t) => t.name());
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should render the game file", async () => {
|
|
232
|
+
await shouldRenderInformation(mario.file, (t) => t.file());
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should render the game release date", async () => {
|
|
236
|
+
await shouldRenderInformation(mario.release, (t) => t.release());
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should render the game developer", async () => {
|
|
240
|
+
await shouldRenderInformation(mario.developer, (t) => t.developer());
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should render the game publisher", async () => {
|
|
244
|
+
await shouldRenderInformation(mario.publisher, (t) => t.publisher());
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("Synopsis", () => {
|
|
249
|
+
it("should render the game description", async () => {
|
|
250
|
+
// Arrange.
|
|
251
|
+
const target = await createTestTarget();
|
|
252
|
+
await target.load();
|
|
253
|
+
|
|
254
|
+
// Act.
|
|
255
|
+
const actual = await target.description();
|
|
256
|
+
|
|
257
|
+
// Assert.
|
|
258
|
+
expect(actual).toEqual(mario.description);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
189
261
|
});
|
package/src/games/game-page.tsx
CHANGED
|
@@ -3,14 +3,24 @@ import {
|
|
|
3
3
|
useParams,
|
|
4
4
|
ZAlert,
|
|
5
5
|
ZBreadcrumbsLocation,
|
|
6
|
+
ZCaption,
|
|
7
|
+
ZCard,
|
|
6
8
|
ZGrid,
|
|
9
|
+
ZIconFontAwesome,
|
|
10
|
+
ZLabel,
|
|
11
|
+
ZParagraph,
|
|
7
12
|
ZStack,
|
|
8
13
|
ZSuspenseProgress,
|
|
9
14
|
} from "@zthun/fashion-boutique";
|
|
10
15
|
import { ZSizeFixed } from "@zthun/fashion-tailor";
|
|
11
16
|
import { firstDefined } from "@zthun/helpful-fn";
|
|
12
17
|
import { isStateErrored, isStateLoading } from "@zthun/helpful-react";
|
|
13
|
-
import {
|
|
18
|
+
import type { IZRomulatorGame } from "@zthun/romulator-client";
|
|
19
|
+
import {
|
|
20
|
+
ZRomulatorGameMediaType,
|
|
21
|
+
ZRomulatorPlayersSerialize,
|
|
22
|
+
} from "@zthun/romulator-client";
|
|
23
|
+
import { kebabCase } from "lodash-es";
|
|
14
24
|
import { useMemo } from "react";
|
|
15
25
|
import { ZRomulatorMediaCard } from "../media/media-card.js";
|
|
16
26
|
import { useGame } from "./games-service.mjs";
|
|
@@ -32,6 +42,82 @@ export function ZRomulatorGamePage() {
|
|
|
32
42
|
[excluded],
|
|
33
43
|
);
|
|
34
44
|
|
|
45
|
+
const renderMedia = (
|
|
46
|
+
game: IZRomulatorGame,
|
|
47
|
+
type: ZRomulatorGameMediaType,
|
|
48
|
+
) => <ZRomulatorMediaCard key={type} identifier={game.id} type={type} />;
|
|
49
|
+
|
|
50
|
+
const renderMediaGallery = (game: IZRomulatorGame) => (
|
|
51
|
+
<ZGrid
|
|
52
|
+
columns={{
|
|
53
|
+
xl: "1fr 1fr 1fr",
|
|
54
|
+
lg: "1fr 1fr",
|
|
55
|
+
md: "1fr",
|
|
56
|
+
}}
|
|
57
|
+
gap={ZSizeFixed.Medium}
|
|
58
|
+
>
|
|
59
|
+
{images.map((i) => renderMedia(game, i))}
|
|
60
|
+
</ZGrid>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const renderInfoCard = (game: IZRomulatorGame) => {
|
|
64
|
+
const renderGameInfoField = (
|
|
65
|
+
label: string,
|
|
66
|
+
value?: string,
|
|
67
|
+
display?: string,
|
|
68
|
+
) => (
|
|
69
|
+
<>
|
|
70
|
+
<ZLabel>{label}:</ZLabel>
|
|
71
|
+
<ZCaption
|
|
72
|
+
compact
|
|
73
|
+
className={`ZRomulatorGamePage-${kebabCase(label)}`}
|
|
74
|
+
data-value={value}
|
|
75
|
+
>
|
|
76
|
+
{firstDefined(value, display)}
|
|
77
|
+
</ZCaption>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const _players = new ZRomulatorPlayersSerialize().serialize(game.players);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<ZCard
|
|
85
|
+
name="info"
|
|
86
|
+
TitleProps={{
|
|
87
|
+
avatar: <ZIconFontAwesome name="gamepad" width={ZSizeFixed.Medium} />,
|
|
88
|
+
heading: "Information",
|
|
89
|
+
subHeading: "Game Details",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<ZGrid columns="auto 1fr" gap={ZSizeFixed.Medium}>
|
|
93
|
+
{renderGameInfoField("Name", game.name)}
|
|
94
|
+
{renderGameInfoField("File", game.file)}
|
|
95
|
+
{renderGameInfoField("Players", _players)}
|
|
96
|
+
{renderGameInfoField("Release Date", game.release)}
|
|
97
|
+
{renderGameInfoField("Developer", game.developer)}
|
|
98
|
+
{renderGameInfoField("Publisher", game.publisher)}
|
|
99
|
+
</ZGrid>
|
|
100
|
+
</ZCard>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const renderSynopsisCard = (game: IZRomulatorGame) => {
|
|
105
|
+
return (
|
|
106
|
+
<ZCard
|
|
107
|
+
name="synopsis"
|
|
108
|
+
TitleProps={{
|
|
109
|
+
avatar: <ZIconFontAwesome name="book" width={ZSizeFixed.Medium} />,
|
|
110
|
+
heading: "Synopsis",
|
|
111
|
+
subHeading: "Game description",
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<pre style={{ textWrap: "wrap" }}>
|
|
115
|
+
<ZParagraph compact>{game.description}</ZParagraph>
|
|
116
|
+
</pre>
|
|
117
|
+
</ZCard>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
35
121
|
const renderContent = () => {
|
|
36
122
|
if (isStateLoading(game)) {
|
|
37
123
|
return (
|
|
@@ -51,17 +137,15 @@ export function ZRomulatorGamePage() {
|
|
|
51
137
|
|
|
52
138
|
return (
|
|
53
139
|
<ZGrid
|
|
54
|
-
columns={{
|
|
55
|
-
xl: "1fr 1fr 1fr 1fr",
|
|
56
|
-
lg: "1fr 1fr 1fr",
|
|
57
|
-
md: "1fr 1fr",
|
|
58
|
-
sm: "1fr",
|
|
59
|
-
}}
|
|
140
|
+
columns={{ xl: "28rem auto", md: "1fr" }}
|
|
60
141
|
gap={ZSizeFixed.Medium}
|
|
142
|
+
align={{ items: "start" }}
|
|
61
143
|
>
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
144
|
+
<ZStack gap={ZSizeFixed.Medium}>
|
|
145
|
+
{renderInfoCard(game)}
|
|
146
|
+
{renderSynopsisCard(game)}
|
|
147
|
+
</ZStack>
|
|
148
|
+
{renderMediaGallery(game)}
|
|
65
149
|
</ZGrid>
|
|
66
150
|
);
|
|
67
151
|
};
|
package/src/media/media-card.tsx
CHANGED
|
@@ -24,7 +24,7 @@ const ZRomulatorMediaTypeName: Record<ZRomulatorMediaType, string> = {
|
|
|
24
24
|
[ZRomulatorGameMediaType.Marquee]: "Marquee",
|
|
25
25
|
[ZRomulatorGameMediaType.PhysicalMedia]: "Physical Media",
|
|
26
26
|
[ZRomulatorGameMediaType.Screenshot]: "Screenshot",
|
|
27
|
-
[ZRomulatorGameMediaType.Title]: "Title
|
|
27
|
+
[ZRomulatorGameMediaType.Title]: "Title",
|
|
28
28
|
[ZRomulatorGameMediaType.Video]: "Video",
|
|
29
29
|
[ZRomulatorSystemMediaType.Controller]: "Controller",
|
|
30
30
|
[ZRomulatorSystemMediaType.Picture]: "System Picture",
|
|
@@ -33,15 +33,15 @@ const ZRomulatorMediaTypeName: Record<ZRomulatorMediaType, string> = {
|
|
|
33
33
|
|
|
34
34
|
// TODO: Localization
|
|
35
35
|
const ZRomulatorMediaTypeDescription: Record<ZRomulatorMediaType, string> = {
|
|
36
|
-
[ZRomulatorGameMediaType.BackCover]: "Rear artwork
|
|
37
|
-
[ZRomulatorGameMediaType.Box3d]: "3D render
|
|
38
|
-
[ZRomulatorGameMediaType.Cover]: "Front cover art
|
|
39
|
-
[ZRomulatorGameMediaType.FanArt]: "Community
|
|
36
|
+
[ZRomulatorGameMediaType.BackCover]: "Rear packaging artwork.",
|
|
37
|
+
[ZRomulatorGameMediaType.Box3d]: "3D box render.",
|
|
38
|
+
[ZRomulatorGameMediaType.Cover]: "Front cover art.",
|
|
39
|
+
[ZRomulatorGameMediaType.FanArt]: "Community artwork.",
|
|
40
40
|
[ZRomulatorGameMediaType.Manual]: "Digital version of the game manual.",
|
|
41
|
-
[ZRomulatorGameMediaType.Marquee]: "
|
|
42
|
-
[ZRomulatorGameMediaType.PhysicalMedia]: "Disc or cartridge
|
|
41
|
+
[ZRomulatorGameMediaType.Marquee]: "Game Logo.",
|
|
42
|
+
[ZRomulatorGameMediaType.PhysicalMedia]: "Disc or cartridge.",
|
|
43
43
|
[ZRomulatorGameMediaType.Screenshot]: "In-game action.",
|
|
44
|
-
[ZRomulatorGameMediaType.Title]: "Screen
|
|
44
|
+
[ZRomulatorGameMediaType.Title]: "Screen upon load.",
|
|
45
45
|
[ZRomulatorGameMediaType.Video]: "Gameplay or trailer video preview.",
|
|
46
46
|
[ZRomulatorSystemMediaType.Controller]: "Primary controller image.",
|
|
47
47
|
[ZRomulatorSystemMediaType.Picture]: "Image of system hardware.",
|