diff --git a/src/Shell.tsx b/src/Shell.tsx index d393727..30ee15d 100644 --- a/src/Shell.tsx +++ b/src/Shell.tsx @@ -17,6 +17,8 @@ import { useMediaQuery } from "@uidotdev/usehooks"; import Header from "./components/Header"; import { Tag } from "./components/Tag"; import Feed from "./components/Feed/Feed"; +import { useAtom } from "jotai"; +import { loginTokenAtom } from "./atoms"; const drawerWidth = 256; const drawerWidthWithPadding = drawerWidth; @@ -24,6 +26,7 @@ const drawerWidthWithPadding = drawerWidth; const Shell: React.FC = ({ children }) => { const [isOpen, setIsOpen] = useState(false); const drawerRef = useRef(null); + const [loginToken] = useAtom(loginTokenAtom); const x = isOpen ? 0 : -drawerWidthWithPadding; const width = isOpen ? drawerWidthWithPadding : 0; @@ -79,22 +82,28 @@ const Shell: React.FC = ({ children }) => { Play - - - History - + {loginToken && ( + + + History + + )} Store - - - Collection NEW - - - - Settings - + {loginToken && ( + + + Collection NEW + + )} + {loginToken && ( + + + Settings + + )}
{/*
*/} @@ -125,7 +134,7 @@ const Shell: React.FC = ({ children }) => { transition={{ type: "tween" }} layout /> - +
{children} diff --git a/src/components/Auth/RegisterButton.tsx b/src/components/Auth/RegisterButton.tsx index a0cc880..cdeb5eb 100644 --- a/src/components/Auth/RegisterButton.tsx +++ b/src/components/Auth/RegisterButton.tsx @@ -17,26 +17,31 @@ import { wsClient } from "../../wsClient"; const RegisterButton = () => { const [isOpen, setIsOpen] = useState(false); + const [isLoginMode, setIsLoginMode] = useState(false); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const queryClient = useQueryClient(); const register = useWSMutation("user.register"); + const login = useWSMutation("user.login"); const [, setToken] = useAtom(loginTokenAtom); useEffect(() => { setUsername(""); setPassword(""); + setError(""); }, [isOpen]); return ( - + - Register + {isLoginMode ? "Login" : "Register"}
@@ -49,6 +54,18 @@ const RegisterButton = () => {
{error &&

{error}

} +
+ +
diff --git a/src/components/PastMatch.tsx b/src/components/PastMatch.tsx index fb54902..ec9c7b2 100644 --- a/src/components/PastMatch.tsx +++ b/src/components/PastMatch.tsx @@ -10,7 +10,7 @@ interface PastMatchProps { const PastMatch = ({ game }: PastMatchProps) => { return (
-
+
Endless
@@ -24,10 +24,10 @@ const PastMatch = ({ game }: PastMatchProps) => { {game.minesCount - game.isFlagged.flat().filter((f) => f).length}
-
+
Duration: {formatTimeSpan(game.finished - game.started)}
-
+
{/* @ts-expect-error as is cheaply typed */} + {loginToken ? ( + + ) : ( + + )}

How to play

Endless minesweeper is just like regular minesweeper but you diff --git a/src/views/profile/Profile.tsx b/src/views/profile/Profile.tsx index 05b68d3..c199b2f 100644 --- a/src/views/profile/Profile.tsx +++ b/src/views/profile/Profile.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useMemo, useRef, useEffect, useState } from "react"; import { useWSQuery } from "../../hooks"; import dayjs from "dayjs"; import { @@ -9,7 +9,67 @@ import { import PastMatch from "../../components/PastMatch"; import GemsIcon from "../../components/GemIcon"; +const TouchTooltip = ({ children, content, date, games }: { + children: React.ReactNode; + content: React.ReactNode; + date: string; + games: number; +}) => { + const [isOpen, setIsOpen] = useState(false); + const [isTouchDevice, setIsTouchDevice] = useState(false); + const timeoutRef = useRef(); + + useEffect(() => { + // Detect if device supports touch + setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0); + }, []); + + const handleTouch = () => { + if (isTouchDevice) { + setIsOpen(true); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setIsOpen(false), 2000); + } + }; + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + // For touch devices, use controlled tooltip + if (isTouchDevice) { + return ( + + + {children} + + +

{date}

+

{games} Games Played

+ + + ); + } + + // For non-touch devices, use default hover behavior + return ( + + + {children} + + +

{date}

+

{games} Games Played

+
+
+ ); +}; + const Profile: React.FC = () => { + const [availableWeeks, setAvailableWeeks] = useState(16); // Default to 4 months + const containerRef = useRef(null); const { data: username } = useWSQuery("user.getSelf", null); const { data: heatmap } = useWSQuery( "user.getHeatmap", @@ -30,15 +90,71 @@ const Profile: React.FC = () => { !!username, ); const now = useMemo(() => dayjs(), []); - const firstOfYear = useMemo(() => now.startOf("year"), [now]); - const weeks = now.diff(firstOfYear, "weeks") + 1; + const startDate = useMemo(() => now.subtract(availableWeeks, "weeks").startOf("week"), [now, availableWeeks]); const maxHeat = heatmap ? Math.max(...heatmap) : 0; + // Calculate available space and adjust weeks accordingly + useEffect(() => { + const calculateAvailableWeeks = () => { + if (!containerRef.current) return; + + const containerWidth = containerRef.current.offsetWidth; + const isMobile = window.innerWidth < 640; // sm breakpoint + + // Calculate dot and gap sizes based on breakpoint + const dotWidth = isMobile ? 16 : 24; // w-4 = 16px, w-6 = 24px + const gapSize = isMobile ? 4 : 8; // gap-1 = 4px, gap-2 = 8px + + // Each week takes: dotWidth + gapSize + const weekWidth = dotWidth + gapSize; + + // Calculate max weeks that fit, with some padding + const maxWeeks = Math.floor((containerWidth - 32) / weekWidth); // 32px for padding + + // Limit between 4 weeks (1 month) and 26 weeks (6 months) + const weeks = Math.max(4, Math.min(26, maxWeeks)); + + setAvailableWeeks(weeks); + }; + + // Use setTimeout to ensure DOM is fully rendered + const timer = setTimeout(calculateAvailableWeeks, 0); + + window.addEventListener('resize', calculateAvailableWeeks); + + return () => { + clearTimeout(timer); + window.removeEventListener('resize', calculateAvailableWeeks); + }; + }, []); + + // Additional effect to recalculate when heatmap data is available + useEffect(() => { + if (heatmap && containerRef.current) { + const timer = setTimeout(() => { + if (!containerRef.current) return; + + const containerWidth = containerRef.current.offsetWidth; + const isMobile = window.innerWidth < 640; + + const dotWidth = isMobile ? 16 : 24; + const gapSize = isMobile ? 4 : 8; + const weekWidth = dotWidth + gapSize; + const maxWeeks = Math.floor((containerWidth - 32) / weekWidth); + const weeks = Math.max(4, Math.min(26, maxWeeks)); + + setAvailableWeeks(weeks); + }, 100); + + return () => clearTimeout(timer); + } + }, [heatmap]); + return ( -
-
-
{username}
-
+
+
+
{username}
+

Total Games: {profile?.totalGames}

Highest Stage: {profile?.highestStage}

@@ -57,40 +173,43 @@ const Profile: React.FC = () => { .map((game) => )}

{heatmap && ( -
+

Activity

-
- {Array.from({ length: weeks }).map((_, w) => ( -
- {Array.from({ length: 7 }).map((_, d) => { - const index = w * 7 + d; - if (index >= heatmap.length) return; - return ( - - -
+
+
+ {Array.from({ length: availableWeeks }).map((_, w) => ( +
+ {Array.from({ length: 7 }).map((_, d) => { + const index = (heatmap.length - (availableWeeks * 7)) + (w * 7 + d); + if (index < 0 || index >= heatmap.length) return; + const date = now + .clone() + .subtract((availableWeeks * 7) - 1 - (w * 7 + d), "days") + .format("DD/MM/YYYY"); + return ( + +
- ))} + + + ); + })} +
+ ))} +
)}