From 8319de0812d3853252b7ae3f138c0301ea6a50cd Mon Sep 17 00:00:00 2001 From: MasterGordon Date: Mon, 23 Sep 2024 00:58:36 +0200 Subject: [PATCH] first layout draft --- backend/controller/controller.ts | 6 - backend/controller/gameController.ts | 2 +- backend/index.ts | 9 +- backend/router.ts | 37 +++-- bun.lockb | Bin 102117 -> 121367 bytes dev.ts | 3 + eslint.config.js | 32 ++-- index.ts | 1 - package.json | 13 +- sqlite.db | Bin 28672 -> 28672 bytes src/Shell.tsx | 98 ++++++++++++ src/atoms.ts | 3 + src/components/Button.tsx | 41 +++++ src/components/Hr.tsx | 5 + src/components/NavLink.tsx | 22 +++ src/hooks.ts | 63 ++++++++ src/index.css | 226 +++++++++++++-------------- src/lib/utils.ts | 6 + src/main.tsx | 11 +- src/wsClient.ts | 74 +++++++++ tailwind.config.js | 0 vite.config.ts | 9 +- 22 files changed, 506 insertions(+), 155 deletions(-) create mode 100644 dev.ts delete mode 100644 index.ts create mode 100644 src/Shell.tsx create mode 100644 src/atoms.ts create mode 100644 src/components/Button.tsx create mode 100644 src/components/Hr.tsx create mode 100644 src/components/NavLink.tsx create mode 100644 src/hooks.ts create mode 100644 src/lib/utils.ts create mode 100644 src/wsClient.ts create mode 100644 tailwind.config.js diff --git a/backend/controller/controller.ts b/backend/controller/controller.ts index d1a70e2..07daf42 100644 --- a/backend/controller/controller.ts +++ b/backend/controller/controller.ts @@ -12,12 +12,6 @@ export type Endpoint = { handler: (input: TInput, context: RequestContext) => Promise; }; -export type Request> = { - method: "POST"; - url: string; - body: z.infer; -}; - export const createEndpoint = ( validate: z.ZodType, handler: (input: TInput, context: RequestContext) => Promise, diff --git a/backend/controller/gameController.ts b/backend/controller/gameController.ts index 484a560..e12b83b 100644 --- a/backend/controller/gameController.ts +++ b/backend/controller/gameController.ts @@ -19,7 +19,7 @@ export const gameController = createController({ if (game.finished) return gameState; return serverToClientGame(gameState); }), - createGame: createEndpoint(z.undefined(), async (_, { user, db }) => { + createGame: createEndpoint(z.null(), async (_, { user, db }) => { if (!user) throw new UnauthorizedError("Unauthorized"); const uuid = crypto.randomUUID() as string; const newGame: ServerGame = game.createGame({ diff --git a/backend/index.ts b/backend/index.ts index f5872d6..9560b57 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -1,4 +1,4 @@ -import type { ServerWebSocket } from "bun"; +import { handleRequest } from "./router"; const allowCors = { "Access-Control-Allow-Origin": "*", @@ -6,7 +6,6 @@ const allowCors = { "Access-Control-Allow-Headers": "Content-Type", }; -const userName = new WeakMap, string>(); const server = Bun.serve({ async fetch(request: Request) { if (request.method === "OPTIONS") { @@ -22,10 +21,10 @@ const server = Bun.serve({ if (typeof message !== "string") { return; } - const user = userName.get(ws); try { const msg = JSON.parse(message); - console.log(msg); + console.log("Received message", msg); + handleRequest(msg, ws); } catch (e) { console.error("Faulty request", message, e); return; @@ -37,3 +36,5 @@ const server = Bun.serve({ }, port: 8076, }); + +console.log("Listening on port 8076"); diff --git a/backend/router.ts b/backend/router.ts index 8f6dff3..b251e89 100644 --- a/backend/router.ts +++ b/backend/router.ts @@ -1,37 +1,52 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ServerWebSocket } from "bun"; import type { Controller, Endpoint } from "./controller/controller"; import { gameController } from "./controller/gameController"; import { db } from "./database/db"; -import { BadRequestError } from "./errors/BadRequestError"; const controllers = { game: gameController, } satisfies Record>; -export const handleRequest = (message: unknown, sessionUser?: string) => { +const userName = new WeakMap, string>(); + +export const setSessionUser = (ws: ServerWebSocket, user: string) => { + userName.set(ws, user); +}; + +export const handleRequest = async ( + message: unknown, + ws: ServerWebSocket, +) => { + // TODO: Remove this + const sessionUser = userName.get(ws) || "Gordon"; const ctx = { user: sessionUser, db, + ws, }; if ( !message || !(typeof message === "object") || !("type" in message) || - !("payload" in message) + !("payload" in message) || + !("id" in message) ) return; - const { type, payload } = message; + const { type, payload, id } = message; if (!(typeof type === "string")) return; const [controllerName, action] = type.split("."); if (!(controllerName in controllers)) return; - // @ts-expect-error controllers[controllerName] is a Controller - const endpoint = controllers[controllerName][action] as Endpoint; - const input = endpoint.validate.safeParse(payload); - if (input.success) { - const result = endpoint.handler(input.data, ctx); - return result; + try { + // @ts-expect-error controllers[controllerName] is a Controller + const endpoint = controllers[controllerName][action] as Endpoint; + const input = endpoint.validate.parse(payload); + const result = await endpoint.handler(input, ctx); + ws.send(JSON.stringify({ id, payload: result })); + return; + } catch (_) { + ws.send(JSON.stringify({ id, error: "Bad Request" })); } - throw new BadRequestError(input.error.message); }; export type Routes = typeof controllers; diff --git a/bun.lockb b/bun.lockb index 7a664dbc27735a0ee1b9b5f94d1f6aeb29284514..f2ecc908d963512428bc8fc203e76d87a1d1a985 100755 GIT binary patch delta 30604 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 M?%&;+YY_7P0m75km;e9( delta 18199 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 QM9dTkwRU{BQZ$eMAFE_soB#j- diff --git a/dev.ts b/dev.ts new file mode 100644 index 0000000..afda337 --- /dev/null +++ b/dev.ts @@ -0,0 +1,3 @@ +import { $ } from "bun"; + +await Promise.all([$`bun run dev:backend`, $`bun run dev:client`]); diff --git a/eslint.config.js b/eslint.config.js index 092408a..a61a109 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,28 +1,36 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], }, }, -) +); diff --git a/index.ts b/index.ts deleted file mode 100644 index f67b2c6..0000000 --- a/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json index c461e0b..0f628f6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "bun run dev.ts", + "dev:backend": "bun run backend/index.ts --watch", + "dev:client": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", @@ -12,14 +14,23 @@ "drizzle:migrate": "bun run backend/migrate.ts" }, "dependencies": { + "@tailwindcss/vite": "^4.0.0-alpha.24", + "@tanstack/react-query": "^5.56.2", "@uidotdev/usehooks": "^2.4.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "drizzle-orm": "^0.33.0", + "framer-motion": "^11.5.6", + "jotai": "^2.10.0", "lucide-react": "^0.441.0", "react": "^18.3.1", "react-confetti-boom": "^1.0.0", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", + "tailwind-merge": "^2.5.2", + "tailwindcss": "^4.0.0-alpha.24", "use-sound": "^4.0.3", + "wouter": "^3.3.5", "zod": "^3.23.8", "zustand": "^4.5.5" }, diff --git a/sqlite.db b/sqlite.db index e548cfad181ba81985d800fbaf1cd86b4224af1b..06eaec2d84faeda9c139b306981c5497622028fa 100644 GIT binary patch literal 28672 zcmeI3zi-<{6vs(ZKb@#-VXJ~Sg6LoYVnI?QMcN1u|DX*ws-4O<(9|$wiabhe^dl9i zB#4p0g@?>RJ9g^Wp+lz*9U64&mZ^V1fo{zbw19#3jxtl(mTkGHT@R12_~YF>-s9)K zxHIpL>mQZ90C8@o+p+?#NWD$bG-%guw#r>A8AI}iW@KmZ5;0U!VbfB+Bx0zlw02sGo#TrNlNl>)2Q zL_HsM{cx99sF-sbCbuy+UpBd&(6+-(kEeIqRtxQL0eT#8cPlF&&Qe>dk zx>+t4u*(C>_aAk-&Y@eBG@j2V61iJBn)2EXdhCDJ#IIdds~2>_>uOj?RSFNu&u=qf zWc$@HvN|r+4|lPnk%hK?WWAmj6?s}@>M_WTCIg+{51=F;3|d4m&=_yrygKCoI3{&5^AVQH;mt}L&H6sJSdJ9(~RE}0c` zb-`SZ;7|9-=ef02Zu9OUshoC)v+Tg@w25ueESrNiOgnZK?GRj53J>wUJe|y`D!tbW zX;-UG*L(7$iK;EH-nB>({xI9vk-FfAx$s0Zc=H@?-K?z4l?PRW`Qoi%`HjAy2B=`W z-9@-coGN}PZ}(cL>)FITzdV-6ZKj~-vQty#qNbHpO&1-649P0G zhA7#NW=pn)R7uoS%aAm;W@~DxXzBFx8!x_oZyY}t&EmQFgX{Bv5D5?f0zd!=00AHX z1b_e#00KY&2mk>f@ZS+&7%ENX|C5OyDfTz^CHsA5_r|T%U-%L>AOHk_01yBI*PFop zY%;Yu^=u-YisP|w7umKZ*sfC(6kXN@T`3xZB^$2n=$edNX&Eog?6h}zvR;y(;YZ2B z7kILC5_|VEJN0r;oqcpN&WAkxICwk!&m%JSQh8N)ZV)(a{7vOQ%}0<>;l7eoQ^1WVxt% zq#|yFisz~4gbL1(Pt-u1qqyVA+tBu$)%HxjSo?W@HWsNzDH$TNG)<6HTf{vVvISjt zW!wtcu8#W?$2ie*jgsYJ>XAw+?!AnE(Q}QckrT3e(_Vpk$cGjq_gs@}AAFL-pNLGo zL#M_hGVOoZv^<>_jqF%5ixbi`PEaEIySrIBIYG?CKe95hmt_7w$<9;kpX~4Kuk07> z59~a9|9bz+2hjlmAOHk_01yBIKmZ5;0U!VbfB+Df00e*l5C8%|00;m9AOHk_ Q01)_}2&9>*bbk!sZz*_ag8%>k delta 64 zcmZp8z}WDBae_1>%S0JxRu%@mvhIy33-q~|`FAq#zvREfzjL#o!fO7>JMF!JV$T@( TfAW9ee+Cr0!N2*Lzmx(1vLqH% diff --git a/src/Shell.tsx b/src/Shell.tsx new file mode 100644 index 0000000..2d909b2 --- /dev/null +++ b/src/Shell.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { Button } from "./components/Button"; +import { motion } from "framer-motion"; +import { + GitBranch, + Github, + History, + LayoutDashboard, + Menu, + Play, + Settings, + Settings2, +} from "lucide-react"; +import Hr from "./components/Hr"; +import NavLink from "./components/NavLink"; + +const drawerWidth = 256; +const drawerWidthWithPadding = drawerWidth; + +const Shell: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + + const x = isOpen ? 0 : -drawerWidthWithPadding; + const width = isOpen ? drawerWidthWithPadding : 0; + useEffect(() => { + const onResize = () => { + setIsOpen(window.innerWidth > 768); + }; + window.addEventListener("resize", onResize); + onResize(); + return () => window.removeEventListener("resize", onResize); + }, []); + + return ( +
+ +
+

+ Minesweeper +
+ Business +

+
+ + + Dashboard + + + + Play + + + + History + + + + Settings + +
+
+ + + Source + +
+
+ +
+ + + + +
+
+
+
+
+
+
+ ); +}; + +export default Shell; diff --git a/src/atoms.ts b/src/atoms.ts new file mode 100644 index 0000000..7180609 --- /dev/null +++ b/src/atoms.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const gameId = atom(undefined); diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..6934afb --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,41 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import { forwardRef } from "react"; +import { cn } from "../lib/utils"; + +const buttonVariants = cva("font-semibold py-2 px-4 rounded-md", { + variants: { + variant: { + default: "bg-primary text-white/95 hover:bg-primary/90", + ghost: "bg-transparent text-white/95 hover:bg-white/05", + }, + size: { + default: "h-10 py-2 px-4", + sm: "h-9 px-3 rounded-md", + lg: "h-11 px-8 rounded-md", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, +}); + +export type ButtonProps = React.ButtonHTMLAttributes & + VariantProps & { + as?: React.FC; + }; + +export const Button = forwardRef( + ( + { className, variant, size, as: Comp = "button" as const, ...props }, + ref, + ) => { + return ( + + ); + }, +); diff --git a/src/components/Hr.tsx b/src/components/Hr.tsx new file mode 100644 index 0000000..126ab4a --- /dev/null +++ b/src/components/Hr.tsx @@ -0,0 +1,5 @@ +const Hr = () => { + return
; +}; + +export default Hr; diff --git a/src/components/NavLink.tsx b/src/components/NavLink.tsx new file mode 100644 index 0000000..94461d5 --- /dev/null +++ b/src/components/NavLink.tsx @@ -0,0 +1,22 @@ +import { Link } from "wouter"; + +interface NavLinkProps { + href: string; + children: React.ReactNode; + external?: boolean; +} + +const NavLink: React.FC = ({ href, children, external }) => { + const Comp = external ? "a" : Link; + return ( + + {children} + + ); +}; + +export default NavLink; diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000..83df5fe --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,63 @@ +import { + useMutation, + useQuery, + useQueryClient, + UseQueryResult, +} from "@tanstack/react-query"; +import { Routes } from "../backend/router"; +import { wsClient } from "./wsClient"; + +export const useWSQuery = < + TController extends keyof Routes, + TAction extends keyof Routes[TController] & string, +>( + action: `${TController}.${TAction}`, + // @ts-expect-error We dont care since this is internal api + payload: Routes[TController][TAction]["validate"]["_input"], +): UseQueryResult< + // @ts-expect-error We dont care since this is internal api + Awaited> +> => { + return useQuery({ + queryKey: [action, payload], + queryFn: async () => { + const result = await wsClient.dispatch(action, payload); + return result; + }, + }); +}; + +export const useWSMutation = < + TController extends keyof Routes, + TAction extends keyof Routes[TController] & string, +>( + action: `${TController}.${TAction}`, + onSuccess?: ( + data: Awaited< + // @ts-expect-error We dont care since this is internal api + ReturnType + >, + ) => void, +) => { + return useMutation({ + // @ts-expect-error We dont care since this is internal api + mutationFn: async ( + // @ts-expect-error We dont care since this is internal api + payload: Routes[TController][TAction]["validate"]["_input"], + ) => { + const result = await wsClient.dispatch(action, payload); + return result; + }, + onSuccess, + }); +}; + +export const useWSInvalidation = < + TController extends keyof Routes, + TAction extends keyof Routes[TController] & string, +>() => { + const queryClient = useQueryClient(); + return (action: `${TController}.${TAction}`) => { + queryClient.invalidateQueries({ queryKey: [action] }); + }; +}; diff --git a/src/index.css b/src/index.css index 1ad06f6..b54dbc4 100644 --- a/src/index.css +++ b/src/index.css @@ -1,116 +1,116 @@ -.game-board { - display: grid; - gap: 2px; - max-width: fit-content; +@import "tailwindcss"; + +@theme { + --color-primary: hotpink; + --bg-brand: -webkit-linear-gradient(225deg, rgb(251, 175, 21), rgb(251, 21, 242), + rgb(21, 198, 251)) 0% 0% / 100% 300%; } -.game-wrapper { - display: flex; - flex-direction: column; - align-items: center; -} - -.mine-button { - background-color: #666; - border: 1px solid black; - width: 2rem; - height: 2rem; - font-size: 1.25rem; - user-select: none; - display: flex; - justify-content: center; - align-items: center; - font-weight: bold; - font-family: monospace; - box-sizing: border-box; - transition: all 0.2s ease-in-out; -} - -html { - background: #111; - color: #eee; -} - -body { - margin: auto; - max-width: 1400px; - padding: 1rem; - font-family: monospace; -} - -.timer { - flex-grow: 1; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 2rem; - font-family: monospace; -} - -.footer { - display: flex; - flex-direction: column; - /* justify-content: space-between; */ - align-items: center; - font-size: 1rem; - font-family: monospace; -} - -pre { - margin: 0; -} - -.stage { - font-size: 1rem; - font-family: monospace; -} - -input { - font-size: 14px; - margin: 12px; - padding: 6px 12px 6px 12px; - border-radius: 0.7em; - background: #333; - color: #eee; - border: 1px solid rgb(251, 21, 242); - -} - -button { - color: white; - font-weight: 600; - font-size: 14px; - margin: 12px; - padding: 6px 12px 6px 12px; - border-radius: 0.7em; - background: -webkit-linear-gradient(225deg, rgb(251, 175, 21), rgb(251, 21, 242), - rgb(21, 198, 251)) 0% 0% / 300% 300%; - background-size: 200% auto; -} - -button:hover { - animation: gradient_move 1s ease infinite; -} - -@keyframes gradient_move { - 0%{background-position: 0% 92%} - 50%{background-position: 100% 9%} - 100%{background-position: 0% 92%} -} - -/* .scores { */ -/* position: fixed; */ -/* top: 0; */ -/* left: 0; */ -/* padding: 1rem; */ +/* .game-board { */ +/* display: grid; */ +/* gap: 2px; */ +/* max-width: fit-content; */ +/* } */ +/**/ +/* .game-wrapper { */ +/* display: flex; */ +/* flex-direction: column; */ +/* align-items: center; */ +/* } */ +/**/ +/* .mine-button { */ +/* background-color: #666; */ +/* border: 1px solid black; */ +/* width: 2rem; */ +/* height: 2rem; */ +/* font-size: 1.25rem; */ +/* user-select: none; */ +/* display: flex; */ +/* justify-content: center; */ +/* align-items: center; */ +/* font-weight: bold; */ +/* font-family: monospace; */ +/* box-sizing: border-box; */ +/* transition: all 0.2s ease-in-out; */ +/* } */ +/**/ +/* html { */ +/* background: #111; */ +/* color: #eee; */ +/* } */ +/**/ +/* body { */ +/* margin: auto; */ +/* max-width: 1400px; */ +/* padding: 1rem; */ +/* font-family: monospace; */ +/* } */ +/**/ +/* .timer { */ +/* flex-grow: 1; */ +/* display: flex; */ +/* justify-content: space-between; */ +/* align-items: center; */ +/* font-size: 2rem; */ +/* font-family: monospace; */ +/* } */ +/**/ +/* .footer { */ +/* display: flex; */ +/* flex-direction: column; */ +/* align-items: center; */ +/* font-size: 1rem; */ +/* font-family: monospace; */ +/* } */ +/**/ +/* pre { */ +/* margin: 0; */ +/* } */ +/**/ +/* .stage { */ +/* font-size: 1rem; */ +/* font-family: monospace; */ +/* } */ +/**/ +/* input { */ +/* font-size: 14px; */ +/* margin: 12px; */ +/* padding: 6px 12px 6px 12px; */ +/* border-radius: 0.7em; */ +/* background: #333; */ +/* color: #eee; */ +/* border: 1px solid rgb(251, 21, 242); */ +/**/ +/* } */ +/**/ +/* button { */ +/* color: white; */ +/* font-weight: 600; */ +/* font-size: 14px; */ +/* margin: 12px; */ +/* padding: 6px 12px 6px 12px; */ +/* border-radius: 0.7em; */ +/* background: -webkit-linear-gradient(225deg, rgb(251, 175, 21), rgb(251, 21, 242), */ +/* rgb(21, 198, 251)) 0% 0% / 300% 300%; */ +/* background-size: 200% auto; */ +/* } */ +/**/ +/* button:hover { */ +/* animation: gradient_move 1s ease infinite; */ +/* } */ +/**/ +/* @keyframes gradient_move { */ +/* 0%{background-position: 0% 92%} */ +/* 50%{background-position: 100% 9%} */ +/* 100%{background-position: 0% 92%} */ +/* } */ +/**/ +/* .header { */ +/* display: grid; */ +/* grid-template-columns: 1fr 1fr; */ +/* margin-bottom: 1rem; */ +/* } */ +/**/ +/* .scores { */ +/* text-align: right; */ /* } */ - -.header { - display: grid; - grid-template-columns: 1fr 1fr; - margin-bottom: 1rem; -} - -.scores { - text-align: right; -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/main.tsx b/src/main.tsx index 99a4fdc..7555605 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,8 @@ import App from "./App.tsx"; import "./index.css"; import { connectWS } from "./ws.ts"; import { Toaster } from "react-hot-toast"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import Shell from "./Shell.tsx"; document.addEventListener("contextmenu", (event) => { event.preventDefault(); @@ -11,9 +13,14 @@ document.addEventListener("contextmenu", (event) => { connectWS(); +const queryClient = new QueryClient(); + createRoot(document.getElementById("root")!).render( - - + + + + {/* */} + , ); diff --git a/src/wsClient.ts b/src/wsClient.ts new file mode 100644 index 0000000..0c04087 --- /dev/null +++ b/src/wsClient.ts @@ -0,0 +1,74 @@ +import type { Routes } from "../backend/router"; + +const connectionString = import.meta.env.DEV + ? "ws://localhost:8076/ws" + : "wss://mb.gordon.business/ws"; + +const messageListeners = new Set<(event: MessageEvent) => void>(); +const addMessageListener = (listener: (event: MessageEvent) => void) => { + messageListeners.add(listener); +}; +const removeMessageListener = (listener: (event: MessageEvent) => void) => { + messageListeners.delete(listener); +}; + +const emitMessage = (event: MessageEvent) => { + messageListeners.forEach((listener) => listener(event)); +}; + +const createWSClient = () => { + const ws = new WebSocket(connectionString); + ws.onmessage = emitMessage; + addMessageListener((event: MessageEvent) => { + const data = JSON.parse(event.data); + console.log(data); + }); + + const dispatch = async < + TController extends keyof Routes, + TAction extends keyof Routes[TController] & string, + >( + action: `${TController}.${TAction}`, + // @ts-expect-error We dont care since this is internal api + payload: Routes[TController][TAction]["validate"]["_input"], + // @ts-expect-error We dont care since this is internal api + ): Promise>> => { + if (ws.readyState !== WebSocket.OPEN) { + await new Promise((res) => { + ws.onopen = () => { + res(); + }; + }); + } + const requestId = crypto.randomUUID(); + ws.send( + JSON.stringify({ + type: action, + payload, + id: requestId, + }), + ); + return new Promise< + // @ts-expect-error We dont care since this is internal api + Awaited> + >((res, rej) => { + const listener = (event: MessageEvent) => { + const data = JSON.parse(event.data); + if (data.id === requestId) { + removeMessageListener(listener); + if (data.error) { + rej(data.error); + } else { + res(data.payload); + } + } + }; + addMessageListener(listener); + }); + }; + return { + dispatch, + }; +}; + +export const wsClient = createWSClient(); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..e69de29 diff --git a/vite.config.ts b/vite.config.ts index 861b04b..50bf145 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import tailwindcss from "@tailwindcss/vite"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), tailwindcss()], +});