diff --git a/bun.lock b/bun.lock index 915d474..1f2e63e 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-icons": "^5.5.0", + "react-tooltip": "^5.29.1", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "zod": "^4.0.10", @@ -87,6 +88,12 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], @@ -377,6 +384,8 @@ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -753,6 +762,8 @@ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-tooltip": ["react-tooltip@5.29.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.1", "classnames": "^2.3.0" }, "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0" } }, "sha512-rmJmEb/p99xWhwmVT7F7riLG08wwKykjHiMGbDPloNJk3tdI73oHsVOwzZ4SRjqMdd5/xwb/4nmz0RcoMfY7Bw=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], diff --git a/package.json b/package.json index 2a4864f..2b1e79b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-icons": "^5.5.0", + "react-tooltip": "^5.29.1", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "zod": "^4.0.10" diff --git a/src/components/player/displayname.tsx b/src/components/player/displayname.tsx index a99532e..e5762df 100644 --- a/src/components/player/displayname.tsx +++ b/src/components/player/displayname.tsx @@ -1,7 +1,7 @@ import { getColor } from "@/lib/colors" import { Player } from "@/lib/schema/player" -import { Wifi, WifiOff } from "lucide-react" import Link from "next/link" +import { OnlineStatus } from "./online-status" type NewPackageRank = Player["player"]["newPackageRank"] type MonthlyPackageRank = Player["player"]["monthlyPackageRank"] @@ -162,26 +162,3 @@ function GuildTag({ tag, tagColor, ign }: { tag?: string, tagColor?: string, ign ) } - -function OnlineStatus({ lastLogin, lastLogout }: { lastLogin: number | undefined, lastLogout: number | undefined }) { - const size = 36 - - if (!lastLogout || !lastLogin) { - return ( - - // Offline. Player is most likely in status offline or a staff with api off. - ) - } - - if (lastLogout > lastLogin) { - return ( - - // {`Offline. Last seen online ${formatRelativeTime(lastLogout, "past")}`} - ) - } - - return ( - - // {`Online. Online for ${formatRelativeTime(lastLogout, "future")}`} - ) -} diff --git a/src/components/player/online-status.tsx b/src/components/player/online-status.tsx new file mode 100644 index 0000000..31b3cf4 --- /dev/null +++ b/src/components/player/online-status.tsx @@ -0,0 +1,72 @@ +"use client" + +import { formatRelativeTime } from "@/lib/formatters" +import { Wifi, WifiOff } from "lucide-react" +import { useEffect, useState } from "react" +import { Tooltip } from "react-tooltip" + +export function OnlineStatus({ lastLogin, lastLogout }: { lastLogin: number | undefined, lastLogout: number | undefined }) { + const [pos, setPos] = useState({ x: 0, y: 0 }) + const size = 36 + + useEffect(() => { + const controller = new AbortController() + + window.addEventListener("mousemove", (e) => { + if (!(e.target instanceof SVGElement)) return + if (e.target.dataset.id !== "online-status") return + + setPos({ x: e.clientX, y: e.clientY - 10 }) + }, { signal: controller.signal }) + + return () => { + controller.abort() + } + }) + + if (!lastLogout || !lastLogin) { + return ( + <> + + + + ) + } + + if (lastLogout > lastLogin) { + return ( + <> + + + + ) + } + + return ( + <> + + + + ) +}