Start showcase pages (#657)

This commit is contained in:
Corwin 2024-04-30 20:52:20 +01:00 committed by GitHub
commit 737a1582dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 531 additions and 118 deletions

View file

@ -98,18 +98,19 @@ setup-cargo-wasm:
build-website-backtrace: build-website-backtrace:
(cd website/backtrace && wasm-pack build --target web) (cd website/backtrace && wasm-pack build --target web)
rm -rf website/agb/src/app/vendor/backtrace rm -rf website/agb/src/vendor/backtrace
mkdir -p website/agb/src/app/vendor mkdir -p website/agb/src/vendor
cp website/backtrace/pkg website/agb/src/app/vendor/backtrace -r cp website/backtrace/pkg website/agb/src/vendor/backtrace -r
build-mgba-wasm: build-mgba-wasm:
rm -rf website/agb/src/app/mgba/vendor rm -rf website/agb/src/components/mgba/vendor
mkdir website/agb/src/app/mgba/vendor mkdir website/agb/src/components/mgba/vendor
{{podman_command}} build --file website/mgba-wasm/BuildMgbaWasm --output=website/agb/src/app/mgba/vendor . {{podman_command}} build --file website/mgba-wasm/BuildMgbaWasm --output=website/agb/src/components/mgba/vendor .
build-combo-rom-site: build-combo-rom-site:
just _build-rom "examples/combo" "AGBGAMES" just _build-rom "examples/combo" "AGBGAMES"
gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz mkdir -p website/agb/src/roms
gzip -9 -c examples/target/examples/combo.gba > website/agb/src/roms/combo.gba.gz
setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { ContentBlock } from "../contentBlock"; import { ContentBlock } from "../../components/contentBlock";
import { useState } from "react"; import { useState } from "react";
import { styled } from "styled-components"; import { styled } from "styled-components";
@ -69,8 +69,11 @@ export default function ColourPicker() {
} }
return ( return (
<ContentBlock> <>
<ContentBlock color="#AAAFFF">
<h1>agbrs colour converter</h1> <h1>agbrs colour converter</h1>
</ContentBlock>
<ContentBlock>
<PickerWrapper> <PickerWrapper>
<PickerColumn> <PickerColumn>
<h2>Regular RGB8</h2> <h2>Regular RGB8</h2>
@ -107,6 +110,7 @@ export default function ColourPicker() {
</PickerColumn> </PickerColumn>
</PickerWrapper> </PickerWrapper>
</ContentBlock> </ContentBlock>
</>
); );
} }

View file

@ -2,7 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ContentBlock } from "../contentBlock"; import { ContentBlock } from "../../components/contentBlock";
import { GameDeveloperSummary } from "./gameDeveloperSummary"; import { GameDeveloperSummary } from "./gameDeveloperSummary";
import { styled } from "styled-components"; import { styled } from "styled-components";
import { Debug } from "./debug"; import { Debug } from "./debug";
@ -14,20 +14,28 @@ export function BacktracePage() {
}, []); }, []);
return ( return (
<ContentBlock> <>
<ContentBlock color="#AAAFFF">
<h1>agbrs crash backtrace viewer</h1> <h1>agbrs crash backtrace viewer</h1>
</ContentBlock>
<ContentBlock>
<p> <p>
You likely got here from the link / QR code that was displayed when a You likely got here from the link / QR code that was displayed when a
game you were playing crashed. This is the default crash page for games game you were playing crashed. This is the default crash page for
made using the agb library. games made using the agb library.
</p> </p>
<p> <p>
The creator of the game is <em>very</em> likely interested in the code The creator of the game is <em>very</em> likely interested in the code
below <em>along with</em> a description of what you were doing at the below <em>along with</em> a description of what you were doing at the
time.{" "} time.{" "}
<strong>Send these to the creator of the game you are playing.</strong> <strong>
Send these to the creator of the game you are playing.
</strong>
</p> </p>
<BacktraceCopyDisplay backtrace={backtrace} setBacktrace={setBacktrace} /> <BacktraceCopyDisplay
backtrace={backtrace}
setBacktrace={setBacktrace}
/>
<p> <p>
<em> <em>
The owners of this website are not necessarily the creators of the The owners of this website are not necessarily the creators of the
@ -38,6 +46,7 @@ export function BacktracePage() {
{backtrace && <Debug encodedBacktrace={backtrace} />} {backtrace && <Debug encodedBacktrace={backtrace} />}
<GameDeveloperSummary /> <GameDeveloperSummary />
</ContentBlock> </ContentBlock>
</>
); );
} }

View file

@ -1,5 +1,9 @@
import { styled } from "styled-components"; import { styled } from "styled-components";
import { AddressInfo, AgbDebug, useAgbDebug } from "../useAgbDebug.hook"; import {
AddressInfo,
AgbDebug,
useAgbDebug,
} from "../../hooks/useAgbDebug.hook";
import { ReactNode, useMemo, useState } from "react"; import { ReactNode, useMemo, useState } from "react";
const BacktraceListWrapper = styled.div` const BacktraceListWrapper = styled.div`

View file

@ -1,6 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globalStyles.css"; import "./globalStyles.css";
import StyledComponentsRegistry from "./registry"; import StyledComponentsRegistry, { BodyPixelRatio } from "./registry";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "agb - a rust framework for making Game Boy Advance games", title: "agb - a rust framework for making Game Boy Advance games",
@ -13,9 +13,9 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body> <StyledComponentsRegistry>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry> <BodyPixelRatio>{children}</BodyPixelRatio>
</body> </StyledComponentsRegistry>
</html> </html>
); );
} }

View file

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View file

@ -1,35 +1,18 @@
"use client"; "use client";
import styled from "styled-components"; import styled from "styled-components";
import { CenteredBlock, ContentBlock } from "./contentBlock"; import { CenteredBlock, ContentBlock } from "../components/contentBlock";
import MgbaWrapper from "./mgba/mgbaWrapper"; import MgbaWrapper from "../components/mgba/mgbaWrapper";
import Image from "next/image"; import Image from "next/image";
import left from "./gba-parts/left.png"; import left from "./left.png";
import right from "./gba-parts/right.png"; import right from "./right.png";
import { MobileController } from "./mobileController"; import { MobileController } from "../components/mobileController/mobileController";
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { GbaKey } from "./mgba/bindings"; import { GbaKey } from "../components/mgba/bindings";
import { useClientValue } from "./useClientValue.hook"; import { useClientValue } from "../hooks/useClientValue.hook";
import { MgbaHandle } from "./mgba/mgba"; import { MgbaHandle } from "../components/mgba/mgba";
import { ExternalLink, ExternalLinkBlock } from "@/components/externalLink";
const ExternalLink = styled.a`
text-decoration: none;
color: black;
background-color: #fad288;
border: solid #fad288 2px;
border-radius: 5px;
padding: 5px 10px;
&:hover {
border: solid black 2px;
}
`;
const HelpLinks = styled.div`
display: flex;
justify-content: space-around;
`;
const GameDisplay = styled.div` const GameDisplay = styled.div`
height: min(calc(100vw / 1.5), min(90vh, 480px)); height: min(calc(100vw / 1.5), min(90vh, 480px));
@ -78,7 +61,7 @@ function shouldStartPlaying(isTouchScreen: boolean | undefined) {
return !isTouchScreen; return !isTouchScreen;
} }
const COMBO_GAME = new URL("combo.gba.gz", import.meta.url); const COMBO_GAME = new URL("../roms/combo.gba.gz", import.meta.url);
function MgbaWithControllerSides() { function MgbaWithControllerSides() {
const mgba = useRef<MgbaHandle>(null); const mgba = useRef<MgbaHandle>(null);
@ -136,14 +119,14 @@ function MgbaWithControllerSides() {
export default function Home() { export default function Home() {
return ( return (
<> <>
<ContentBlock> <ContentBlock color="#AAAFFF">
<h1>agb - a rust framework for making Game Boy Advance games</h1> <h1>agb - a rust framework for making Game Boy Advance games</h1>
</ContentBlock> </ContentBlock>
<ContentBlock uncentered> <ContentBlock uncentered>
<MgbaWithControllerSides /> <MgbaWithControllerSides />
</ContentBlock> </ContentBlock>
<ContentBlock color="#f5755e"> <ContentBlock color="#256256">
<HelpLinks> <ExternalLinkBlock>
<ExternalLink href="https://github.com/agbrs/agb"> <ExternalLink href="https://github.com/agbrs/agb">
GitHub GitHub
</ExternalLink> </ExternalLink>
@ -151,7 +134,8 @@ export default function Home() {
<ExternalLink href="https://docs.rs/agb/latest/agb/"> <ExternalLink href="https://docs.rs/agb/latest/agb/">
Docs Docs
</ExternalLink> </ExternalLink>
</HelpLinks> <ExternalLink href="./showcase">Showcase</ExternalLink>
</ExternalLinkBlock>
</ContentBlock> </ContentBlock>
</> </>
); );

View file

@ -1,8 +1,8 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useServerInsertedHTML } from "next/navigation"; import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components"; import styled, { ServerStyleSheet, StyleSheetManager } from "styled-components";
export default function StyledComponentsRegistry({ export default function StyledComponentsRegistry({
children, children,
@ -27,3 +27,18 @@ export default function StyledComponentsRegistry({
</StyleSheetManager> </StyleSheetManager>
); );
} }
const BodyWithPixelRatio = styled.body<{
$pixel: number;
}>`
--device-pixel: calc(1px / ${(props) => props.$pixel});
`;
export function BodyPixelRatio({ children }: { children: React.ReactNode }) {
const [pixel, setPixel] = useState(1);
useEffect(() => {
setPixel(window.devicePixelRatio);
}, []);
return <BodyWithPixelRatio $pixel={pixel}>{children}</BodyWithPixelRatio>;
}

View file

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 206 B

View file

@ -0,0 +1,81 @@
import { slugify } from "@/sluggify";
import { Games, ShowcaseGame } from "../games";
import { ContentBlock } from "@/components/contentBlock";
import { ExternalLink, ExternalLinkBlock } from "@/components/externalLink";
import Link from "next/link";
import {
BackToShowcaseWrapper,
DescriptionAndScreenshots,
Description,
Screenshots,
} from "./styles";
export async function generateStaticParams() {
return Games.map((game) => ({
game: slugify(game.name),
}));
}
function getGame(slug: string) {
const game = Games.find((game) => slugify(game.name) === slug);
if (!game) {
throw new Error("Not valid game name, this should never happen");
}
return game;
}
export function generateMetadata({ params }: { params: { game: string } }) {
const game = getGame(params.game);
return { title: game.name };
}
export default function Page({ params }: { params: { game: string } }) {
const game = getGame(params.game);
return <Display game={game} />;
}
function DeveloperNames({ names }: { names: string[] }) {
if (names.length === 0) {
throw new Error("You must specify developer names");
}
if (names.length === 1) {
return names[0];
}
if (names.length === 2) {
return names.join(" and ");
}
const first = names.slice(0, -1);
return first.join(", ") + `, and ${names[names.length - 1]}`;
}
function Display({ game }: { game: ShowcaseGame }) {
return (
<>
<ContentBlock color="#AAAFFF">
<BackToShowcaseWrapper>
<Link href={`../showcase#${slugify(game.name)}`}>
<strong>&lt;</strong> Back to showcase
</Link>
</BackToShowcaseWrapper>
<h1>{game.name}</h1>
<div>
By: <DeveloperNames names={game.developers} />
</div>
</ContentBlock>
<ContentBlock>
<DescriptionAndScreenshots>
<Description>{game.description}</Description>
<Screenshots screenshots={game.screenshots} />
</DescriptionAndScreenshots>
</ContentBlock>
<ContentBlock color="#256256">
<ExternalLinkBlock>
{game.itch && (
<ExternalLink href={game.itch.href}>View on itch.io</ExternalLink>
)}
</ExternalLinkBlock>
</ContentBlock>
</>
);
}

View file

@ -0,0 +1,56 @@
"use client";
import { styled } from "styled-components";
import Image, { StaticImageData } from "next/image";
export function Screenshots({
screenshots,
}: {
screenshots: StaticImageData[];
}) {
return (
<ScreenshotsWrapper>
{screenshots.map((screenshot) => (
<Screenshot src={screenshot} alt="" key={screenshot.src} />
))}
</ScreenshotsWrapper>
);
}
const ScreenshotsWrapper = styled.div`
flex: 4;
text-align: center;
`;
const Screenshot = styled(Image)`
width: 100%;
width: max(
round(down, 100%, calc(240 * var(--device-pixel))),
calc(240 * var(--device-pixel))
);
height: auto;
image-rendering: pixelated;
`;
export const Description = styled.div`
flex: 5;
:first-child {
margin-top: 0;
}
`;
export const DescriptionAndScreenshots = styled.div`
display: flex;
gap: 16px;
@media (max-width: 1000px) {
display: block;
}
`;
export const BackToShowcaseWrapper = styled.div`
a {
text-decoration: none;
color: black;
}
`;

View file

@ -0,0 +1,30 @@
import { ShowcaseGame, shuffle } from "@/app/showcase/games";
import d1 from "./the-dungeon-puzzlers-lament-0.png";
import d2 from "./the-dungeon-puzzlers-lament-1.png";
const Screenshots = [d1, d2];
export const Dungeon: ShowcaseGame = {
name: "The Dungeon Puzzler's Lament",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani"]),
screenshots: Screenshots,
description: (
<>
<p>
Get through as many levels as possible in this space themed, dice
rolling roguelike.
</p>
<p>
Build up powerful combos to defeat enemies which keep getting stronger.
Slowly acquire more dice and upgrade them in order to handle the
increasing strength of the enemies you face.
</p>
<p>
Hyperspace Roll was influenced by great games such as Slay the Spire,
FTL and the board game Escape: The Curse of the Temple.
</p>
</>
),
itch: new URL("https://setsquare.itch.io/dungeon-puzzlers-lament"),
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -0,0 +1,43 @@
import { ShowcaseGame, shuffle } from "../../../games";
import h1 from "./the-hat-chooses-the-wizard-0.png";
import h2 from "./the-hat-chooses-the-wizard-1.png";
import h3 from "./the-hat-chooses-the-wizard-2.png";
import h4 from "./the-hat-chooses-the-wizard-3.png";
const HatWizScreenshots = [h1, h2, h3, h4];
export const HatWiz: ShowcaseGame = {
name: "The Hat Chooses the Wizard",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani"]),
screenshots: HatWizScreenshots,
description: (
<>
<p>
&lsquo;The Hat Chooses the Wizard&rsquo; is a 2D platformer. This game
was developed as an entry for the GMTK game jam 2021, with the theme
&ldquo;joined together&rdquo;. The entire game, except for the music,
was produced in just 48 hours.
</p>
<p>
In this game, you play as a wizard searching for his missing staff.
However, the path to the staff is filled with dangerous obstacles and
monsters. Luckily, you have a powerful magic hat that can be thrown and
recalled, allowing you to fly towards it and reach otherwise
inaccessible platforms.
</p>
<p>
With this unique mechanic, you can explore the game&apos;s levels and
defeat enemies. The game&apos;s simple but challenging gameplay will put
your platforming skills to the test as you try to reach the end.
</p>
<p>
The music is by Otto Halmén released under creative commons attribution
3.0 and can be found here:{" "}
<a href="https://opengameart.org/content/sylvan-waltz-standard-looped-version">
opengameart.org/content/sylvan-waltz-standard-looped-version
</a>
</p>
</>
),
itch: new URL("https://lostimmortal.itch.io/the-hat-chooses-the-wizard"),
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,30 @@
import { ShowcaseGame, shuffle } from "@/app/showcase/games";
import h1 from "./hyperspace-roll-0.png";
import h2 from "./hyperspace-roll-1.png";
const Screenshots = [h1, h2];
export const Hyperspace: ShowcaseGame = {
name: "Hyperspace Roll",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani", "Sam Williams"]),
screenshots: Screenshots,
description: (
<>
<p>
Get through as many levels as possible in this space themed, dice
rolling roguelike.
</p>
<p>
Build up powerful combos to defeat enemies which keep getting stronger.
Slowly acquire more dice and upgrade them in order to handle the
increasing strength of the enemies you face.
</p>
<p>
Hyperspace Roll was influenced by great games such as Slay the Spire,
FTL and the board game Escape: The Curse of the Temple.
</p>
</>
),
itch: new URL("https://lostimmortal.itch.io/hyperspace-roll"),
};

View file

@ -0,0 +1,26 @@
import { ShowcaseGame, shuffle } from "@/app/showcase/games";
import p1 from "./the-purple-night-0.png";
import p2 from "./the-purple-night-1.png";
const Screenshots = [p1, p2];
export const Purple: ShowcaseGame = {
name: "The Purple Night",
developers: shuffle(["Corwin Kuiper", "Gwilym Inzani", "Sam Williams"]),
screenshots: Screenshots,
description: (
<>
<p>Save a lost soul and take them safely back to the afterlife!</p>
<p>
The purple night is a platformer game where your health bar is your
sword. The more damage you take, the shorter your sword gets, making you
more nimble and your attacks faster, but also increasing your risk.
</p>
<p>
Do you choose to stay at high health but low mobility, or low health and
higher mobility?
</p>
</>
),
itch: new URL("https://lostimmortal.itch.io/the-purple-night"),
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,34 @@
import { StaticImageData } from "next/image";
import { ReactNode } from "react";
import { HatWiz } from "./data/tapir/hatwiz/hatwiz";
import { Purple } from "./data/tapir/purple/purple";
import { Hyperspace } from "./data/tapir/hyperspace/hyperspace";
import { Dungeon } from "./data/tapir/dungeon/dungeon";
export interface ShowcaseGame {
name: string;
developers: string[];
rom?: URL;
screenshots: StaticImageData[];
description: ReactNode;
itch?: URL;
otherLink?: URL;
}
export function shuffle<T>(a: T[]) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
export const Games: ShowcaseGame[] = shuffle([
HatWiz,
Purple,
Hyperspace,
Dungeon,
]);

View file

@ -0,0 +1,39 @@
import { Metadata } from "next";
import { ContentBlock } from "@/components/contentBlock";
import { Games, ShowcaseGame } from "./games";
import { slugify } from "@/sluggify";
import { GameDisplay, GameGrid, GameImage } from "./styles";
export const metadata: Metadata = {
title: "Showcase - agb",
};
export default function ColourPickerPage() {
return (
<>
<ContentBlock color="#AAAFFF">
<h1>Showcase</h1>
</ContentBlock>
<ContentBlock uncentered>
<GameGrid>
{Games.map((game, idx) => (
<Game key={idx} game={game} />
))}
</GameGrid>
</ContentBlock>
</>
);
}
function Game({ game }: { game: ShowcaseGame }) {
const showcaseImage = game.screenshots[game.screenshots.length - 1];
return (
<GameDisplay
href={`./showcase/${slugify(game.name)}`}
id={slugify(game.name)}
>
<GameImage src={showcaseImage} alt={`Screenshot of ${game.name}`} />
<h2>{game.name}</h2>
</GameDisplay>
);
}

View file

@ -0,0 +1,34 @@
"use client";
import Link from "next/link";
import styled from "styled-components";
import Image from "next/image";
export const GameGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, min(100vw, 600px));
justify-content: center;
gap: 48px;
`;
export const GameImage = styled(Image)`
width: 100%;
width: max(
round(down, 100%, calc(240 * var(--device-pixel))),
min(calc(240 * var(--device-pixel)), 100vw)
);
height: auto;
image-rendering: pixelated;
`;
export const GameDisplay = styled(Link)`
width: 100%;
text-align: center;
color: black;
text-decoration: none;
h2 {
margin: 0;
margin-top: 8px;
}
`;

View file

@ -14,11 +14,8 @@ const Section = styled.section<{ $color: string }>`
const CENTERED_CSS = ` const CENTERED_CSS = `
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
max-width: 60%; width: 60%;
min-width: min(95%, 1000px);
@media (max-width: 40rem) {
max-width: 90%;
}
`; `;
export const CenteredBlock = styled.div` export const CenteredBlock = styled.div`

View file

@ -0,0 +1,23 @@
"use client";
import Link from "next/link";
import { styled } from "styled-components";
export const ExternalLink = styled(Link)`
text-decoration: none;
color: black;
background-color: #fad288;
border: solid #fad288 2px;
border-radius: 5px;
padding: 5px 10px;
&:hover {
border: solid black 2px;
}
`;
export const ExternalLinkBlock = styled.div`
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: space-around;
`;

View file

Before

Width:  |  Height:  |  Size: 163 B

After

Width:  |  Height:  |  Size: 163 B

View file

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

View file

Before

Width:  |  Height:  |  Size: 190 B

After

Width:  |  Height:  |  Size: 190 B

View file

Before

Width:  |  Height:  |  Size: 189 B

After

Width:  |  Height:  |  Size: 189 B

View file

Before

Width:  |  Height:  |  Size: 241 B

After

Width:  |  Height:  |  Size: 241 B

View file

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 156 B

View file

@ -1,4 +1,4 @@
import { FC, useMemo, useState } from "react"; import { useMemo, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import Image from "next/image"; import Image from "next/image";
@ -8,8 +8,8 @@ import DPad from "./gba-parts/dpad.png";
import ABButtons from "./gba-parts/ab.png"; import ABButtons from "./gba-parts/ab.png";
import Select from "./gba-parts/SELECT.png"; import Select from "./gba-parts/SELECT.png";
import Start from "./gba-parts/START.png"; import Start from "./gba-parts/START.png";
import { GbaKey } from "./mgba/bindings"; import { GbaKey } from "../mgba/bindings";
import { MgbaHandle } from "./mgba/mgba"; import { MgbaHandle } from "../mgba/mgba";
const MobileControls = styled.div` const MobileControls = styled.div`
display: flex; display: flex;

View file

@ -4,7 +4,7 @@ import debugInit, {
DebugFile, DebugFile,
InitOutput, InitOutput,
AddressInfo, AddressInfo,
} from "./vendor/backtrace/backtrace"; } from "../vendor/backtrace/backtrace";
let agbDebug: Promise<InitOutput> | undefined; let agbDebug: Promise<InitOutput> | undefined;

View file

@ -0,0 +1,3 @@
export function slugify(x: string) {
return x.toLowerCase().split(" ").join("-");
}