add author page!

This commit is contained in:
dylan 2024-04-02 19:04:34 -07:00
parent 2b614dcb79
commit 11783ba2fb
6 changed files with 177 additions and 52 deletions

62
src/client/AuthorPage.tsx Normal file
View File

@ -0,0 +1,62 @@
import { Link, useParams } from "react-router-dom"
import { useEffect, useState } from "react";
import { DbRelease } from "../server/dbal/dbal";
import { css } from "@emotion/css";
type Info = {
author: string | null;
games: {slug: string; releases: DbRelease[]}[];
}
export const AuthorPage = () => {
const {author} = useParams();
const [info, setInfo] = useState<Info | null>(null);
useEffect(() => {
const fetchInfo = async () => {
let url = `/api/author?author=${author}`;
const information = await fetch(url);
const json = await information.json();
console.log('json', json);
setInfo(json);
}
fetchInfo();
}, [setInfo, author]);
if (!info) {
return (
<div>
LOADING...
</div>
)
}
if (!info.author) {
return (
<div>
NOT FOUND
</div>
)
}
return (
<div className={css`
margin: auto;
width: max-content;
max-inline-size: 66ch;
padding: 1.5em;
display: flex;
flex-direction: column;
gap: 1em;
`}>
<h1>{author}</h1>
{
info.games.map(game => (
<Link key={game.slug} to={`/u/${author}/${game.slug}`}>
<h3>{game.releases[0].manifest.title ?? game.slug}</h3>
</Link>
))
}
</div>
)
}

View File

@ -1,4 +1,4 @@
import { useNavigate, useParams } from "react-router-dom" import { Link, useParams } from "react-router-dom"
import { Pico8Console } from "./pico8-client/Pico8Console"; import { Pico8Console } from "./pico8-client/Pico8Console";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DbRelease } from "../server/dbal/dbal"; import { DbRelease } from "../server/dbal/dbal";
@ -10,15 +10,19 @@ type Info = {
} }
export const GamePage = () => { export const GamePage = () => {
const {author, slug, version} = useParams(); const {author, slug} = useParams();
const navigate = useNavigate(); // const [searchParams, setSearchParams] = useSearchParams();
// const version = searchParams.get('v');
const [v, setVersion] = useState<string | null>(null);
const [info, setInfo] = useState<Info | null>(null); const [info, setInfo] = useState<Info | null>(null);
const version = v ?? info?.release?.version ?? info?.versions[0];
useEffect(() => { useEffect(() => {
const fetchInfo = async () => { const fetchInfo = async () => {
let url = `/api/release?author=${author}&slug=${slug}`; let url = `/api/release?author=${author}&slug=${slug}`;
if (version) { if (version) {
url += `&version=${version.slice(1)}`; url += `&version=${version}`;
} }
const information = await fetch(url); const information = await fetch(url);
const json = await information.json(); const json = await information.json();
@ -29,9 +33,7 @@ export const GamePage = () => {
if (!info) { if (!info) {
return ( return (
<div className={` <div>
min-height: 100vh;
`}>
LOADING... LOADING...
</div> </div>
) )
@ -39,20 +41,13 @@ export const GamePage = () => {
if (!info.release) { if (!info.release) {
return ( return (
<div className={` <div>
min-height: 100vh;
`}>
NOT FOUND NOT FOUND
</div> </div>
) )
} }
return ( return (
<div className={css`
min-height: 100vh;
background-color: hsl(230, 10%, 10%);
color: white;
`}>
<div className={css` <div className={css`
margin: auto; margin: auto;
width: max-content; width: max-content;
@ -64,7 +59,7 @@ export const GamePage = () => {
`}> `}>
<div> <div>
<h1>{info.release.manifest.title ?? slug!.split("-").map(word => word[0].toUpperCase()+word.slice(1)).join(" ")}</h1> <h1>{info.release.manifest.title ?? slug!.split("-").map(word => word[0].toUpperCase()+word.slice(1)).join(" ")}</h1>
<h2>by {info.release.author}</h2> <h2>by <Link to={`/u/${info.release.author}`}>{info.release.author}</Link></h2>
</div> </div>
<div className={css` <div className={css`
width: 512px; width: 512px;
@ -83,7 +78,7 @@ export const GamePage = () => {
display: flex; display: flex;
justify-content: end; justify-content: end;
`}> `}>
Version: <select defaultValue={info.release.version} onChange={(ev) => navigate(`/u/${author}/${slug}/v${ev.target.value}`)}> Version: <select defaultValue={info.release.version} onChange={(ev) => setVersion(ev.target.value)}>
{ {
[...info.versions].reverse().map(v => ( [...info.versions].reverse().map(v => (
<option key={v} value={v}>{v}</option> <option key={v} value={v}>{v}</option>
@ -96,6 +91,5 @@ export const GamePage = () => {
<p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p> <p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p>
</div> */} </div> */}
</div> </div>
</div>
) )
} }

View File

@ -1,12 +1,20 @@
import { Outlet, RouterProvider, ScrollRestoration, createBrowserRouter, redirect } from "react-router-dom" import { Outlet, RouterProvider, ScrollRestoration, createBrowserRouter, redirect } from "react-router-dom"
import { HomePage } from "./HomePage"; import { HomePage } from "./HomePage";
import { GamePage } from "./GamePage"; import { GamePage } from "./GamePage";
import { AuthorPage } from "./AuthorPage";
import { css } from "@emotion/css";
const RouteRoot = () => { const RouteRoot = () => {
return <> return <>
{/* <Nav> */} {/* <Nav> */}
<div className={css`
min-height: 100vh;
background-color: hsl(230, 10%, 10%);
color: white;
`}>
<ScrollRestoration /> <ScrollRestoration />
<Outlet/> <Outlet/>
</div>
{/* </Nav> */} {/* </Nav> */}
</> </>
} }
@ -23,11 +31,11 @@ const router = createBrowserRouter([
// } // }
}, },
{ {
path: "/u/:author/:slug", path: "/u/:author",
element: <GamePage/>, element: <AuthorPage/>,
}, },
{ {
path: "/u/:author/:slug/:version", path: "/u/:author/:slug",
element: <GamePage/>, element: <GamePage/>,
}, },
], ],

View File

@ -0,0 +1,34 @@
import { Type } from "@sinclair/typebox";
import { FirRouteInput, FirRouteOptions } from "../util/routewrap.ts";
import { getAuthorGames, getReleases } from "../dbal/dbal.ts";
const method = "GET";
const url = "/api/author";
const payloadT = Type.Any();
const handler = async ({payload}: FirRouteInput<typeof payloadT>) => {
const {author} = payload;
if (typeof author !== "string") {
return {
author: null,
releases: [],
};
}
console.log("author", author);
const games = await getAuthorGames({author});
return {
author,
games,
}
};
export default {
method,
url,
payloadT,
handler,
} as const satisfies FirRouteOptions<typeof payloadT>;

View File

@ -38,12 +38,18 @@ const compareByVersion = (a: DbRelease, b: DbRelease) => compareVersions(a.versi
export const getReleases = async (where: { export const getReleases = async (where: {
author: string; author: string;
slug: string; slug?: string;
version?: string; version?: string;
}): Promise<DbRelease[]> => { }): Promise<DbRelease[]> => {
const {author, slug, version} = where; const {author, slug, version} = where;
let rows: DbReleaseInternal[]; let rows: DbReleaseInternal[];
if (!version) { if (!slug) {
rows = await db.query(sql`
SELECT * from releases
WHERE
author = ${author}
`);
} else if (!version) {
rows = await db.query(sql` rows = await db.query(sql`
SELECT * from releases SELECT * from releases
WHERE WHERE
@ -84,6 +90,25 @@ export const getRelease = async (where: {
} }
} }
export const getAuthorGames = async (where: {
author: string;
}) => {
const releases = await getReleases(where);
const games = releases.reduce((accum, curr) => {
const found = accum.find(r => r.slug === curr.slug);
if (found) {
found.releases.push(curr);
} else {
accum.push({slug: curr.slug, releases: [curr]});
}
return accum;
}, [] as {slug: string; releases: DbRelease[]}[]);
games.forEach(game => {
game.releases.sort(compareByVersion);
});
return games;
}
export const insertRelease = async (props: {manifest: PicobookManifest, carts: {name: string; rom: number[]}[]}) => { export const insertRelease = async (props: {manifest: PicobookManifest, carts: {name: string; rom: number[]}[]}) => {
const {manifest, carts} = props; const {manifest, carts} = props;
// console.log('carts', JSON.stringify(carts)); // console.log('carts', JSON.stringify(carts));

View File

@ -1,4 +1,5 @@
import echo from "./api/echo.ts"; import echo from "./api/echo.ts";
import getAuthor from "./api/getAuthor.ts";
import getRelease from "./api/getRelease.ts"; import getRelease from "./api/getRelease.ts";
import release from "./api/release.ts"; import release from "./api/release.ts";
import webhook from "./api/webhook.ts"; import webhook from "./api/webhook.ts";
@ -8,6 +9,7 @@ export const routeList = [
webhook, webhook,
release, release,
getRelease, getRelease,
getAuthor,
]; ];
export type RouteList = typeof routeList; export type RouteList = typeof routeList;