From f2183f0d1533175ba6db2997c1a1d963a3a3d705 Mon Sep 17 00:00:00 2001 From: MasterGordon Date: Thu, 11 Sep 2025 14:47:51 +0200 Subject: [PATCH] added connection status --- backend/router.ts | 12 ++++- src/atoms.ts | 3 ++ src/components/ConnectionStatus.tsx | 40 +++++++++++++++ src/components/Header.tsx | 4 +- src/views/profile/Profile.tsx | 77 +++++++++++++++-------------- src/wsClient.ts | 63 ++++++++++++++++++++--- 6 files changed, 153 insertions(+), 46 deletions(-) create mode 100644 src/components/ConnectionStatus.tsx diff --git a/backend/router.ts b/backend/router.ts index 04ed1ef..5f85f6e 100644 --- a/backend/router.ts +++ b/backend/router.ts @@ -37,12 +37,20 @@ export const handleRequest = async ( !message || !(typeof message === "object") || !("type" in message) || - !("payload" in message) || !("id" in message) ) return; - const { type, payload, id } = message; + const { type, id } = message; if (!(typeof type === "string")) return; + + // Handle ping message + if (type === 'ping') { + ws.send(JSON.stringify({ type: 'pong', id })); + return; + } + + if (!("payload" in message)) return; + const { payload } = message; const [controllerName, action] = type.split("."); if (!(controllerName in controllers)) return; try { diff --git a/src/atoms.ts b/src/atoms.ts index 7b0749b..5ae1247 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -15,3 +15,6 @@ interface LootboxResult { lootbox: string; } export const lootboxResultAtom = atom(); + +export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting'; +export const connectionStatusAtom = atom('connecting'); diff --git a/src/components/ConnectionStatus.tsx b/src/components/ConnectionStatus.tsx new file mode 100644 index 0000000..6786b04 --- /dev/null +++ b/src/components/ConnectionStatus.tsx @@ -0,0 +1,40 @@ +import { useAtom } from "jotai"; +import { connectionStatusAtom, type ConnectionStatus as ConnectionStatusType } from "../atoms"; +import { Wifi, WifiOff } from "lucide-react"; +import { motion, AnimatePresence } from "motion/react"; + +const statusConfig: Record = { + connecting: { color: "text-yellow-400", icon: , label: "Connecting..." }, + connected: { color: "text-green-400", icon: , label: "Connected" }, + disconnected: { color: "text-red-400", icon: , label: "Disconnected" }, + reconnecting: { color: "text-yellow-400", icon: , label: "Reconnecting..." }, +}; + +export const ConnectionStatus = () => { + const [status] = useAtom(connectionStatusAtom); + const config = statusConfig[status]; + + // Only show when not connected + const shouldShow = status !== 'connected'; + + return ( + + {shouldShow && ( + + + {config.icon} + + {config.label} + + )} + + ); +}; \ No newline at end of file diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d706469..5bc0d7d 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -15,6 +15,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; import { loginTokenAtom } from "../atoms"; import Gems from "./Gems"; +import { ConnectionStatus } from "./ConnectionStatus"; const Header = () => { const [, setLocation] = useLocation(); @@ -28,7 +29,8 @@ const Header = () => { const { data: gems } = useWSQuery("user.getOwnGems", null); return ( -
+
+
{username ? ( diff --git a/src/views/profile/Profile.tsx b/src/views/profile/Profile.tsx index c199b2f..13621a1 100644 --- a/src/views/profile/Profile.tsx +++ b/src/views/profile/Profile.tsx @@ -9,19 +9,22 @@ import { import PastMatch from "../../components/PastMatch"; import GemsIcon from "../../components/GemIcon"; -const TouchTooltip = ({ children, content, date, games }: { - children: React.ReactNode; - content: React.ReactNode; +const TouchTooltip = ({ + children, + date, + games, +}: { + children: React.ReactNode; date: string; games: number; }) => { const [isOpen, setIsOpen] = useState(false); const [isTouchDevice, setIsTouchDevice] = useState(false); - const timeoutRef = useRef(); + const timeoutRef = useRef(); useEffect(() => { // Detect if device supports touch - setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0); + setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0); }, []); const handleTouch = () => { @@ -42,7 +45,11 @@ const TouchTooltip = ({ children, content, date, games }: { if (isTouchDevice) { return ( - + {children} @@ -56,9 +63,7 @@ const TouchTooltip = ({ children, content, date, games }: { // For non-touch devices, use default hover behavior return ( - - {children} - + {children}

{date}

{games} Games Played

@@ -90,41 +95,40 @@ const Profile: React.FC = () => { !!username, ); const now = useMemo(() => dayjs(), []); - 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 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); - + + window.addEventListener("resize", calculateAvailableWeeks); + return () => { clearTimeout(timer); - window.removeEventListener('resize', calculateAvailableWeeks); + window.removeEventListener("resize", calculateAvailableWeeks); }; }, []); @@ -133,19 +137,19 @@ const Profile: React.FC = () => { 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]); @@ -153,7 +157,9 @@ const Profile: React.FC = () => { return (
-
{username}
+
+ {username} +

Total Games: {profile?.totalGames}

Highest Stage: {profile?.highestStage}

@@ -178,22 +184,21 @@ const Profile: React.FC = () => {
{Array.from({ length: availableWeeks }).map((_, w) => ( -
+
{Array.from({ length: 7 }).map((_, d) => { - const index = (heatmap.length - (availableWeeks * 7)) + (w * 7 + 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") + .subtract(availableWeeks * 7 - 1 - (w * 7 + d), "days") .format("DD/MM/YYYY"); return ( - -