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": {
"@elysiajs/eden": "^1.3.2",
"@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3",
"solid-js": "^1.9.5",
"object-hash": "^3.0.0"
"js-cookie": "^3.0.5",
"object-hash": "^3.0.0",
"solid-js": "^1.9.5"
},
"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-plugin-solid": "^2.11.8"
}

View File

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

View File

@@ -1,32 +1,50 @@
import "./style.css";
import { Route, Router } from "@solidjs/router";
import { lazy, Suspense } from "solid-js";
import pkg from "../package.json";
import { createResource, lazy, Suspense } from "solid-js";
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 = () => (
<div class="full free clear">
<span
style={{
margin: "5px",
"font-size": "0.8rem",
"font-family": "monospace",
"pointer-events": "all",
}}
class="fixed-br"
>
v{pkg.version}
</span>
</div>
);
const Profile = () => {
let dialogRef!: HTMLDialogElement;
const [profile] = createResource(async () => api.profile.get());
return (
<>
<div
onClick={() => dialogRef.showModal()}
class="i-solar-user-circle-bold button s-10 m-2 cursor-pointer fixed tr"
/>
<dialog ref={dialogRef} closedby="any">
<div class="fixed tr bg-emerald-100 m-2 p-4 rounded-xl border-2 shadow-md shadow-black">
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 = () => (
<Router
root={(props) => (
<>
<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>
);
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}
draggable={false}
class={props.class}
style={{
"border-radius": "5px",
...props.style,
}}
style={props.style}
width="100px"
src={
props.face == "down"

View File

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

View File

@@ -3,29 +3,13 @@ import Card from "./Card";
import { Hand } from "../../../shared/cards";
import { GameContext } from "./Game";
import { produce } from "solid-js/store";
import { Stylable } from "./toolbox";
export default ((props) => {
const { submitAction, view } = useContext(GameContext)!;
return (
<div
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",
}}
>
<div class={"hand " + props.class} style={props.style}>
<For each={props.hand}>
{(card) => (
<Card
@@ -39,4 +23,4 @@ export default ((props) => {
</For>
</div>
);
}) satisfies Component<{ hand: Hand }>;
}) satisfies Component<{ hand: Hand } & Stylable>;

View File

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

View File

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

View File

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

View File

@@ -12,17 +12,30 @@ body {
body::before {
z-index: -1;
content: "";
font-size: 28px;
position: absolute;
width: 100%;
height: 100%;
}
a {
/* a {
color: rgb(18, 229, 113);
}
a:visited {
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 {
@@ -31,41 +44,19 @@ a:visited {
}
.hand {
height: 160px;
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 {
height: 100%;
width: 100%;
}
margin-bottom: 50px;
padding: 10px;
.w-full {
width: 100%;
}
.center {
overflow: scroll;
scrollbar-width: none;
display: flex;
justify-content: center;
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;
gap: 5px;
}

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 solidPlugin from "vite-plugin-solid";
import UnoCSS from "unocss/vite";
export default defineConfig({
plugins: [solidPlugin()],
plugins: [solidPlugin(), UnoCSS()],
build: {
outDir: "../server/public",
emptyOutDir: true,

View File

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

View File

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

View File

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

1152
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff