add author page!
This commit is contained in:
parent
2b614dcb79
commit
11783ba2fb
62
src/client/AuthorPage.tsx
Normal file
62
src/client/AuthorPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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/>,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
34
src/server/api/getAuthor.ts
Normal file
34
src/server/api/getAuthor.ts
Normal 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>;
|
@ -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));
|
||||||
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user