@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/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-DscDEeqF.js"></script>
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.16.0",
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.16.0",
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": "56e07d7f5f7c01962d6838eee99bac360917b850"
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
  });
@@ -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 { ZRomulatorGameMediaType } from "@zthun/romulator-client";
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
- {images.map((type) => (
63
- <ZRomulatorMediaCard key={type} identifier={game.id} type={type} />
64
- ))}
144
+ <ZStack gap={ZSizeFixed.Medium}>
145
+ {renderInfoCard(game)}
146
+ {renderSynopsisCard(game)}
147
+ </ZStack>
148
+ {renderMediaGallery(game)}
65
149
  </ZGrid>
66
150
  );
67
151
  };
@@ -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 Screen",
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 from the game packaging.",
37
- [ZRomulatorGameMediaType.Box3d]: "3D render showcasing the game box.",
38
- [ZRomulatorGameMediaType.Cover]: "Front cover art highlighting the game.",
39
- [ZRomulatorGameMediaType.FanArt]: "Community-created artwork for the game.",
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]: "Arcade marquee graphic used on cabinets.",
42
- [ZRomulatorGameMediaType.PhysicalMedia]: "Disc or cartridge for the game.",
41
+ [ZRomulatorGameMediaType.Marquee]: "Game Logo.",
42
+ [ZRomulatorGameMediaType.PhysicalMedia]: "Disc or cartridge.",
43
43
  [ZRomulatorGameMediaType.Screenshot]: "In-game action.",
44
- [ZRomulatorGameMediaType.Title]: "Screen when the game loads.",
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.",