wip
This commit is contained in:
parent
8319de0812
commit
276d2faf42
|
|
@ -24,3 +24,5 @@ temp_dbs
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
deploy.sh
|
||||
|
|
|
|||
|
|
@ -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> = {
|
||||
|
|
|
|||
|
|
@ -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 { 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>,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 { 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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
--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; */
|
||||
|
|
|
|||
12
src/main.tsx
12
src/main.tsx
|
|
@ -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>,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue