Compare commits

..

7 Commits

Author SHA1 Message Date
ef5a5e059a ""auth"" working nicely 2025-08-17 19:57:55 -04:00
c755b83d3d can change name 2025-08-16 00:15:07 -05:00
1c915d1713 some proper auth 2025-08-15 15:27:24 -05:00
4419dd7acc style cleanup 2025-08-12 19:42:30 -04:00
01d1571b0e works I guess? 2025-08-11 23:41:48 -04:00
bfb4fd3a63 Merge branch 'prod' into dev 2025-08-11 22:31:10 -04:00
a8a6d02cc7 build ready (I hope) 2025-08-10 18:46:45 -04:00
18 changed files with 997 additions and 565 deletions

View File

@@ -8,11 +8,18 @@
}, },
"dependencies": { "dependencies": {
"@elysiajs/eden": "^1.3.2", "@elysiajs/eden": "^1.3.2",
"@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",
"solid-js": "^1.9.5", "js-cookie": "^3.0.5",
"object-hash": "^3.0.0" "object-hash": "^3.0.0",
"solid-js": "^1.9.5"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/solar": "^1.2.4",
"@types/js-cookie": "^3.0.6",
"@unocss/preset-icons": "^66.4.2",
"@unocss/preset-wind4": "^66.4.2",
"unocss": "^66.4.2",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-solid": "^2.11.8" "vite-plugin-solid": "^2.11.8"
} }

View File

@@ -2,8 +2,6 @@ import { type Api } from "../../server/src/api";
import { treaty } from "@elysiajs/eden"; import { treaty } from "@elysiajs/eden";
const { api } = treaty<Api>("http://localhost:5001", { const { api } = treaty<Api>("http://localhost:5001", {
headers: { fetch: { credentials: "include" },
human: "daniel",
},
}); });
export default api; export default api;

View File

@@ -1,32 +1,50 @@
import "./style.css";
import { Route, Router } from "@solidjs/router"; import { Route, Router } from "@solidjs/router";
import { lazy, Suspense } from "solid-js"; import { createResource, lazy, Suspense } from "solid-js";
import pkg from "../package.json";
import { render } from "solid-js/web"; import { render } from "solid-js/web";
import Root from "./routes/index"; import "virtual:uno.css";
import pkg from "../package.json";
import "./style.css";
import api from "./api";
import Cookies from "js-cookie";
const Version = () => ( const Profile = () => {
<div class="full free clear"> let dialogRef!: HTMLDialogElement;
<span const [profile] = createResource(async () => api.profile.get());
style={{
margin: "5px", return (
"font-size": "0.8rem", <>
"font-family": "monospace", <div
"pointer-events": "all", onClick={() => dialogRef.showModal()}
}} class="i-solar-user-circle-bold button s-10 m-2 cursor-pointer fixed tr"
class="fixed-br" />
>
v{pkg.version} <dialog ref={dialogRef} closedby="any">
</span> <div class="fixed tr bg-emerald-100 m-2 p-4 rounded-xl border-2 shadow-md shadow-black">
</div> Name:{" "}
); <input
value={profile()?.data?.name ?? ""}
onChange={(e) =>
api.setName.post({ name: e.target.value })
}
class="bg-emerald-200 border-1.5 rounded-full px-4"
/>
</div>
</dialog>
</>
);
};
const App = () => ( const App = () => (
<Router <Router
root={(props) => ( root={(props) => (
<> <>
<Suspense>{props.children}</Suspense> <Suspense>{props.children}</Suspense>
<Version /> <Profile />
{/* Version */}
<span class="fixed br m-2 font-mono text-xs">
{"v" + pkg.version}
</span>
</> </>
)} )}
> >
@@ -42,4 +60,7 @@ const App = () => (
</Router> </Router>
); );
render(App, document.getElementById("app")!); // todo: fix this
(Cookies.get("token") == null ? api.whoami.post() : Promise.resolve()).then(
() => render(App, document.getElementById("app")!)
);

View File

@@ -24,10 +24,7 @@ export default ((props) => {
onClick={props.onClick} onClick={props.onClick}
draggable={false} draggable={false}
class={props.class} class={props.class}
style={{ style={props.style}
"border-radius": "5px",
...props.style,
}}
width="100px" width="100px"
src={ src={
props.face == "down" props.face == "down"

View File

@@ -30,19 +30,13 @@ export default (props: { instanceId: string }) => {
return ( return (
<GameContext.Provider value={{ view, submitAction }}> <GameContext.Provider value={{ view, submitAction }}>
<Show when={view.latest != undefined}> <Show when={view.latest != undefined}>
<div <Pile
class="full column center" count={view.latest!.deckCount}
style={{ "row-gap": "20px", "font-size": "32px" }} class="cursor-pointer fixed center"
> onClick={() => submitAction({ type: "draw" })}
<div class="full center"> />
<Pile
count={view.latest!.deckCount} <Hand class="fixed bc" hand={view.latest!.myHand} />
style={{ cursor: "pointer" }}
onClick={() => submitAction({ type: "draw" })}
/>
</div>
<Hand hand={view.latest!.myHand} />
</div>
</Show> </Show>
</GameContext.Provider> </GameContext.Provider>
); );

View File

@@ -3,29 +3,13 @@ import Card from "./Card";
import { Hand } from "../../../shared/cards"; import { Hand } from "../../../shared/cards";
import { GameContext } from "./Game"; import { GameContext } from "./Game";
import { produce } from "solid-js/store"; import { produce } from "solid-js/store";
import { Stylable } from "./toolbox";
export default ((props) => { export default ((props) => {
const { submitAction, view } = useContext(GameContext)!; const { submitAction, view } = useContext(GameContext)!;
return ( return (
<div <div class={"hand " + props.class} style={props.style}>
class="hand"
style={{
"min-width": "100px",
width: "fit-content",
"max-width": "80%",
border: "2px dashed white",
"border-radius": "12px",
margin: "10px",
"margin-bottom": "25px",
padding: "10px",
height: "180px",
overflow: "scroll",
"scrollbar-width": "none",
display: "flex",
gap: "5px",
}}
>
<For each={props.hand}> <For each={props.hand}>
{(card) => ( {(card) => (
<Card <Card
@@ -39,4 +23,4 @@ export default ((props) => {
</For> </For>
</div> </div>
); );
}) satisfies Component<{ hand: Hand }>; }) satisfies Component<{ hand: Hand } & Stylable>;

View File

@@ -5,20 +5,14 @@ import { Clickable, Stylable } from "./toolbox";
export default ((props) => { export default ((props) => {
return ( return (
<div <Show when={props.count > 0}>
{...props} <Card
class={`center ${props.class ?? ""}}`.trim()} onClick={props.onClick}
style={{ style={props.style}
width: "200px", class={props.class + " shadow-lg shadow-black"}
height: "400px", face="down"
...(props.style as JSX.CSSProperties), />
}} </Show>
onClick={props.onClick}
>
<Show when={props.count > 0}>
<Card face="down" />
</Show>
</div>
); );
}) satisfies Component< }) satisfies Component<
{ {

View File

@@ -8,19 +8,7 @@ export default () => {
return ( return (
<> <>
<Game instanceId={params.instance} /> <Game instanceId={params.instance} />
<A <A href={`/${params.game}`} class="fixed tl m-4 px-2 py-1.5 button">
href={`/${params.game}`}
style={{
position: "absolute",
padding: "10px",
top: "0",
left: "0",
margin: "20px",
"background-color": "white",
"border-radius": "8px",
border: "2px solid black",
}}
>
Back Back
</A> </A>
</> </>

View File

@@ -14,15 +14,10 @@ export default () => {
return ( return (
<Suspense> <Suspense>
<div style={{ padding: "20px" }}> <div style={{ padding: "20px" }}>
<h1 style={{ margin: 0 }}>{param.game}</h1> <p class="text-[40px]">{param.game}</p>
<button <button
onClick={() => class="px-2 py-1.5 m-4 button rounded"
api.simple.newGame onClick={() => api.simple.newGame.post().then(refetch)}
.post({
players: ["daniel"],
})
.then(refetch)
}
> >
New Game New Game
</button> </button>

View File

@@ -12,17 +12,30 @@ body {
body::before { body::before {
z-index: -1; z-index: -1;
content: ""; content: "";
font-size: 28px;
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
a { /* a {
color: rgb(18, 229, 113); color: rgb(18, 229, 113);
} }
a:visited { a:visited {
color: rgb(23, 138, 125); color: rgb(23, 138, 125);
} */
.button {
cursor: pointer;
background-color: white;
color: black;
box-shadow: 0px 5px 10px black;
border-radius: 10%;
transition: background-color 0.15s, color 0.15s, transform 0.15s;
}
.button:hover {
background-color: rgb(23, 138, 125);
color: white;
transform: scale(1.1);
} }
#app { #app {
@@ -31,41 +44,19 @@ a:visited {
} }
.hand { .hand {
height: 160px;
background: radial-gradient(rgb(24, 70, 82), rgb(1, 42, 41)); background: radial-gradient(rgb(24, 70, 82), rgb(1, 42, 41));
} min-width: 100px;
width: fit-content;
max-width: 90%;
border: 2px dashed white;
border-radius: 12px;
.full { margin-bottom: 50px;
height: 100%; padding: 10px;
width: 100%;
}
.w-full { overflow: scroll;
width: 100%; scrollbar-width: none;
}
.center {
display: flex; display: flex;
justify-content: center; gap: 5px;
align-items: center;
}
.column {
display: flex;
flex-direction: column;
}
.free {
position: absolute;
top: 0;
left: 0;
}
.fixed-br {
position: fixed;
bottom: 0;
right: 0;
}
.clear {
pointer-events: none;
} }

View File

@@ -0,0 +1,40 @@
import presetWind4 from "@unocss/preset-wind4";
import { defineConfig } from "unocss";
import { presetIcons } from "unocss";
import {} from "@iconify-json/solar";
export default defineConfig({
presets: [
presetWind4(),
presetIcons({
collections: {
solar: () =>
import("@iconify-json/solar/icons.json").then(
(i) => i.default as any
),
},
}),
],
shortcuts: [[/^s-(\d+)$/, ([, d]) => `w-${d} h-${d}`]],
rules: [
["tl", { top: 0, left: 0 }],
["tr", { top: 0, right: 0 }],
["bl", { bottom: 0, left: 0 }],
["br", { bottom: 0, right: 0 }],
[
"bc",
{
bottom: 0,
left: 0,
right: 0,
"margin-left": "auto",
"margin-right": "auto",
},
],
[
"center",
{ top: "50%", left: "50%", transform: "translate(-50%, -50%)" },
],
],
});

View File

@@ -1,8 +1,9 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid"; import solidPlugin from "vite-plugin-solid";
import UnoCSS from "unocss/vite";
export default defineConfig({ export default defineConfig({
plugins: [solidPlugin()], plugins: [solidPlugin(), UnoCSS()],
build: { build: {
outDir: "../server/public", outDir: "../server/public",
emptyOutDir: true, emptyOutDir: true,

View File

@@ -17,8 +17,8 @@ model Game {
} }
model Human { model Human {
key String @id key String @id @default(cuid(2))
name String name String @default("")
Instance Instance[] Instance Instance[]
} }

View File

@@ -2,21 +2,38 @@ import { prisma } from "./db/db";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { simpleApi } from "./games/simple"; import { simpleApi } from "./games/simple";
import { human } from "./human";
const api = new Elysia({ prefix: "/api" }) const api = new Elysia({ prefix: "/api" })
// [wip] .post("/whoami", async ({ cookie: { token } }) => {
.group("/prisma", (app) => if (token.value == null) {
app const newHuman = await prisma.human.create({
.post("/game", ({ body }: { body: Prisma.GameFindManyArgs }) => data: {},
prisma.game.findMany(body) });
) token.value = newHuman.key;
.post( }
"/instance", })
({ body }: { body: Prisma.InstanceFindManyArgs }) => .use(human)
prisma.instance.findMany(body) .post(
) "/setName",
({ body: { name }, humanKey }) =>
prisma.human.update({
where: {
key: humanKey,
},
data: {
name,
},
}),
{
body: t.Object({
name: t.String(),
}),
}
)
.get("/profile", ({ humanKey }) =>
prisma.human.findFirst({ where: { key: humanKey } })
) )
.get("/games", () => prisma.game.findMany()) .get("/games", () => prisma.game.findMany())
.get("/instances", ({ query: { game } }) => .get("/instances", ({ query: { game } }) =>
@@ -31,6 +48,7 @@ const api = new Elysia({ prefix: "/api" })
}, },
}) })
) )
.use(simpleApi); .use(simpleApi);
export default api; export default api;

View File

@@ -9,6 +9,7 @@ import {
import { heq } from "@games/shared/utils"; import { heq } from "@games/shared/utils";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { prisma } from "../db/db"; import { prisma } from "../db/db";
import { human } from "../human";
// omniscient game state // omniscient game state
export type GameState = { export type GameState = {
@@ -111,47 +112,38 @@ export const resolveAction = (
}; };
export const simpleApi = new Elysia({ prefix: "/simple" }) export const simpleApi = new Elysia({ prefix: "/simple" })
// .guard({ headers: t.Object({ Human: t.String() }) }) .use(human)
.post( .post("/newGame", ({ humanKey }) => {
"/newGame", return prisma.instance.create({
(args: { body: { players: string[] }; headers: { human: string } }) => { data: {
return prisma.instance.create({ gameState: newGame([humanKey]),
data: { gameKey: "simple",
gameState: newGame(args.body.players), createdByKey: humanKey,
gameKey: "simple", },
createdByKey: args.headers.human, });
}, })
});
}
)
.group("/:instanceId", (app) => .group("/:instanceId", (app) =>
app app
.get( .get("/", ({ params: { instanceId }, humanKey }) =>
"/", prisma.instance
({ params: { instanceId }, headers: { human: humanId } }) => .findUnique({
prisma.instance where: {
.findUnique({ id: instanceId,
where: { },
id: instanceId, })
}, .then((game) =>
}) getView(
.then((game) => getKnowledge(
getView( game!.gameState as GameState,
getKnowledge( humanKey
game!.gameState as GameState, ),
humanId! humanKey
),
humanId!
)
) )
)
) )
.post( .post(
"/", "/",
({ ({ params: { instanceId }, body: { action }, humanKey }) =>
params: { instanceId },
body: { action },
headers: { human: humanId },
}) =>
prisma.instance prisma.instance
.findUniqueOrThrow({ .findUniqueOrThrow({
where: { where: {
@@ -161,7 +153,7 @@ export const simpleApi = new Elysia({ prefix: "/simple" })
.then(async (game) => { .then(async (game) => {
const newState = resolveAction( const newState = resolveAction(
game.gameState as GameState, game.gameState as GameState,
humanId!, humanKey,
action action
); );
await prisma.instance.update({ await prisma.instance.update({
@@ -171,8 +163,8 @@ export const simpleApi = new Elysia({ prefix: "/simple" })
}, },
}); });
return getView( return getView(
getKnowledge(newState, humanId!), getKnowledge(newState, humanKey),
humanId! humanKey
); );
}), }),
{ {

View File

@@ -0,0 +1,8 @@
import Elysia from "elysia";
export const human = new Elysia({ name: "human" })
.derive(async ({ cookie: { token }, status }) => {
const humanKey = token.value;
return humanKey != null ? { humanKey } : status(401);
})
.as("scoped");

View File

@@ -6,15 +6,15 @@ import { staticPlugin } from "@elysiajs/static";
const port = env.PORT || 5001; const port = env.PORT || 5001;
const app = new Elysia() const app = new Elysia()
.use(cors()) .use(
cors({
origin: "http://localhost:3000",
credentials: true,
})
)
.onRequest(({ request }) => { .onRequest(({ request }) => {
console.log(request.method, request.url); console.log(request.method, request.url);
}) })
.onError(({ code, error, body, headers }) => {
console.error("headers", JSON.stringify(headers, null, 2));
console.error("body", JSON.stringify(body, null, 2));
console.error(code, error);
})
.get("/ping", () => "pong") .get("/ping", () => "pong")
.use(api) .use(api)
.get("/*", () => Bun.file("./public/index.html")) .get("/*", () => Bun.file("./public/index.html"))

1152
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff