phase 3: dashboard UI — layout, map, markers, region overlays, interactions
This commit is contained in:
parent
64f7099603
commit
c33257092d
163
bun.lock
163
bun.lock
@ -5,9 +5,11 @@
|
||||
"": {
|
||||
"name": "bonus4",
|
||||
"dependencies": {
|
||||
"@googlemaps/markerclusterer": "^2.6.2",
|
||||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@total-typescript/ts-reset": "^0.6.1",
|
||||
"@vis.gl/react-google-maps": "^1.7.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -18,6 +20,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.5.6",
|
||||
"prisma": "^7.3.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"recharts": "^3.7.0",
|
||||
@ -145,6 +148,16 @@
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@googlemaps/markerclusterer": ["@googlemaps/markerclusterer@2.6.2", "", { "dependencies": { "@types/supercluster": "^7.1.3", "fast-equals": "^5.2.2", "supercluster": "^8.0.1" } }, "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
@ -271,6 +284,126 @@
|
||||
|
||||
"@prisma/studio-core": ["@prisma/studio-core@0.13.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="],
|
||||
|
||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
"@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
|
||||
|
||||
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||
|
||||
"@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
|
||||
|
||||
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
|
||||
|
||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
||||
|
||||
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
|
||||
|
||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
|
||||
|
||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||
|
||||
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
|
||||
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
||||
|
||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||
|
||||
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
|
||||
|
||||
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||
|
||||
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
||||
|
||||
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
|
||||
|
||||
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
|
||||
|
||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||
|
||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
|
||||
|
||||
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
|
||||
|
||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||
|
||||
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||
|
||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
|
||||
|
||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||
|
||||
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||
|
||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||
|
||||
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
|
||||
|
||||
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
|
||||
|
||||
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
|
||||
|
||||
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
|
||||
|
||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||
|
||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||
|
||||
"@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
|
||||
|
||||
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
||||
|
||||
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
|
||||
|
||||
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
@ -313,6 +446,8 @@
|
||||
|
||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.4", "", { "dependencies": { "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ=="],
|
||||
|
||||
"@total-typescript/ts-reset": ["@total-typescript/ts-reset@0.6.1", "", {}, "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
@ -335,6 +470,8 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/google.maps": ["@types/google.maps@3.58.1", "", {}, "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
@ -347,6 +484,8 @@
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="],
|
||||
@ -381,6 +520,8 @@
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
||||
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
||||
@ -507,6 +648,8 @@
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.4", "", {}, "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw=="],
|
||||
@ -585,6 +728,8 @@
|
||||
|
||||
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
@ -625,6 +770,8 @@
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
|
||||
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
@ -761,6 +908,8 @@
|
||||
|
||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||
|
||||
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
@ -939,6 +1088,8 @@
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
|
||||
|
||||
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
@ -949,6 +1100,12 @@
|
||||
|
||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="],
|
||||
@ -1041,6 +1198,8 @@
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||
|
||||
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
|
||||
|
||||
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@ -1093,6 +1252,10 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
|
||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
|
||||
|
||||
109
next-shim.d.ts
vendored
Normal file
109
next-shim.d.ts
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
|
||||
// shim any nextjs modules that don't behave well with nodenext in here
|
||||
|
||||
// see: file://./node_modules/next/dynamic.d.ts
|
||||
declare module 'next/dynamic' {
|
||||
import React from 'react';
|
||||
type ComponentModule<P = {}> = {
|
||||
default: React.ComponentType<P>;
|
||||
};
|
||||
export declare type LoaderComponent<P = {}> = Promise<React.ComponentType<P> | ComponentModule<P>>;
|
||||
export declare type Loader<P = {}> = (() => LoaderComponent<P>) | LoaderComponent<P>;
|
||||
export type LoaderMap = {
|
||||
[module: string]: () => Loader<any>;
|
||||
};
|
||||
export type LoadableGeneratedOptions = {
|
||||
webpack?(): any;
|
||||
modules?(): LoaderMap;
|
||||
};
|
||||
export type DynamicOptionsLoadingProps = {
|
||||
error?: Error | null;
|
||||
isLoading?: boolean;
|
||||
pastDelay?: boolean;
|
||||
retry?: () => void;
|
||||
timedOut?: boolean;
|
||||
};
|
||||
export type DynamicOptions<P = {}> = LoadableGeneratedOptions & {
|
||||
loading?: (loadingProps: DynamicOptionsLoadingProps) => React.ReactNode;
|
||||
loader?: Loader<P> | LoaderMap;
|
||||
loadableGenerated?: LoadableGeneratedOptions;
|
||||
ssr?: boolean;
|
||||
};
|
||||
export type LoadableOptions<P = {}> = DynamicOptions<P>;
|
||||
export type LoadableFn<P = {}> = (opts: LoadableOptions<P>) => React.ComponentType<P>;
|
||||
export type LoadableComponent<P = {}> = React.ComponentType<P>;
|
||||
export declare function noSSR<P = {}>(
|
||||
LoadableInitializer: LoadableFn<P>,
|
||||
loadableOptions: DynamicOptions<P>,
|
||||
): React.ComponentType<P>;
|
||||
/**
|
||||
* This function lets you dynamically import a component.
|
||||
* It uses [React.lazy()](https://react.dev/reference/react/lazy) with [Suspense](https://react.dev/reference/react/Suspense) under the hood.
|
||||
*
|
||||
* Read more: [Next.js Docs: `next/dynamic`](https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading#nextdynamic)
|
||||
*/
|
||||
export default function dynamic<P = {}>(
|
||||
dynamicOptions: DynamicOptions<P> | Loader<P>,
|
||||
options?: DynamicOptions<P>,
|
||||
): React.ComponentType<P>;
|
||||
export {};
|
||||
}
|
||||
|
||||
// see: file://./node_modules/next/link.d.ts
|
||||
declare module 'next/link' {
|
||||
import React from 'react';
|
||||
import type { UrlObject } from 'url';
|
||||
type Url = string | UrlObject;
|
||||
type OnNavigateEventHandler = (event: { preventDefault: () => void }) => void;
|
||||
type InternalLinkProps = {
|
||||
href: Url;
|
||||
as?: Url;
|
||||
replace?: boolean;
|
||||
scroll?: boolean;
|
||||
shallow?: boolean;
|
||||
passHref?: boolean;
|
||||
prefetch?: boolean | 'auto' | null;
|
||||
locale?: string | false;
|
||||
legacyBehavior?: boolean;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
onTouchStart?: React.TouchEventHandler<HTMLAnchorElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
onNavigate?: OnNavigateEventHandler;
|
||||
};
|
||||
export interface LinkProps<RouteInferType = any> extends InternalLinkProps {}
|
||||
declare const Link: React.ForwardRefExoticComponent<
|
||||
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps<any>> &
|
||||
LinkProps<any> & {
|
||||
children?: React.ReactNode | undefined;
|
||||
} & React.RefAttributes<HTMLAnchorElement>
|
||||
>;
|
||||
export declare const useLinkStatus: () => { pending: boolean };
|
||||
export default Link;
|
||||
}
|
||||
|
||||
// see: file://./node_modules/next/error.d.ts
|
||||
declare module 'next/error' {
|
||||
import React from 'react';
|
||||
import type { NextPageContext } from '../shared/lib/utils';
|
||||
export type ErrorProps = {
|
||||
statusCode: number;
|
||||
hostname?: string;
|
||||
title?: string;
|
||||
withDarkMode?: boolean;
|
||||
};
|
||||
declare function _getInitialProps({ req, res, err }: NextPageContext): Promise<ErrorProps> | ErrorProps;
|
||||
/**
|
||||
* `Error` component used for handling errors.
|
||||
*/
|
||||
export default class Error<P = {}> extends React.Component<P & ErrorProps> {
|
||||
static displayName: string;
|
||||
static getInitialProps: typeof _getInitialProps;
|
||||
static origGetInitialProps: typeof _getInitialProps;
|
||||
render(): import('react/jsx-runtime').JSX.Element;
|
||||
}
|
||||
export {};
|
||||
}
|
||||
@ -35,9 +35,11 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@googlemaps/markerclusterer": "^2.6.2",
|
||||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@total-typescript/ts-reset": "^0.6.1",
|
||||
"@vis.gl/react-google-maps": "^1.7.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -48,6 +50,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.5.6",
|
||||
"prisma": "^7.3.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"recharts": "^3.7.0",
|
||||
|
||||
9
prisma/sql/getAllDatacentersWithLocation.sql
Normal file
9
prisma/sql/getAllDatacentersWithLocation.sql
Normal file
@ -0,0 +1,9 @@
|
||||
SELECT
|
||||
d.id, d.name, d.operator, d.capacity_mw, d.status, d.year_opened,
|
||||
d.region_id,
|
||||
ST_AsGeoJSON(d.location)::TEXT as location_geojson,
|
||||
r.code as region_code,
|
||||
r.name as region_name
|
||||
FROM datacenters d
|
||||
JOIN grid_regions r ON d.region_id = r.id
|
||||
ORDER BY d.capacity_mw DESC
|
||||
1
reset.d.ts
vendored
Normal file
1
reset.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
import '@total-typescript/ts-reset';
|
||||
@ -1,6 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { findDatacentersInRegion, findNearbyDatacenters } from '@/generated/prisma/sql.js';
|
||||
import {
|
||||
findDatacentersInRegion,
|
||||
findNearbyDatacenters,
|
||||
getAllDatacentersWithLocation,
|
||||
} from '@/generated/prisma/sql.js';
|
||||
import { prisma } from '@/lib/db.js';
|
||||
import { serialize } from '@/lib/superjson.js';
|
||||
|
||||
@ -57,6 +61,18 @@ export async function fetchDatacentersInRegion(
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllDatacentersWithLocation(): Promise<ActionResult<getAllDatacentersWithLocation.Result[]>> {
|
||||
try {
|
||||
const rows = await prisma.$queryRawTyped(getAllDatacentersWithLocation());
|
||||
return { ok: true, data: serialize(rows) };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Failed to fetch datacenters with locations: ${err instanceof Error ? err.message : String(err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchNearbyDatacenters(
|
||||
lat: number,
|
||||
lng: number,
|
||||
|
||||
22
src/app/demand/page.tsx
Normal file
22
src/app/demand/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Demand Analysis | Energy & AI Dashboard',
|
||||
description: 'Regional electricity demand growth, peak tracking, and datacenter load impact',
|
||||
};
|
||||
|
||||
export default function DemandPage() {
|
||||
return (
|
||||
<div className="px-6 py-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Demand Analysis</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Regional demand growth trends, peak demand tracking, and estimated datacenter load as a percentage of regional
|
||||
demand.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex h-96 items-center justify-center rounded-lg border border-dashed border-border">
|
||||
<p className="text-sm text-muted-foreground">Charts coming in Phase 4</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/app/generation/page.tsx
Normal file
22
src/app/generation/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Generation Mix | Energy & AI Dashboard',
|
||||
description: 'Generation by fuel type per region, renewable vs fossil splits, and carbon intensity',
|
||||
};
|
||||
|
||||
export default function GenerationPage() {
|
||||
return (
|
||||
<div className="px-6 py-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Generation Mix</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Stacked area charts showing generation by fuel type per region, with renewable vs fossil comparisons and carbon
|
||||
intensity indicators.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex h-96 items-center justify-center rounded-lg border border-dashed border-border">
|
||||
<p className="text-sm text-muted-foreground">Charts coming in Phase 4</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { Toaster } from 'sonner';
|
||||
|
||||
import { Footer } from '@/components/layout/footer.js';
|
||||
import { Nav } from '@/components/layout/nav.js';
|
||||
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@ -12,9 +16,11 @@ export const metadata: Metadata = {
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="min-h-screen antialiased">
|
||||
<body className="flex min-h-screen flex-col antialiased">
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false} disableTransitionOnChange>
|
||||
{children}
|
||||
<Nav />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
<Toaster theme="dark" richColors position="bottom-right" />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
||||
102
src/app/map/page.tsx
Normal file
102
src/app/map/page.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { fetchAllDatacentersWithLocation } from '@/actions/datacenters.js';
|
||||
import { fetchPriceHeatmapData } from '@/actions/prices.js';
|
||||
import type { DatacenterMarkerData } from '@/components/map/datacenter-marker.js';
|
||||
import { EnergyMapLoader } from '@/components/map/energy-map-loader.js';
|
||||
import type { RegionHeatmapData } from '@/components/map/region-overlay.js';
|
||||
import type { getAllDatacentersWithLocation, getRegionPriceHeatmap } from '@/generated/prisma/sql.js';
|
||||
import { deserialize } from '@/lib/superjson.js';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Interactive Map | Energy & AI Dashboard',
|
||||
description: 'Datacenter locations, grid region overlays, and real-time electricity price heatmap',
|
||||
};
|
||||
|
||||
interface GeoJsonPoint {
|
||||
type: string;
|
||||
coordinates: [number, number];
|
||||
}
|
||||
|
||||
function isGeoJsonPoint(val: unknown): val is GeoJsonPoint {
|
||||
if (typeof val !== 'object' || val === null || !('coordinates' in val)) return false;
|
||||
const obj = val as Record<string, unknown>;
|
||||
const coords = obj['coordinates'];
|
||||
if (!Array.isArray(coords) || coords.length < 2) return false;
|
||||
return typeof coords[0] === 'number' && typeof coords[1] === 'number';
|
||||
}
|
||||
|
||||
function parseLocationGeoJson(geojson: string | null): { lat: number; lng: number } | null {
|
||||
if (!geojson) return null;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(geojson);
|
||||
if (isGeoJsonPoint(parsed)) {
|
||||
return { lat: parsed.coordinates[1], lng: parsed.coordinates[0] };
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseBoundaryGeoJson(geojsonStr: string | null): object | null {
|
||||
if (!geojsonStr) return null;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(geojsonStr);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default async function MapPage() {
|
||||
const [dcResult, priceResult] = await Promise.all([fetchAllDatacentersWithLocation(), fetchPriceHeatmapData()]);
|
||||
|
||||
const datacenters: DatacenterMarkerData[] = [];
|
||||
if (dcResult.ok) {
|
||||
const rows = deserialize<getAllDatacentersWithLocation.Result[]>(dcResult.data);
|
||||
for (const row of rows) {
|
||||
const loc = parseLocationGeoJson(row.location_geojson);
|
||||
if (loc) {
|
||||
datacenters.push({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
operator: row.operator,
|
||||
capacity_mw: row.capacity_mw,
|
||||
status: row.status,
|
||||
year_opened: row.year_opened,
|
||||
region_id: row.region_id,
|
||||
region_code: row.region_code,
|
||||
region_name: row.region_name,
|
||||
lat: loc.lat,
|
||||
lng: loc.lng,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const regions: RegionHeatmapData[] = [];
|
||||
if (priceResult.ok) {
|
||||
const rows = deserialize<getRegionPriceHeatmap.Result[]>(priceResult.data);
|
||||
for (const row of rows) {
|
||||
regions.push({
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
boundaryGeoJson: parseBoundaryGeoJson(row.boundary_geojson),
|
||||
avgPrice: row.avg_price,
|
||||
maxPrice: row.max_price,
|
||||
avgDemand: row.avg_demand,
|
||||
datacenterCount: row.datacenter_count,
|
||||
totalDcCapacityMw: row.total_dc_capacity_mw,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-3.5rem-3rem)]">
|
||||
<EnergyMapLoader datacenters={datacenters} regions={regions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
src/app/page.tsx
162
src/app/page.tsx
@ -1,8 +1,160 @@
|
||||
export default function DashboardHome() {
|
||||
import { MetricCard } from '@/components/dashboard/metric-card.js';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js';
|
||||
import { deserialize } from '@/lib/superjson.js';
|
||||
import { Activity, ArrowRight, BarChart3, Droplets, Flame, Map, Server } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { fetchDatacenters } from '@/actions/datacenters.js';
|
||||
import { fetchRegionDemandSummary } from '@/actions/demand.js';
|
||||
import { fetchLatestCommodityPrices, fetchLatestPrices } from '@/actions/prices.js';
|
||||
|
||||
function formatNumber(value: number, decimals = 1): string {
|
||||
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(decimals)}M`;
|
||||
if (value >= 1_000) return `${(value / 1_000).toFixed(decimals)}K`;
|
||||
return value.toFixed(decimals);
|
||||
}
|
||||
|
||||
export default async function DashboardHome() {
|
||||
const [pricesResult, commoditiesResult, datacentersResult, demandResult] = await Promise.all([
|
||||
fetchLatestPrices(),
|
||||
fetchLatestCommodityPrices(),
|
||||
fetchDatacenters(),
|
||||
fetchRegionDemandSummary(),
|
||||
]);
|
||||
|
||||
const prices = pricesResult.ok
|
||||
? deserialize<
|
||||
Array<{
|
||||
price_mwh: number;
|
||||
demand_mw: number;
|
||||
region_code: string;
|
||||
region_name: string;
|
||||
timestamp: Date;
|
||||
}>
|
||||
>(pricesResult.data)
|
||||
: [];
|
||||
|
||||
const commodities = commoditiesResult.ok
|
||||
? deserialize<Array<{ commodity: string; price: number; unit: string; timestamp: Date }>>(commoditiesResult.data)
|
||||
: [];
|
||||
|
||||
const datacenters = datacentersResult.ok
|
||||
? deserialize<Array<{ id: string; capacityMw: number }>>(datacentersResult.data)
|
||||
: [];
|
||||
|
||||
const demandRows = demandResult.ok
|
||||
? deserialize<Array<{ avg_demand: number | null; region_code: string }>>(demandResult.data)
|
||||
: [];
|
||||
|
||||
const avgPrice = prices.length > 0 ? prices.reduce((sum, p) => sum + p.price_mwh, 0) / prices.length : 0;
|
||||
|
||||
const natGas = commodities.find(c => c.commodity === 'natural_gas');
|
||||
const wtiCrude = commodities.find(c => c.commodity === 'wti_crude');
|
||||
|
||||
const totalCapacityMw = datacenters.reduce((sum, dc) => sum + dc.capacityMw, 0);
|
||||
|
||||
const datacenterCount = datacenters.length;
|
||||
|
||||
const avgDemand =
|
||||
demandRows.length > 0 ? demandRows.reduce((sum, r) => sum + (r.avg_demand ?? 0), 0) / demandRows.length : 0;
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight">Energy & AI Dashboard</h1>
|
||||
<p className="mt-4 text-muted-foreground">Phase 1 scaffold complete. Dashboard coming soon.</p>
|
||||
</main>
|
||||
<div className="px-6 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Real-time overview of AI datacenter energy impact across US grid regions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<MetricCard
|
||||
title="Avg Electricity Price"
|
||||
value={avgPrice > 0 ? `$${avgPrice.toFixed(2)}` : '--'}
|
||||
unit="/MWh"
|
||||
icon={BarChart3}
|
||||
/>
|
||||
<MetricCard title="Total DC Capacity" value={formatNumber(totalCapacityMw)} unit="MW" icon={Activity} />
|
||||
<MetricCard title="Datacenters Tracked" value={datacenterCount.toLocaleString()} icon={Server} />
|
||||
<MetricCard
|
||||
title="Natural Gas Spot"
|
||||
value={natGas ? `$${natGas.price.toFixed(2)}` : '--'}
|
||||
unit={natGas?.unit ?? '/MMBtu'}
|
||||
icon={Flame}
|
||||
/>
|
||||
<MetricCard
|
||||
title="WTI Crude Oil"
|
||||
value={wtiCrude ? `$${wtiCrude.price.toFixed(2)}` : '--'}
|
||||
unit={wtiCrude?.unit ?? '/bbl'}
|
||||
icon={Droplets}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-4 lg:grid-cols-2">
|
||||
<Link href="/map">
|
||||
<Card className="group cursor-pointer transition-colors hover:border-primary/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Map className="h-5 w-5 text-chart-1" />
|
||||
Interactive Map
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Explore datacenter locations, grid region overlays, and real-time price heatmaps.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary transition-transform group-hover:translate-x-1">
|
||||
View Map <ArrowRight className="h-4 w-4" />
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5 text-chart-2" />
|
||||
Recent Prices by Region
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{prices.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{prices.map(p => (
|
||||
<div key={p.region_code} className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">{p.region_name}</span>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="font-mono font-semibold">${p.price_mwh.toFixed(2)}</span>
|
||||
<span className="text-xs text-muted-foreground">/MWh</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No price data available yet.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{avgDemand > 0 && (
|
||||
<div className="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5 text-chart-3" />
|
||||
Demand Summary (7-day avg)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Average regional demand:{' '}
|
||||
<span className="font-mono font-semibold text-foreground">{formatNumber(avgDemand)} MW</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
21
src/app/trends/page.tsx
Normal file
21
src/app/trends/page.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Price Trends | Energy & AI Dashboard',
|
||||
description: 'Regional electricity price trends, commodity overlays, and AI milestone annotations',
|
||||
};
|
||||
|
||||
export default function TrendsPage() {
|
||||
return (
|
||||
<div className="px-6 py-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Price Trends</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Multi-line regional electricity price charts with commodity overlays and AI milestone annotations.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex h-96 items-center justify-center rounded-lg border border-dashed border-border">
|
||||
<p className="text-sm text-muted-foreground">Charts coming in Phase 4</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/components/dashboard/metric-card.tsx
Normal file
30
src/components/dashboard/metric-card.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js';
|
||||
import { cn } from '@/lib/utils.js';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface MetricCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
icon: LucideIcon;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MetricCard({ title, value, unit, icon: Icon, className }: MetricCardProps) {
|
||||
return (
|
||||
<Card className={cn('gap-0 py-4', className)}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||
<Icon className="h-4 w-4" />
|
||||
{title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="text-3xl font-bold tracking-tight">{value}</span>
|
||||
{unit && <span className="text-sm text-muted-foreground">{unit}</span>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
9
src/components/layout/footer.tsx
Normal file
9
src/components/layout/footer.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border/40 py-4">
|
||||
<div className="px-6 text-center text-xs text-muted-foreground">
|
||||
For educational and informational purposes only. Not financial advice.
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
56
src/components/layout/nav.tsx
Normal file
56
src/components/layout/nav.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils.js';
|
||||
import { Activity, BarChart3, Flame, LayoutDashboard, Map, TrendingUp } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation.js';
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '/map', label: 'Map', icon: Map },
|
||||
{ href: '/trends', label: 'Trends', icon: TrendingUp },
|
||||
{ href: '/demand', label: 'Demand', icon: Activity },
|
||||
{ href: '/generation', label: 'Generation', icon: Flame },
|
||||
] as const;
|
||||
|
||||
export function Nav() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
{/* Ticker tape slot — Phase 5 */}
|
||||
<div id="ticker-tape-slot" />
|
||||
|
||||
<div className="flex h-14 items-center px-6">
|
||||
<Link href="/" className="mr-8 flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5 text-chart-1" />
|
||||
<span className="text-lg font-semibold tracking-tight">Energy & AI Dashboard</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-1">
|
||||
{NAV_LINKS.map(({ href, label, icon: Icon }) => {
|
||||
const isActive = href === '/' ? pathname === '/' : pathname.startsWith(href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground',
|
||||
)}>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Auto-refresh countdown slot — Phase 5 */}
|
||||
<div id="auto-refresh-slot" className="ml-auto" />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
53
src/components/map/datacenter-detail-panel.tsx
Normal file
53
src/components/map/datacenter-detail-panel.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet.js';
|
||||
import type { DatacenterMarkerData } from './datacenter-marker.js';
|
||||
|
||||
interface DatacenterDetailPanelProps {
|
||||
datacenter: DatacenterMarkerData | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DatacenterDetailPanel({ datacenter, onClose }: DatacenterDetailPanelProps) {
|
||||
return (
|
||||
<Sheet open={datacenter !== null} onOpenChange={open => !open && onClose()}>
|
||||
<SheetContent side="right" className="overflow-y-auto bg-zinc-950 text-zinc-100">
|
||||
{datacenter && (
|
||||
<>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-zinc-100">{datacenter.name}</SheetTitle>
|
||||
<SheetDescription className="text-zinc-400">
|
||||
{datacenter.operator} · {datacenter.region_name} ({datacenter.region_code})
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<MetricItem label="Capacity" value={`${datacenter.capacity_mw} MW`} />
|
||||
<MetricItem label="Status" value={datacenter.status} />
|
||||
<MetricItem label="Year Opened" value={String(datacenter.year_opened)} />
|
||||
<MetricItem label="Grid Region" value={datacenter.region_code} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-3">
|
||||
<div className="mb-1 text-xs font-medium text-zinc-500">Location</div>
|
||||
<div className="font-mono text-sm text-zinc-300">
|
||||
{datacenter.lat.toFixed(4)}, {datacenter.lng.toFixed(4)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricItem({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-3">
|
||||
<div className="mb-0.5 text-xs font-medium text-zinc-500">{label}</div>
|
||||
<div className="text-sm font-semibold text-zinc-200">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
src/components/map/datacenter-marker.tsx
Normal file
91
src/components/map/datacenter-marker.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { AdvancedMarker } from '@vis.gl/react-google-maps';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const OPERATOR_COLORS: Record<string, string> = {
|
||||
AWS: '#FF9900',
|
||||
Google: '#4285F4',
|
||||
Microsoft: '#00A4EF',
|
||||
Meta: '#0668E1',
|
||||
Oracle: '#F80000',
|
||||
Equinix: '#ED1C24',
|
||||
'Digital Realty': '#0072CE',
|
||||
CoreWeave: '#7B2FBE',
|
||||
QTS: '#00B388',
|
||||
CyrusOne: '#003B5C',
|
||||
NTT: '#E60012',
|
||||
Vantage: '#0099D8',
|
||||
};
|
||||
|
||||
function getOperatorColor(operator: string): string {
|
||||
return OPERATOR_COLORS[operator] ?? '#6B7280';
|
||||
}
|
||||
|
||||
function getMarkerSize(capacityMw: number): number {
|
||||
if (capacityMw >= 500) return 32;
|
||||
if (capacityMw >= 200) return 26;
|
||||
if (capacityMw >= 100) return 22;
|
||||
if (capacityMw >= 50) return 18;
|
||||
return 14;
|
||||
}
|
||||
|
||||
export interface DatacenterMarkerData {
|
||||
id: string;
|
||||
name: string;
|
||||
operator: string;
|
||||
capacity_mw: number;
|
||||
status: string;
|
||||
year_opened: number;
|
||||
region_id: string;
|
||||
region_code: string;
|
||||
region_name: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
interface DatacenterMarkerProps {
|
||||
datacenter: DatacenterMarkerData;
|
||||
onClick: (datacenter: DatacenterMarkerData) => void;
|
||||
}
|
||||
|
||||
export function DatacenterMarker({ datacenter, onClick }: DatacenterMarkerProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const size = getMarkerSize(datacenter.capacity_mw);
|
||||
const color = getOperatorColor(datacenter.operator);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(datacenter);
|
||||
}, [datacenter, onClick]);
|
||||
|
||||
return (
|
||||
<AdvancedMarker
|
||||
position={{ lat: datacenter.lat, lng: datacenter.lng }}
|
||||
onClick={handleClick}
|
||||
title={`${datacenter.name} (${datacenter.operator}) - ${datacenter.capacity_mw} MW`}>
|
||||
<div
|
||||
className="relative flex cursor-pointer items-center justify-center transition-transform duration-150"
|
||||
style={{ transform: hovered ? 'scale(1.3)' : 'scale(1)' }}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}>
|
||||
<div
|
||||
className="rounded-full border-2 border-white/80 shadow-lg"
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor: color,
|
||||
boxShadow: hovered ? `0 0 12px ${color}80` : `0 2px 4px rgba(0,0,0,0.3)`,
|
||||
}}
|
||||
/>
|
||||
{hovered && (
|
||||
<div className="absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 rounded-md bg-zinc-900/95 px-3 py-1.5 text-xs whitespace-nowrap text-zinc-100 shadow-xl">
|
||||
<div className="font-semibold">{datacenter.name}</div>
|
||||
<div className="text-zinc-400">
|
||||
{datacenter.operator} · {datacenter.capacity_mw} MW
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdvancedMarker>
|
||||
);
|
||||
}
|
||||
24
src/components/map/energy-map-loader.tsx
Normal file
24
src/components/map/energy-map-loader.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import type { DatacenterMarkerData } from './datacenter-marker.js';
|
||||
import type { RegionHeatmapData } from './region-overlay.js';
|
||||
|
||||
const EnergyMap = dynamic(() => import('./energy-map.js').then(m => m.EnergyMap), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex h-full w-full items-center justify-center bg-zinc-950">
|
||||
<div className="text-sm text-zinc-500">Loading map...</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
interface EnergyMapLoaderProps {
|
||||
datacenters: DatacenterMarkerData[];
|
||||
regions: RegionHeatmapData[];
|
||||
}
|
||||
|
||||
export function EnergyMapLoader({ datacenters, regions }: EnergyMapLoaderProps) {
|
||||
return <EnergyMap datacenters={datacenters} regions={regions} />;
|
||||
}
|
||||
72
src/components/map/energy-map.tsx
Normal file
72
src/components/map/energy-map.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { APIProvider, Map } from '@vis.gl/react-google-maps';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DatacenterDetailPanel } from './datacenter-detail-panel.js';
|
||||
import { DatacenterMarker, type DatacenterMarkerData } from './datacenter-marker.js';
|
||||
import { MapControls } from './map-controls.js';
|
||||
import { RegionDetailPanel } from './region-detail-panel.js';
|
||||
import { RegionOverlay, type RegionHeatmapData } from './region-overlay.js';
|
||||
|
||||
const US_CENTER = { lat: 39.8, lng: -98.5 };
|
||||
const DEFAULT_ZOOM = 4;
|
||||
|
||||
interface EnergyMapProps {
|
||||
datacenters: DatacenterMarkerData[];
|
||||
regions: RegionHeatmapData[];
|
||||
}
|
||||
|
||||
export function EnergyMap({ datacenters, regions }: EnergyMapProps) {
|
||||
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? '';
|
||||
const mapId = process.env.NEXT_PUBLIC_GOOGLE_MAP_ID ?? '';
|
||||
|
||||
const [filteredDatacenters, setFilteredDatacenters] = useState(datacenters);
|
||||
const [selectedDatacenter, setSelectedDatacenter] = useState<DatacenterMarkerData | null>(null);
|
||||
const [selectedRegion, setSelectedRegion] = useState<RegionHeatmapData | null>(null);
|
||||
|
||||
const handleDatacenterClick = useCallback((dc: DatacenterMarkerData) => {
|
||||
setSelectedDatacenter(dc);
|
||||
setSelectedRegion(null);
|
||||
}, []);
|
||||
|
||||
const handleRegionClick = useCallback(
|
||||
(regionCode: string) => {
|
||||
const region = regions.find(r => r.code === regionCode);
|
||||
if (region) {
|
||||
setSelectedRegion(region);
|
||||
setSelectedDatacenter(null);
|
||||
}
|
||||
},
|
||||
[regions],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback((filtered: DatacenterMarkerData[]) => {
|
||||
setFilteredDatacenters(filtered);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<APIProvider apiKey={apiKey}>
|
||||
<div className="relative h-full w-full">
|
||||
<MapControls datacenters={datacenters} onFilterChange={handleFilterChange} />
|
||||
|
||||
<Map
|
||||
mapId={mapId}
|
||||
defaultCenter={US_CENTER}
|
||||
defaultZoom={DEFAULT_ZOOM}
|
||||
gestureHandling="greedy"
|
||||
disableDefaultUI={false}
|
||||
className="h-full w-full">
|
||||
<RegionOverlay regions={regions} onRegionClick={handleRegionClick} />
|
||||
|
||||
{filteredDatacenters.map(dc => (
|
||||
<DatacenterMarker key={dc.id} datacenter={dc} onClick={handleDatacenterClick} />
|
||||
))}
|
||||
</Map>
|
||||
|
||||
<DatacenterDetailPanel datacenter={selectedDatacenter} onClose={() => setSelectedDatacenter(null)} />
|
||||
|
||||
<RegionDetailPanel region={selectedRegion} datacenters={datacenters} onClose={() => setSelectedRegion(null)} />
|
||||
</div>
|
||||
</APIProvider>
|
||||
);
|
||||
}
|
||||
110
src/components/map/map-controls.tsx
Normal file
110
src/components/map/map-controls.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils.js';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { DatacenterMarkerData } from './datacenter-marker.js';
|
||||
|
||||
interface MapControlsProps {
|
||||
datacenters: DatacenterMarkerData[];
|
||||
onFilterChange: (filtered: DatacenterMarkerData[]) => void;
|
||||
}
|
||||
|
||||
export function MapControls({ datacenters, onFilterChange }: MapControlsProps) {
|
||||
const operators = useMemo(() => {
|
||||
const set = new Set(datacenters.map(d => d.operator));
|
||||
return Array.from(set).sort();
|
||||
}, [datacenters]);
|
||||
|
||||
const [selectedOperators, setSelectedOperators] = useState<Set<string>>(new Set());
|
||||
const [minCapacity, setMinCapacity] = useState(0);
|
||||
|
||||
const applyFilters = useCallback(
|
||||
(ops: Set<string>, minCap: number) => {
|
||||
const filtered = datacenters.filter(d => {
|
||||
if (ops.size > 0 && !ops.has(d.operator)) return false;
|
||||
if (d.capacity_mw < minCap) return false;
|
||||
return true;
|
||||
});
|
||||
onFilterChange(filtered);
|
||||
},
|
||||
[datacenters, onFilterChange],
|
||||
);
|
||||
|
||||
const toggleOperator = useCallback(
|
||||
(op: string) => {
|
||||
setSelectedOperators(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(op)) {
|
||||
next.delete(op);
|
||||
} else {
|
||||
next.add(op);
|
||||
}
|
||||
applyFilters(next, minCapacity);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[applyFilters, minCapacity],
|
||||
);
|
||||
|
||||
const handleCapacityChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = Number(e.target.value);
|
||||
setMinCapacity(val);
|
||||
applyFilters(selectedOperators, val);
|
||||
},
|
||||
[applyFilters, selectedOperators],
|
||||
);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
setSelectedOperators(new Set());
|
||||
setMinCapacity(0);
|
||||
onFilterChange(datacenters);
|
||||
}, [datacenters, onFilterChange]);
|
||||
|
||||
const hasFilters = selectedOperators.size > 0 || minCapacity > 0;
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 left-4 z-10 flex max-h-[calc(100vh-12rem)] w-64 flex-col gap-3 overflow-y-auto rounded-lg border border-zinc-700/60 bg-zinc-900/90 p-3 shadow-xl backdrop-blur">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold tracking-wider text-zinc-400 uppercase">Filters</span>
|
||||
{hasFilters && (
|
||||
<button onClick={clearFilters} className="text-xs text-blue-400 hover:text-blue-300">
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-zinc-400">Operator</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{operators.map(op => (
|
||||
<button
|
||||
key={op}
|
||||
onClick={() => toggleOperator(op)}
|
||||
className={cn(
|
||||
'rounded-md px-2 py-0.5 text-xs font-medium transition-colors',
|
||||
selectedOperators.has(op)
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-300',
|
||||
)}>
|
||||
{op}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-zinc-400">Min Capacity: {minCapacity} MW</label>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={500}
|
||||
step={10}
|
||||
value={minCapacity}
|
||||
onChange={handleCapacityChange}
|
||||
className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-zinc-700 accent-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
src/components/map/region-detail-panel.tsx
Normal file
83
src/components/map/region-detail-panel.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet.js';
|
||||
import type { DatacenterMarkerData } from './datacenter-marker.js';
|
||||
import type { RegionHeatmapData } from './region-overlay.js';
|
||||
|
||||
interface RegionDetailPanelProps {
|
||||
region: RegionHeatmapData | null;
|
||||
datacenters: DatacenterMarkerData[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RegionDetailPanel({ region, datacenters, onClose }: RegionDetailPanelProps) {
|
||||
const regionDatacenters = region
|
||||
? datacenters.filter(dc => dc.region_code === region.code).sort((a, b) => b.capacity_mw - a.capacity_mw)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Sheet open={region !== null} onOpenChange={open => !open && onClose()}>
|
||||
<SheetContent side="right" className="overflow-y-auto bg-zinc-950 text-zinc-100">
|
||||
{region && (
|
||||
<>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-zinc-100">{region.name}</SheetTitle>
|
||||
<SheetDescription className="text-zinc-400">Grid region {region.code}</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<MetricItem
|
||||
label="Avg Price (24h)"
|
||||
value={region.avgPrice !== null ? `$${region.avgPrice.toFixed(2)}/MWh` : 'No data'}
|
||||
/>
|
||||
<MetricItem
|
||||
label="Max Price (24h)"
|
||||
value={region.maxPrice !== null ? `$${region.maxPrice.toFixed(2)}/MWh` : 'No data'}
|
||||
/>
|
||||
<MetricItem
|
||||
label="Avg Demand"
|
||||
value={region.avgDemand !== null ? `${Math.round(region.avgDemand).toLocaleString()} MW` : 'No data'}
|
||||
/>
|
||||
<MetricItem label="Datacenters" value={String(region.datacenterCount ?? 0)} />
|
||||
<MetricItem
|
||||
label="Total DC Capacity"
|
||||
value={
|
||||
region.totalDcCapacityMw !== null
|
||||
? `${Math.round(region.totalDcCapacityMw).toLocaleString()} MW`
|
||||
: '0 MW'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{regionDatacenters.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold text-zinc-300">Datacenters in Region</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{regionDatacenters.map(dc => (
|
||||
<div key={dc.id} className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-3">
|
||||
<div className="text-sm font-medium text-zinc-200">{dc.name}</div>
|
||||
<div className="mt-0.5 text-xs text-zinc-500">
|
||||
{dc.operator} · {dc.capacity_mw} MW · {dc.status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricItem({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-3">
|
||||
<div className="mb-0.5 text-xs font-medium text-zinc-500">{label}</div>
|
||||
<div className="text-sm font-semibold text-zinc-200">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
151
src/components/map/region-overlay.tsx
Normal file
151
src/components/map/region-overlay.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
'use client';
|
||||
|
||||
import { useMap } from '@vis.gl/react-google-maps';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export interface RegionHeatmapData {
|
||||
code: string;
|
||||
name: string;
|
||||
boundaryGeoJson: object | null;
|
||||
avgPrice: number | null;
|
||||
maxPrice: number | null;
|
||||
avgDemand: number | null;
|
||||
datacenterCount: number | null;
|
||||
totalDcCapacityMw: number | null;
|
||||
}
|
||||
|
||||
interface RegionOverlayProps {
|
||||
regions: RegionHeatmapData[];
|
||||
onRegionClick: (regionCode: string) => void;
|
||||
}
|
||||
|
||||
function priceToColor(price: number | null): string {
|
||||
if (price === null || price <= 0) return 'rgba(100, 100, 100, 0.25)';
|
||||
|
||||
// Scale: 0-20 cool blue, 20-50 yellow/orange, 50+ red/magenta
|
||||
const clamped = Math.min(price, 100);
|
||||
const ratio = clamped / 100;
|
||||
|
||||
if (ratio < 0.2) {
|
||||
// Blue to cyan
|
||||
const t = ratio / 0.2;
|
||||
return `rgba(${Math.round(30 + t * 30)}, ${Math.round(80 + t * 140)}, ${Math.round(220 - t * 20)}, 0.30)`;
|
||||
} else if (ratio < 0.5) {
|
||||
// Cyan to yellow/orange
|
||||
const t = (ratio - 0.2) / 0.3;
|
||||
return `rgba(${Math.round(60 + t * 195)}, ${Math.round(220 - t * 60)}, ${Math.round(200 - t * 170)}, 0.35)`;
|
||||
} else {
|
||||
// Orange to red/magenta
|
||||
const t = (ratio - 0.5) / 0.5;
|
||||
return `rgba(${Math.round(255 - t * 35)}, ${Math.round(160 - t * 120)}, ${Math.round(30 + t * 80)}, 0.40)`;
|
||||
}
|
||||
}
|
||||
|
||||
function priceToBorderColor(price: number | null): string {
|
||||
if (price === null || price <= 0) return 'rgba(150, 150, 150, 0.4)';
|
||||
|
||||
const clamped = Math.min(price, 100);
|
||||
const ratio = clamped / 100;
|
||||
|
||||
if (ratio < 0.3) return 'rgba(59, 130, 246, 0.6)';
|
||||
if (ratio < 0.6) return 'rgba(245, 158, 11, 0.6)';
|
||||
return 'rgba(239, 68, 68, 0.6)';
|
||||
}
|
||||
|
||||
export function RegionOverlay({ regions, onRegionClick }: RegionOverlayProps) {
|
||||
const map = useMap();
|
||||
const dataLayerRef = useRef<google.maps.Data | null>(null);
|
||||
const listenersRef = useRef<google.maps.MapsEventListener[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
// Clean up previous layer
|
||||
if (dataLayerRef.current) {
|
||||
dataLayerRef.current.setMap(null);
|
||||
dataLayerRef.current = null;
|
||||
}
|
||||
for (const listener of listenersRef.current) {
|
||||
listener.remove();
|
||||
}
|
||||
listenersRef.current = [];
|
||||
|
||||
const dataLayer = new google.maps.Data({ map });
|
||||
dataLayerRef.current = dataLayer;
|
||||
|
||||
// Build price lookup
|
||||
const priceMap = new Map<string, RegionHeatmapData>();
|
||||
for (const region of regions) {
|
||||
if (region.boundaryGeoJson) {
|
||||
priceMap.set(region.code, region);
|
||||
try {
|
||||
const geoJsonFeature = {
|
||||
type: 'Feature' as const,
|
||||
geometry: region.boundaryGeoJson,
|
||||
properties: { code: region.code, name: region.name },
|
||||
};
|
||||
dataLayer.addGeoJson(geoJsonFeature);
|
||||
} catch {
|
||||
// Skip invalid GeoJSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Style features by price
|
||||
dataLayer.setStyle(feature => {
|
||||
const rawCode = feature.getProperty('code');
|
||||
const code = typeof rawCode === 'string' ? rawCode : '';
|
||||
const regionData = priceMap.get(code);
|
||||
const price = regionData?.avgPrice ?? null;
|
||||
|
||||
return {
|
||||
fillColor: priceToColor(price),
|
||||
fillOpacity: 1,
|
||||
strokeColor: priceToBorderColor(price),
|
||||
strokeWeight: 1.5,
|
||||
strokeOpacity: 0.8,
|
||||
zIndex: 1,
|
||||
};
|
||||
});
|
||||
|
||||
// Hover highlight
|
||||
const overListener = dataLayer.addListener('mouseover', (event: google.maps.Data.MouseEvent) => {
|
||||
if (event.feature) {
|
||||
dataLayer.overrideStyle(event.feature, {
|
||||
strokeWeight: 3,
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 1,
|
||||
zIndex: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
listenersRef.current.push(overListener);
|
||||
|
||||
const outListener = dataLayer.addListener('mouseout', (event: google.maps.Data.MouseEvent) => {
|
||||
if (event.feature) {
|
||||
dataLayer.revertStyle(event.feature);
|
||||
}
|
||||
});
|
||||
listenersRef.current.push(outListener);
|
||||
|
||||
// Click handler
|
||||
const clickListener = dataLayer.addListener('click', (event: google.maps.Data.MouseEvent) => {
|
||||
const rawCode = event.feature?.getProperty('code');
|
||||
if (typeof rawCode === 'string' && rawCode) {
|
||||
onRegionClick(rawCode);
|
||||
}
|
||||
});
|
||||
listenersRef.current.push(clickListener);
|
||||
|
||||
return () => {
|
||||
dataLayer.setMap(null);
|
||||
for (const listener of listenersRef.current) {
|
||||
listener.remove();
|
||||
}
|
||||
listenersRef.current = [];
|
||||
dataLayerRef.current = null;
|
||||
};
|
||||
}, [map, regions, onRegionClick]);
|
||||
|
||||
return null;
|
||||
}
|
||||
56
src/components/ui/card.tsx
Normal file
56
src/components/ui/card.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils.js';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn('flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="card-title" className={cn('leading-none font-semibold', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="card-description" className={cn('text-sm text-muted-foreground', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="card-content" className={cn('px-6', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div data-slot="card-footer" className={cn('flex items-center px-6 [.border-t]:pt-6', className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||
106
src/components/ui/sheet.tsx
Normal file
106
src/components/ui/sheet.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { Dialog as SheetPrimitive } from 'radix-ui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils.js';
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = 'right',
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
'fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500',
|
||||
side === 'right' &&
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
side === 'left' &&
|
||||
'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
side === 'top' &&
|
||||
'inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
side === 'bottom' &&
|
||||
'inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="sheet-header" className={cn('flex flex-col gap-1.5 p-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="sheet-footer" className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn('font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger };
|
||||
@ -24,11 +24,7 @@
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"isolatedModules": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "esnext",
|
||||
@ -41,9 +37,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@/*": ["./src/*"],
|
||||
"next/font": ["./node_modules/next/font/index.d.ts"],
|
||||
"next/font/local": ["./node_modules/next/font/local/index.d.ts"],
|
||||
"next/font/google": ["./node_modules/next/font/google/index.d.ts"],
|
||||
@ -55,15 +49,13 @@
|
||||
"next.config.ts",
|
||||
"prisma.config.ts",
|
||||
"prisma/seed.ts",
|
||||
"next-shim.d.ts",
|
||||
"scripts/**/*.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
".next/dev/types/**/*.ts",
|
||||
"reset.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"dist"
|
||||
]
|
||||
"exclude": ["node_modules", ".next", "dist"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user