Skip to content

Commit

Permalink
feat: add basic projects list and hex flower engine project
Browse files Browse the repository at this point in the history
  • Loading branch information
ericrallen committed May 3, 2024
1 parent 99aff53 commit 5bb947e
Show file tree
Hide file tree
Showing 20 changed files with 310 additions and 65 deletions.
2 changes: 1 addition & 1 deletion _games/gravedigger-dark-ritual.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ coverImage: /assets/blog/gravedigger/cover.png
bannerImage: /assets/blog/gravedigger/hero.png
ogImage:
url: /assets/blog/gravedigger/cover.png
releaseDate: "TBD"
date: "TBD"
ctas:
- label: "Play the Prototype"
url: "https://dvdagames.itch.io/dark-ritual"
Expand Down
24 changes: 24 additions & 0 deletions _projects/hex-flower-engine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: "Hex Flower Engine"
excerpt: An interactive implementation of Goblin's Henchman's Hex Flower Engine in React, using the better random number generator from our TypeScript dice rolling library for rolls that are more fairly distributed.
coverImage: /assets/blog/hex-flower-engine/hex-flower.png
bannerImage: /assets/blog/hex-flower-engine/hex-flower.gif
ogImage:
url: /assets/blog/hex-flower-engine/hex-flower.gif
date: 2020-07-11
ctas:
- label: "Run the Engine"
url: "https://dvdagames.github.io/react-hex-flower-engine/"
price: "FREE"
---
The [Hex Flower Engine](https://goblinshenchman.wordpress.com/2018/10/25/2d6-hex-power-flower/) is an ingenious invention from [Goblin's Henchman](https://goblinshenchman.wordpress.com/) that gives Game Masters (GMs) and Dungeon Masters (DMs) a way to generate random results that are more predictable and feel more realistic than a simple table or a single die roll.

> A versatile game engine using 2D6 and a 19-Hex Flower (it’s like a random table, but with a memory).
It relies on rolling `2d6` (two six-sided dice) and using the results to decide which direction to move in a grid of 19 hexagons laid out in an even larger hexagon.

[Goblin's Henchman has written way more than I ever could describing the various use cases for and ideas behind the engine](https://goblinshenchman.wordpress.com/category/hex-flower/), so I'll leave that to them.

This particular implementation of their ideas is a simple React-based web application hosted on GitHub Pages. The code is entirely open source, so you can easily fork it to tweak to your liking, or submit an Issue or Pull Request for some feature you would like to see.

This was selfishly created for my own use as a Tempest Cleric in a D&D campaign where I wanted to know if there was ever an existing storm that I could use to power up my Call Lightning spell. The DM generously created a version of the Hex Flower Engine for us to use, so I built the campaign an interactive digital version we could rely on each session.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions src/app/_components/date-formatter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { parseISO, format } from "date-fns";
import { format } from "date-fns/format";

type Props = {
dateString: string;
};

const DateFormatter = ({ dateString }: Props) => {
try {
const date = parseISO(dateString);
const date = format(dateString, "yyyy-MM-dd");

return <time dateTime={dateString}>{format(date, "yyyy-MM-dd")}</time>;
return <time dateTime={dateString}>{date}</time>;
} catch (e) {
return <span>{dateString}</span>;
console.log("ERROR: cannot parse date");
console.error(e);
return <span>{dateString.toString()}</span>;
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/app/_components/game-header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import CoverImage from "./cover-image";
import DateFormatter from "./date-formatter";
import { PostTitle } from "@/app/_components/post-title";
import { CTA } from "@/interfaces/game";
import { CTA } from "@/interfaces/cta";

type Props = {
title: string;
Expand Down
3 changes: 1 addition & 2 deletions src/app/_components/game-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { type Author } from "@/interfaces/author";
import { type CTA } from "@/interfaces/game";
import { type CTA } from "@/interfaces/cta";
import Link from "next/link";
import CoverImage from "./cover-image";
import DateFormatter from "./date-formatter";
Expand Down
4 changes: 3 additions & 1 deletion src/app/_components/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export function Nav({ includeHome = true }: NavProps): React.ReactElement {
<li className="mr-[20px]">
<Link href="/games">Games</Link>
</li>
<li className="mr-[20px]">Projects</li>
<li className="mr-[20px]">
<Link href="/projects">Projects</Link>
</li>
<li className="">
<Link href="/about">About</Link>
</li>
Expand Down
43 changes: 43 additions & 0 deletions src/app/_components/project-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type CTA } from "@/interfaces/cta";
import Link from "next/link";
import CoverImage from "./cover-image";
import DateFormatter from "./date-formatter";

type Props = {
title: string;
coverImage: string;
releaseDate: string;
excerpt: string;
slug: string;
price: string;
ctas: CTA[];
};

export function ProjectPreview({ title, coverImage, releaseDate, excerpt, slug, price, ctas }: Props) {
return (
<div className="w-[33%]">
<h3 className="text-xl mb-3 leading-snug">
<Link as={`/projects/${slug}`} href="/projects/[slug]" className="hover:underline">
{title}
</Link>
</h3>
<div className="mb-5">
<CoverImage slug={slug} title={title} src={coverImage} />
</div>
<div className="text-lg mb-4">
<strong>Release Date:</strong> <DateFormatter dateString={releaseDate} />
</div>
{typeof ctas !== "undefined" && (
<div className="mb-4">
{ctas.length > 0 &&
ctas.map((cta) => (
<a key={cta.label} href={cta.url} className="btn">
{cta.label}
</a>
))}
</div>
)}
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
</div>
);
}
4 changes: 2 additions & 2 deletions src/app/games/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Header from "../../_components/header";
import { PostBody } from "../../_components/post-body";
import { GameHeader } from "../../_components/game-header";

export default async function Post({ params }: Params) {
export default async function Game({ params }: Params) {
const game = getGameBySlug(params.slug);

if (!game) {
Expand All @@ -21,7 +21,7 @@ export default async function Post({ params }: Params) {
<Container>
<Header />
<article className="mb-32">
<GameHeader title={game.title} bannerImage={game.bannerImage} releaseDate={game.releaseDate} ctas={game.ctas} />
<GameHeader title={game.title} bannerImage={game.bannerImage} releaseDate={game.date} ctas={game.ctas} />
<PostBody content={content} />
</article>
</Container>
Expand Down
12 changes: 10 additions & 2 deletions src/app/games/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Container from "../_components/container";
import Header from "../_components/header";
import { GamePreview } from "../_components/game-preview";

export default async function Post() {
export default async function Games() {
const games = getAllGames();

if (!games) {
Expand All @@ -24,7 +24,7 @@ export default async function Post() {
key={game.slug}
title={game.title}
coverImage={game.coverImage}
releaseDate={game.releaseDate}
releaseDate={game.date}
ctas={game.ctas}
excerpt={game.excerpt}
slug={game.slug}
Expand Down Expand Up @@ -53,3 +53,11 @@ export function generateMetadata(): Metadata {
},
};
}

export async function generateStaticParams() {
const games = getAllGames();

return games.map((game) => ({
slug: game.slug,
}));
}
61 changes: 61 additions & 0 deletions src/app/projects/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAllProjects, getProjectBySlug } from "../../../lib/api";
import markdownToHtml from "../../../lib/markdownToHtml";
import Container from "../../_components/container";
import Header from "../../_components/header";
import { PostBody } from "../../_components/post-body";
import { GameHeader } from "../../_components/game-header";

export default async function Project({ params }: Params) {
const project = getProjectBySlug(params.slug);

if (!project) {
return notFound();
}

const content = await markdownToHtml(project.content || "");

return (
<main>
<Container>
<Header />
<article className="mb-32">
<GameHeader title={project.title} bannerImage={project.bannerImage} releaseDate={project.date} ctas={project.ctas} />
<PostBody content={content} />
</article>
</Container>
</main>
);
}

type Params = {
params: {
slug: string;
};
};

export function generateMetadata({ params }: Params): Metadata {
const project = getProjectBySlug(params.slug);

if (!project) {
return notFound();
}

const title = `${project.title} | DVDA Games`;

return {
openGraph: {
title,
images: [project.ogImage.url],
},
};
}

export async function generateStaticParams() {
const projects = getAllProjects();

return projects.map((project) => ({
slug: project.slug,
}));
}
63 changes: 63 additions & 0 deletions src/app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAllProjects } from "../../lib/api";
import Container from "../_components/container";
import Header from "../_components/header";
import { ProjectPreview } from "../_components/project-preview";

export default async function Projects() {
const projects = getAllProjects();

if (!projects) {
return notFound();
}

return (
<main>
<Container>
<Header />
<h1 className="text-2xl md:text-3xl lg:text-4xl font-bold tracking-tighter leading-tight mb-[20px]">Our Projects</h1>
<p className="text-md mb-[10px]">Some open-source tools we've created for you to use for free.</p>
<article className="mb-32">
{projects.map((project) => (
<ProjectPreview
key={project.slug}
title={project.title}
coverImage={project.coverImage}
releaseDate={project.date}
ctas={project.ctas}
excerpt={project.excerpt}
slug={project.slug}
price={project.price}
/>
))}
</article>
</Container>
</main>
);
}

export function generateMetadata(): Metadata {
const projects = getAllProjects();

if (!projects) {
return notFound();
}

const title = `Our Projects | DVDA Games`;

return {
openGraph: {
title,
images: projects.map((project) => project.ogImage.url),
},
};
}

export async function generateStaticParams() {
const projects = getAllProjects();

return projects.map((project) => ({
slug: project.slug,
}));
}
7 changes: 7 additions & 0 deletions src/interfaces/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Game } from "./game";
import type { Post } from "./post";
import type { Project } from "./project";

export type ContentType = "post" | "game" | "project";

export type Content = Post | Game | Project;
4 changes: 4 additions & 0 deletions src/interfaces/cta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type CTA = {
label: string;
url: string;
};
8 changes: 3 additions & 5 deletions src/interfaces/game.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
export type CTA = {
label: string;
url: string;
};
import type { CTA } from "./cta";

export type Game = {
slug: string;
title: string;
releaseDate: string;
date: string;
price: string;
coverImage: string;
bannerImage: string;
Expand All @@ -17,4 +14,5 @@ export type Game = {
ctas: CTA[];
content: string;
featured?: boolean;
archived?: boolean;
};
1 change: 1 addition & 0 deletions src/interfaces/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export type Post = {
content: string;
preview?: boolean;
featured?: boolean;
archived?: boolean;
};
19 changes: 19 additions & 0 deletions src/interfaces/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { CTA } from "./cta";

export type Project = {
slug: string;
title: string;
url: string;
date: string;
price: string;
coverImage: string;
bannerImage: string;
excerpt: string;
ogImage: {
url: string;
};
ctas: CTA[];
content: string;
featured?: boolean;
archived?: boolean;
};
Loading

0 comments on commit 5bb947e

Please sign in to comment.