wip
This commit is contained in:
parent
8319de0812
commit
276d2faf42
|
|
@ -24,3 +24,5 @@ temp_dbs
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
deploy.sh
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import type { ServerWebSocket } from "bun";
|
||||||
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
interface RequestContext {
|
interface RequestContext {
|
||||||
user?: string;
|
user?: string;
|
||||||
db: BunSQLiteDatabase;
|
db: BunSQLiteDatabase;
|
||||||
|
ws: ServerWebSocket<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Endpoint<TInput, TResponse> = {
|
export type Endpoint<TInput, TResponse> = {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createController, createEndpoint } from "./controller";
|
||||||
|
import { loginUser, registerUser } from "../repositories/userRepository";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { resetSessionUser, setSessionUser } from "../router";
|
||||||
|
|
||||||
|
const secret = process.env.SECRET!;
|
||||||
|
|
||||||
|
const signString = (payload: string) => {
|
||||||
|
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userController = createController({
|
||||||
|
getSelf: createEndpoint(z.null(), async (_, { user }) => {
|
||||||
|
return user;
|
||||||
|
}),
|
||||||
|
login: createEndpoint(
|
||||||
|
z.object({ username: z.string(), password: z.string() }),
|
||||||
|
async (input, { db, ws }) => {
|
||||||
|
const { name: user } = await loginUser(
|
||||||
|
db,
|
||||||
|
input.username,
|
||||||
|
input.password,
|
||||||
|
);
|
||||||
|
const session = { user, expires: Date.now() + 1000 * 60 * 60 * 24 * 14 };
|
||||||
|
const sig = signString(JSON.stringify(session));
|
||||||
|
setSessionUser(ws, user);
|
||||||
|
return { token: JSON.stringify({ session, sig }) };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loginWithToken: createEndpoint(
|
||||||
|
z.object({ token: z.string() }),
|
||||||
|
async (input, { ws }) => {
|
||||||
|
const { session, sig } = JSON.parse(input.token);
|
||||||
|
const { user } = session;
|
||||||
|
if (sig !== signString(JSON.stringify(session))) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
if (Date.now() > session.expires) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
setSessionUser(ws, user);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
logout: createEndpoint(z.null(), async (_, { ws }) => {
|
||||||
|
resetSessionUser(ws);
|
||||||
|
}),
|
||||||
|
register: createEndpoint(
|
||||||
|
z.object({ username: z.string().max(15), password: z.string().min(6) }),
|
||||||
|
async (input, { db, ws }) => {
|
||||||
|
await registerUser(db, input.username, input.password);
|
||||||
|
const user = input.username;
|
||||||
|
const session = { user, expires: Date.now() + 1000 * 60 * 60 * 24 * 14 };
|
||||||
|
const sig = signString(JSON.stringify(session));
|
||||||
|
setSessionUser(ws, user);
|
||||||
|
return { token: JSON.stringify({ session, sig }) };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
@ -3,9 +3,11 @@ import type { ServerWebSocket } from "bun";
|
||||||
import type { Controller, Endpoint } from "./controller/controller";
|
import type { Controller, Endpoint } from "./controller/controller";
|
||||||
import { gameController } from "./controller/gameController";
|
import { gameController } from "./controller/gameController";
|
||||||
import { db } from "./database/db";
|
import { db } from "./database/db";
|
||||||
|
import { userController } from "./controller/userController";
|
||||||
|
|
||||||
const controllers = {
|
const controllers = {
|
||||||
game: gameController,
|
game: gameController,
|
||||||
|
user: userController,
|
||||||
} satisfies Record<string, Controller<any>>;
|
} satisfies Record<string, Controller<any>>;
|
||||||
|
|
||||||
const userName = new WeakMap<ServerWebSocket<unknown>, string>();
|
const userName = new WeakMap<ServerWebSocket<unknown>, string>();
|
||||||
|
|
@ -14,6 +16,10 @@ export const setSessionUser = (ws: ServerWebSocket<unknown>, user: string) => {
|
||||||
userName.set(ws, user);
|
userName.set(ws, user);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const resetSessionUser = (ws: ServerWebSocket<unknown>) => {
|
||||||
|
userName.delete(ws);
|
||||||
|
};
|
||||||
|
|
||||||
export const handleRequest = async (
|
export const handleRequest = async (
|
||||||
message: unknown,
|
message: unknown,
|
||||||
ws: ServerWebSocket<unknown>,
|
ws: ServerWebSocket<unknown>,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" /> -->
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="format-detection" content="telephone=no"/>
|
<meta name="format-detection" content="telephone=no"/>
|
||||||
|
<meta name="darkreader-lock">
|
||||||
<title>Minesweeper</title>
|
<title>Minesweeper</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,12 @@
|
||||||
"drizzle:migrate": "bun run backend/migrate.ts"
|
"drizzle:migrate": "bun run backend/migrate.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@tailwindcss/vite": "^4.0.0-alpha.24",
|
"@tailwindcss/vite": "^4.0.0-alpha.24",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
|
"@tanstack/react-query-devtools": "^5.0.0-alpha.91",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ import { Button } from "./components/Button";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Github,
|
|
||||||
History,
|
History,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Menu,
|
Menu,
|
||||||
Play,
|
Play,
|
||||||
Settings,
|
Settings,
|
||||||
Settings2,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Hr from "./components/Hr";
|
import Hr from "./components/Hr";
|
||||||
import NavLink from "./components/NavLink";
|
import NavLink from "./components/NavLink";
|
||||||
|
import { useMediaQuery } from "@uidotdev/usehooks";
|
||||||
|
import Header from "./components/Header";
|
||||||
|
|
||||||
const drawerWidth = 256;
|
const drawerWidth = 256;
|
||||||
const drawerWidthWithPadding = drawerWidth;
|
const drawerWidthWithPadding = drawerWidth;
|
||||||
|
|
@ -22,14 +22,10 @@ const Shell: React.FC = () => {
|
||||||
|
|
||||||
const x = isOpen ? 0 : -drawerWidthWithPadding;
|
const x = isOpen ? 0 : -drawerWidthWithPadding;
|
||||||
const width = isOpen ? drawerWidthWithPadding : 0;
|
const width = isOpen ? drawerWidthWithPadding : 0;
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onResize = () => {
|
setIsOpen(!isMobile);
|
||||||
setIsOpen(window.innerWidth > 768);
|
}, [isMobile]);
|
||||||
};
|
|
||||||
window.addEventListener("resize", onResize);
|
|
||||||
onResize();
|
|
||||||
return () => window.removeEventListener("resize", onResize);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative bg-black min-h-screen">
|
<div className="relative bg-black min-h-screen">
|
||||||
|
|
@ -85,7 +81,8 @@ const Shell: React.FC = () => {
|
||||||
layout
|
layout
|
||||||
/>
|
/>
|
||||||
<motion.div className="flex flex-col gap-4 grow max-w-6xl mx-auto">
|
<motion.div className="flex flex-col gap-4 grow max-w-6xl mx-auto">
|
||||||
<div className="flex flex-col justify-center gap-4 sm:mx-16 mt-12 sm:mt-0">
|
<div className="flex flex-col justify-center gap-4 sm:mx-16 mt-16 sm:mt-2 mx-2">
|
||||||
|
<Header />
|
||||||
<div className="bg-gray-950 p-4 rounded-lg w-full"></div>
|
<div className="bg-gray-950 p-4 rounded-lg w-full"></div>
|
||||||
<div className="bg-gray-950 p-4 rounded-lg w-full"></div>
|
<div className="bg-gray-950 p-4 rounded-lg w-full"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
import { atomWithStorage } from "jotai/utils";
|
||||||
|
|
||||||
export const gameId = atom<string | undefined>(undefined);
|
export const gameId = atom<string | undefined>(undefined);
|
||||||
|
export const loginToken = atomWithStorage<string | undefined>(
|
||||||
|
"loginToken",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../Dialog";
|
||||||
|
|
||||||
|
const LoginButton = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUsername("");
|
||||||
|
setPassword("");
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="default">Login</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Login</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<label className="text-white/70 font-bold">Username</label>
|
||||||
|
<input
|
||||||
|
className="border-white/10 border-2 rounded-md p-2"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label className="text-white/70 font-bold">Password</label>
|
||||||
|
<input
|
||||||
|
className="border-white/10 border-2 rounded-md p-2"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={() => setIsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary">Login</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginButton;
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../Dialog";
|
||||||
|
|
||||||
|
const RegisterButton = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUsername("");
|
||||||
|
setPassword("");
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="primary">Register</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Register</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<label className="text-white/70 font-bold">Username</label>
|
||||||
|
<input
|
||||||
|
className="border-white/10 border-2 rounded-md p-2"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label className="text-white/70 font-bold">Password</label>
|
||||||
|
<input
|
||||||
|
className="border-white/10 border-2 rounded-md p-2"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={() => setIsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary">Register</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterButton;
|
||||||
|
|
@ -2,11 +2,15 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva("font-semibold py-2 px-4 rounded-md", {
|
const buttonVariants = cva("font-semibold py-2 px-4 rounded-md flex gap-2", {
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-white/95 hover:bg-primary/90",
|
default: "bg-gray-900 text-white/95",
|
||||||
ghost: "bg-transparent text-white/95 hover:bg-white/05",
|
ghost: "bg-transparent text-white/95 hover:bg-white/05",
|
||||||
|
outline:
|
||||||
|
"bg-transparent text-white/95 hover:bg-white/05 border-white/10 border-1",
|
||||||
|
primary:
|
||||||
|
"[background:var(--bg-brand)] text-white/95 hover:bg-white/05 hover:animate-gradientmove",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 py-2 px-4",
|
default: "h-10 py-2 px-4",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg border-white/20 text-white/90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
));
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
));
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { UserRound } from "lucide-react";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "./DropdownMenu";
|
||||||
|
import { useLocation } from "wouter";
|
||||||
|
import LoginButton from "./Auth/LoginButton";
|
||||||
|
import { useWSQuery } from "../hooks";
|
||||||
|
import RegisterButton from "./Auth/RegisterButton";
|
||||||
|
import banner from "../images/banner.png";
|
||||||
|
import mine from "../images/mine.png";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const [, setLocation] = useLocation();
|
||||||
|
const { data: username } = useWSQuery("user.getSelf", null);
|
||||||
|
return (
|
||||||
|
<div className="w-full flex gap-4">
|
||||||
|
<div className="grow" />
|
||||||
|
<img src={banner} className="w-auto h-16" />
|
||||||
|
<img
|
||||||
|
src={mine}
|
||||||
|
className="w-auto h-16 drop-shadow-[0px_0px_10px_#fff] -rotate-12"
|
||||||
|
/>
|
||||||
|
<div className="grow" />
|
||||||
|
{username ? (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<UserRound className="text-white/70" />
|
||||||
|
<p className="text-white/70 font-bold">{username}</p>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="text-white/70 w-auto mt-2 bg-black">
|
||||||
|
<DropdownMenuItem onClick={() => setLocation("/profile")}>
|
||||||
|
Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setLocation("/settings")}>
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LoginButton />
|
||||||
|
<RegisterButton />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
import { forwardRef, PropsWithChildren } from "react";
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
|
const PopoverTrigger: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Trigger asChild>{children}</PopoverPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PopoverContent = forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-gray-950",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
));
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent };
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
|
|
@ -4,6 +4,16 @@
|
||||||
--color-primary: hotpink;
|
--color-primary: hotpink;
|
||||||
--bg-brand: -webkit-linear-gradient(225deg, rgb(251, 175, 21), rgb(251, 21, 242),
|
--bg-brand: -webkit-linear-gradient(225deg, rgb(251, 175, 21), rgb(251, 21, 242),
|
||||||
rgb(21, 198, 251)) 0% 0% / 100% 300%;
|
rgb(21, 198, 251)) 0% 0% / 100% 300%;
|
||||||
|
--bg-secondary: linear-gradient(90deg, #D9AFD9 0%, #97D9E1 100%) 0% 0% / 100% 300%;
|
||||||
|
--animate-gradientmove: gradientmove 1s ease 0s 1 normal forwards;
|
||||||
|
@keyframes gradientmove {
|
||||||
|
0%{background-position: 0% 0%}
|
||||||
|
100%{background-position: 0% 100%}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .game-board { */
|
/* .game-board { */
|
||||||
|
|
@ -99,11 +109,6 @@
|
||||||
/* animation: gradient_move 1s ease infinite; */
|
/* 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 { */
|
/* .header { */
|
||||||
/* display: grid; */
|
/* display: grid; */
|
||||||
|
|
|
||||||
12
src/main.tsx
12
src/main.tsx
|
|
@ -4,8 +4,13 @@ import App from "./App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { connectWS } from "./ws.ts";
|
import { connectWS } from "./ws.ts";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import {
|
||||||
|
QueryCache,
|
||||||
|
QueryClient,
|
||||||
|
QueryClientProvider,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import Shell from "./Shell.tsx";
|
import Shell from "./Shell.tsx";
|
||||||
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
document.addEventListener("contextmenu", (event) => {
|
document.addEventListener("contextmenu", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -13,7 +18,9 @@ document.addEventListener("contextmenu", (event) => {
|
||||||
|
|
||||||
connectWS();
|
connectWS();
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient({
|
||||||
|
queryCache: new QueryCache(),
|
||||||
|
});
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
@ -21,6 +28,7 @@ createRoot(document.getElementById("root")!).render(
|
||||||
<Toaster position="top-right" reverseOrder={false} />
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
<Shell />
|
<Shell />
|
||||||
{/* <App /> */}
|
{/* <App /> */}
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue