Compare commits
7 Commits
e12def2e10
...
ef5a5e059a
| Author | SHA1 | Date | |
|---|---|---|---|
| ef5a5e059a | |||
| c755b83d3d | |||
| 1c915d1713 | |||
| 4419dd7acc | |||
| 01d1571b0e | |||
| bfb4fd3a63 | |||
| a8a6d02cc7 |
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")!)
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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<
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
40
packages/client/uno.config.ts
Normal file
40
packages/client/uno.config.ts
Normal 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%)" },
|
||||
],
|
||||
],
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -17,8 +17,8 @@ model Game {
|
||||
}
|
||||
|
||||
model Human {
|
||||
key String @id
|
||||
name String
|
||||
key String @id @default(cuid(2))
|
||||
name String @default("")
|
||||
Instance Instance[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}),
|
||||
{
|
||||
|
||||
8
packages/server/src/human.ts
Normal file
8
packages/server/src/human.ts
Normal 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");
|
||||
@@ -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
1152
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user