Compare commits
No commits in common. "bcdee860c78f328ed9a61b261658f5817b45a724" and "676bd7a9784bdbdd9cd822297d4009a35560a4bd" have entirely different histories.
bcdee860c7
...
676bd7a978
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- run: bun install --frozen-lockfile
|
- run: bun install
|
||||||
- run: bun run build
|
- run: bun run build
|
||||||
- run: bun test
|
- run: bun test
|
||||||
- run: bun lint
|
- run: bun lint
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "next",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/random-seed": "^0.3.5",
|
"@types/random-seed": "^0.3.5",
|
||||||
"@types/react": "^19.1.13",
|
"@types/react": "^19.1.13",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ import { useMediaQuery } from "@uidotdev/usehooks";
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
import { Tag } from "./components/Tag";
|
import { Tag } from "./components/Tag";
|
||||||
import Feed from "./components/Feed/Feed";
|
import Feed from "./components/Feed/Feed";
|
||||||
import { useWSQuery } from "./hooks";
|
import { useAtom } from "jotai";
|
||||||
|
import { loginTokenAtom } from "./atoms";
|
||||||
|
|
||||||
const drawerWidth = 256;
|
const drawerWidth = 256;
|
||||||
const drawerWidthWithPadding = drawerWidth;
|
const drawerWidthWithPadding = drawerWidth;
|
||||||
|
|
@ -25,7 +26,7 @@ const drawerWidthWithPadding = drawerWidth;
|
||||||
const Shell: React.FC<PropsWithChildren> = ({ children }) => {
|
const Shell: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const drawerRef = useRef<HTMLDivElement>(null);
|
const drawerRef = useRef<HTMLDivElement>(null);
|
||||||
const { data: username } = useWSQuery("user.getSelf", null);
|
const [loginToken] = useAtom(loginTokenAtom);
|
||||||
|
|
||||||
const x = isOpen ? 0 : -drawerWidthWithPadding;
|
const x = isOpen ? 0 : -drawerWidthWithPadding;
|
||||||
const width = isOpen ? drawerWidthWithPadding : 0;
|
const width = isOpen ? drawerWidthWithPadding : 0;
|
||||||
|
|
@ -81,7 +82,7 @@ const Shell: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
<Play />
|
<Play />
|
||||||
Play
|
Play
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{username && (
|
{loginToken && (
|
||||||
<NavLink href="/history">
|
<NavLink href="/history">
|
||||||
<History />
|
<History />
|
||||||
History
|
History
|
||||||
|
|
@ -91,13 +92,13 @@ const Shell: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
<Store />
|
<Store />
|
||||||
Store
|
Store
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{username && (
|
{loginToken && (
|
||||||
<NavLink href="/collection">
|
<NavLink href="/collection">
|
||||||
<Library />
|
<Library />
|
||||||
Collection <Tag size="sm">NEW</Tag>
|
Collection <Tag size="sm">NEW</Tag>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
{username && (
|
{loginToken && (
|
||||||
<NavLink href="/settings">
|
<NavLink href="/settings">
|
||||||
<Settings />
|
<Settings />
|
||||||
Settings
|
Settings
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||||
import PasswordInput from "./PasswordInput";
|
import PasswordInput from "./PasswordInput";
|
||||||
import { wsClient } from "../../wsClient";
|
import { wsClient } from "../../wsClient";
|
||||||
|
|
||||||
const RegisterButton = ({ label }: { label?: string }) => {
|
const RegisterButton = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isLoginMode, setIsLoginMode] = useState(false);
|
const [isLoginMode, setIsLoginMode] = useState(false);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
|
|
@ -36,7 +36,7 @@ const RegisterButton = ({ label }: { label?: string }) => {
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="primary" className="self-start">
|
<Button variant="primary" className="self-start">
|
||||||
{label ?? "Register"}
|
Register
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -188,11 +188,11 @@ const Board: React.FC<BoardProps> = (props) => {
|
||||||
(props.width || props.height
|
(props.width || props.height
|
||||||
? { left: 0, right: boardWidth, top: 0, bottom: boardHeight }
|
? { left: 0, right: boardWidth, top: 0, bottom: boardHeight }
|
||||||
: {
|
: {
|
||||||
left: -theme.size,
|
left: -theme.size,
|
||||||
right: boardWidth + theme.size,
|
right: boardWidth + theme.size,
|
||||||
top: -theme.size,
|
top: -theme.size,
|
||||||
bottom: boardHeight + theme.size,
|
bottom: boardHeight + theme.size,
|
||||||
}),
|
}),
|
||||||
[boardHeight, boardWidth, props.height, props.width, theme],
|
[boardHeight, boardWidth, props.height, props.width, theme],
|
||||||
);
|
);
|
||||||
const clampZoom = useMemo(
|
const clampZoom = useMemo(
|
||||||
|
|
|
||||||
40
src/main.tsx
40
src/main.tsx
|
|
@ -15,49 +15,17 @@ import Collection from "./views/collection/Collection.tsx";
|
||||||
import { AnimatePresence } from "motion/react";
|
import { AnimatePresence } from "motion/react";
|
||||||
import Store from "./views/store/Store.tsx";
|
import Store from "./views/store/Store.tsx";
|
||||||
import Profile from "./views/profile/Profile.tsx";
|
import Profile from "./views/profile/Profile.tsx";
|
||||||
import { useWSQuery } from "./hooks.ts";
|
|
||||||
import RegisterButton from "./components/Auth/RegisterButton.tsx";
|
|
||||||
|
|
||||||
const ProtectedRoute: React.FC<{
|
|
||||||
component: React.ComponentType<any>;
|
|
||||||
path: string;
|
|
||||||
}> = ({ component: Component, path }) => {
|
|
||||||
const { data: username, isLoading } = useWSQuery("user.getSelf", null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Route path={path}>
|
|
||||||
{(params) => {
|
|
||||||
if (isLoading) return null;
|
|
||||||
if (!username) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center py-24 gap-6">
|
|
||||||
<h2 className="text-white/90 text-2xl font-bold">
|
|
||||||
This page is only available to logged-in users.
|
|
||||||
</h2>
|
|
||||||
<RegisterButton label="Login to access" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Component {...params} />;
|
|
||||||
}}
|
|
||||||
</Route>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
const token = localStorage.getItem("loginToken");
|
const token = localStorage.getItem("loginToken");
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const res = await wsClient.dispatch("user.loginWithToken", {
|
await wsClient.dispatch("user.loginWithToken", {
|
||||||
token: JSON.parse(token),
|
token: JSON.parse(token),
|
||||||
});
|
});
|
||||||
if (!res.success) {
|
|
||||||
localStorage.removeItem("loginToken");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
localStorage.removeItem("loginToken");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -73,9 +41,9 @@ setup().then(() => {
|
||||||
<Route path="/play/:gameId?">
|
<Route path="/play/:gameId?">
|
||||||
{(params) => <Endless gameId={params.gameId} />}
|
{(params) => <Endless gameId={params.gameId} />}
|
||||||
</Route>
|
</Route>
|
||||||
<ProtectedRoute path="/history" component={MatchHistory} />
|
<Route path="/history" component={MatchHistory} />
|
||||||
<ProtectedRoute path="/settings" component={Settings} />
|
<Route path="/settings" component={Settings} />
|
||||||
<ProtectedRoute path="/collection" component={Collection} />
|
<Route path="/collection" component={Collection} />
|
||||||
<Route path="/store" component={Store} />
|
<Route path="/store" component={Store} />
|
||||||
<Route path="/profile/:username?">
|
<Route path="/profile/:username?">
|
||||||
{(params) => <Profile username={params.username} />}
|
{(params) => <Profile username={params.username} />}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useWSMutation, useWSQuery } from "../../hooks";
|
import { useWSMutation, useWSQuery } from "../../hooks";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { gameIdAtom } from "../../atoms";
|
import { gameIdAtom, loginTokenAtom } from "../../atoms";
|
||||||
import { Button } from "../../components/Button";
|
import { Button } from "../../components/Button";
|
||||||
import LeaderboardButton from "../../components/LeaderboardButton";
|
import LeaderboardButton from "../../components/LeaderboardButton";
|
||||||
import { ShareButton } from "../../components/ShareButton";
|
import { ShareButton } from "../../components/ShareButton";
|
||||||
|
|
@ -15,6 +15,7 @@ interface EndlessProps {
|
||||||
|
|
||||||
const Endless: React.FC<EndlessProps> = (props) => {
|
const Endless: React.FC<EndlessProps> = (props) => {
|
||||||
const [gameId, setGameId] = useAtom(gameIdAtom);
|
const [gameId, setGameId] = useAtom(gameIdAtom);
|
||||||
|
const [loginToken] = useAtom(loginTokenAtom);
|
||||||
const { data: game } = useWSQuery("game.getGameState", gameId!, !!gameId);
|
const { data: game } = useWSQuery("game.getGameState", gameId!, !!gameId);
|
||||||
const { data: settings } = useWSQuery("user.getSettings", null);
|
const { data: settings } = useWSQuery("user.getSettings", null);
|
||||||
const { data: currentUsername } = useWSQuery("user.getSelf", null);
|
const { data: currentUsername } = useWSQuery("user.getSelf", null);
|
||||||
|
|
@ -92,7 +93,7 @@ const Endless: React.FC<EndlessProps> = (props) => {
|
||||||
<div className="w-full grid md:grid-cols-[350px_1fr]">
|
<div className="w-full grid md:grid-cols-[350px_1fr]">
|
||||||
<div className="flex flex-col md:border-r-white/10 md:border-r-1 gap-8 pr-12">
|
<div className="flex flex-col md:border-r-white/10 md:border-r-1 gap-8 pr-12">
|
||||||
<h2 className="text-white/90 text-xl">Minesweeper Endless</h2>
|
<h2 className="text-white/90 text-xl">Minesweeper Endless</h2>
|
||||||
{currentUsername ? (
|
{loginToken ? (
|
||||||
<Button
|
<Button
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -106,7 +107,7 @@ const Endless: React.FC<EndlessProps> = (props) => {
|
||||||
Start Game
|
Start Game
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<RegisterButton label="Sign in to Play" />
|
<RegisterButton />
|
||||||
)}
|
)}
|
||||||
<h2 className="text-white/80 text-lg mt-8">How to play</h2>
|
<h2 className="text-white/80 text-lg mt-8">How to play</h2>
|
||||||
<p className="text-white/90">
|
<p className="text-white/90">
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,11 @@ import { useEffect } from "react";
|
||||||
import { initParticlesEngine, Particles as ParticlesComponent } from "@tsparticles/react";
|
import { initParticlesEngine, Particles as ParticlesComponent } from "@tsparticles/react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import BounceImg from "../../components/BounceImg";
|
import BounceImg from "../../components/BounceImg";
|
||||||
import RegisterButton from "../../components/Auth/RegisterButton";
|
|
||||||
|
|
||||||
const Store = () => {
|
const Store = () => {
|
||||||
const openLootbox = useWSMutation("user.openLootbox");
|
const openLootbox = useWSMutation("user.openLootbox");
|
||||||
const [lootboxResult, setLootboxResult] = useAtom(lootboxResultAtom);
|
const [lootboxResult, setLootboxResult] = useAtom(lootboxResultAtom);
|
||||||
const currentLootbox = lootboxes.find((l) => l.id === lootboxResult?.lootbox);
|
const currentLootbox = lootboxes.find((l) => l.id === lootboxResult?.lootbox);
|
||||||
const { data: username } = useWSQuery("user.getSelf", null);
|
|
||||||
const { refetch } = useWSQuery("user.getOwnGems", null);
|
const { refetch } = useWSQuery("user.getOwnGems", null);
|
||||||
|
|
||||||
// this should be run only once per application lifetime
|
// this should be run only once per application lifetime
|
||||||
|
|
@ -186,24 +184,18 @@ const Store = () => {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
<BounceImg src={lootbox.image} className="w-[360px]" />
|
<BounceImg src={lootbox.image} className="w-[360px]" />
|
||||||
{username ? (
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
size="default"
|
||||||
size="default"
|
className="mx-auto items-center"
|
||||||
className="mx-auto items-center"
|
onClick={() => {
|
||||||
onClick={() => {
|
openLootbox
|
||||||
openLootbox
|
.mutateAsync({ id: lootbox.id })
|
||||||
.mutateAsync({ id: lootbox.id })
|
.then(() => refetch());
|
||||||
.then(() => refetch());
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Buy for <b>{lootbox.priceText}</b> <GemsIcon />
|
||||||
Buy for <b>{lootbox.priceText}</b> <GemsIcon />
|
</Button>
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<RegisterButton label="Login to Buy" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue