This commit is contained in:
MasterGordon 2024-09-24 01:27:07 +02:00
parent 8319de0812
commit 276d2faf42
22 changed files with 635 additions and 20 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
SECRET=Aijd108jAW8dj98uj12918AWdijo83289jAdioj

2
.gitignore vendored
View File

@ -24,3 +24,5 @@ temp_dbs
*.njsproj
*.sln
*.sw?
deploy.sh

View File

@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ServerWebSocket } from "bun";
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
import type { z } from "zod";
interface RequestContext {
user?: string;
db: BunSQLiteDatabase;
ws: ServerWebSocket<unknown>;
}
export type Endpoint<TInput, TResponse> = {

View File

@ -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 }) };
},
),
});

View File

@ -3,9 +3,11 @@ import type { ServerWebSocket } from "bun";
import type { Controller, Endpoint } from "./controller/controller";
import { gameController } from "./controller/gameController";
import { db } from "./database/db";
import { userController } from "./controller/userController";
const controllers = {
game: gameController,
user: userController,
} satisfies Record<string, Controller<any>>;
const userName = new WeakMap<ServerWebSocket<unknown>, string>();
@ -14,6 +16,10 @@ export const setSessionUser = (ws: ServerWebSocket<unknown>, user: string) => {
userName.set(ws, user);
};
export const resetSessionUser = (ws: ServerWebSocket<unknown>) => {
userName.delete(ws);
};
export const handleRequest = async (
message: unknown,
ws: ServerWebSocket<unknown>,

BIN
bun.lockb

Binary file not shown.

View File

@ -3,8 +3,9 @@
<head>
<meta charset="UTF-8" />
<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="darkreader-lock">
<title>Minesweeper</title>
</head>
<body>

View File

@ -14,8 +14,12 @@
"drizzle:migrate": "bun run backend/migrate.ts"
},
"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",
"@tanstack/react-query": "^5.56.2",
"@tanstack/react-query-devtools": "^5.0.0-alpha.91",
"@uidotdev/usehooks": "^2.4.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",

View File

@ -3,16 +3,16 @@ 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";
import { useMediaQuery } from "@uidotdev/usehooks";
import Header from "./components/Header";
const drawerWidth = 256;
const drawerWidthWithPadding = drawerWidth;
@ -22,14 +22,10 @@ const Shell: React.FC = () => {
const x = isOpen ? 0 : -drawerWidthWithPadding;
const width = isOpen ? drawerWidthWithPadding : 0;
const isMobile = useMediaQuery("(max-width: 768px)");
useEffect(() => {
const onResize = () => {
setIsOpen(window.innerWidth > 768);
};
window.addEventListener("resize", onResize);
onResize();
return () => window.removeEventListener("resize", onResize);
}, []);
setIsOpen(!isMobile);
}, [isMobile]);
return (
<div className="relative bg-black min-h-screen">
@ -85,7 +81,8 @@ const Shell: React.FC = () => {
layout
/>
<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>

View File

@ -1,3 +1,8 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
export const gameId = atom<string | undefined>(undefined);
export const loginToken = atomWithStorage<string | undefined>(
"loginToken",
undefined,
);

View File

@ -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;

View File

@ -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;

View File

@ -2,11 +2,15 @@ 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", {
const buttonVariants = cva("font-semibold py-2 px-4 rounded-md flex gap-2", {
variants: {
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",
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: {
default: "h-10 py-2 px-4",

119
src/components/Dialog.tsx Normal file
View File

@ -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,
};

View File

@ -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,
};

58
src/components/Header.tsx Normal file
View File

@ -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;

View File

@ -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 };

BIN
src/images/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
src/images/expert.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
src/images/mine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -4,6 +4,16 @@
--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%;
--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 { */
@ -99,11 +109,6 @@
/* 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; */

View File

@ -4,8 +4,13 @@ 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 {
QueryCache,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import Shell from "./Shell.tsx";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
document.addEventListener("contextmenu", (event) => {
event.preventDefault();
@ -13,7 +18,9 @@ document.addEventListener("contextmenu", (event) => {
connectWS();
const queryClient = new QueryClient();
const queryClient = new QueryClient({
queryCache: new QueryCache(),
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
@ -21,6 +28,7 @@ createRoot(document.getElementById("root")!).render(
<Toaster position="top-right" reverseOrder={false} />
<Shell />
{/* <App /> */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</StrictMode>,
);