diff --git a/nextjs/package-lock.json b/nextjs/package-lock.json index aae164173..76e313446 100644 --- a/nextjs/package-lock.json +++ b/nextjs/package-lock.json @@ -77,13 +77,17 @@ "react-dom": "^18.2.0", "react-fast-marquee": "^1.6.4", "react-grid-layout-next": "^2.2.0", + "react-hexgrid": "^2.0.1", "react-hook-form": "^7.51.0", "react-loader-spinner": "^6.1.6", "react-map-gl": "^7.1.7", "react-quill": "^2.0.0", + "react-resizable-panels": "^2.1.7", "react-simple-code-editor": "^0.13.1", + "recharts": "^2.15.0", "slug": "^9.1.0", "sonner": "^1.4.3", + "swr": "^2.3.0", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", "theme-colors": "^0.1.0", @@ -7628,6 +7632,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, "node_modules/@types/d3-format": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", @@ -7635,11 +7657,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" + }, "node_modules/@types/d3-scale": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -7652,11 +7688,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, "node_modules/@types/d3-time": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/date-fns": { @@ -11263,6 +11313,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -12902,6 +12961,15 @@ "node": ">=12" } }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -12923,6 +12991,15 @@ "node": ">=12" } }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -12952,6 +13029,18 @@ "node": ">=12" } }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", @@ -12976,6 +13065,15 @@ "node": ">=12" } }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -13308,6 +13406,12 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/decode-uri-component": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", @@ -13569,6 +13673,14 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "license": "ISC" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -13647,6 +13759,16 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -15529,6 +15651,12 @@ "node": ">=0.10.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "node_modules/filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -21792,6 +21920,12 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -24159,6 +24293,38 @@ "node": ">=6" } }, + "node_modules/react-hexgrid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-hexgrid/-/react-hexgrid-2.0.1.tgz", + "integrity": "sha512-5yBYUAhagw3SNeqgyzcRblxtQQHZFabCaNHufX1q6eXwaGHQx2wEaX+gNgBjB4RpC6VIMOBCjGE7AUK43k2rkg==", + "dependencies": { + "classnames": "^2.3.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.13" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-hexgrid/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/react-hook-form": { "version": "7.54.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", @@ -24364,6 +24530,16 @@ "react": ">= 16.3" } }, + "node_modules/react-resizable-panels": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", + "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-simple-code-editor": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", @@ -24374,6 +24550,21 @@ "react-dom": "*" } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-spinners": { "version": "0.13.8", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", @@ -24407,6 +24598,22 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -24493,6 +24700,50 @@ "integrity": "sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg==", "license": "ISC" }, + "node_modules/recharts": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", + "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/reduce-object": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/reduce-object/-/reduce-object-0.1.3.tgz", @@ -26523,6 +26774,18 @@ "tslib": "^2.0.3" } }, + "node_modules/swr": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz", + "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -27992,6 +28255,28 @@ "dev": true, "license": "MIT" }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vinyl": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", diff --git a/nextjs/package.json b/nextjs/package.json index f3198f52d..126f79957 100644 --- a/nextjs/package.json +++ b/nextjs/package.json @@ -93,13 +93,17 @@ "react-dom": "^18.2.0", "react-fast-marquee": "^1.6.4", "react-grid-layout-next": "^2.2.0", + "react-hexgrid": "^2.0.1", "react-hook-form": "^7.51.0", "react-loader-spinner": "^6.1.6", "react-map-gl": "^7.1.7", "react-quill": "^2.0.0", + "react-resizable-panels": "^2.1.7", "react-simple-code-editor": "^0.13.1", + "recharts": "^2.15.0", "slug": "^9.1.0", "sonner": "^1.4.3", + "swr": "^2.3.0", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", "theme-colors": "^0.1.0", diff --git a/nextjs/src/app/globals.css b/nextjs/src/app/globals.css index 928a9d6b2..e82a71aa2 100644 --- a/nextjs/src/app/globals.css +++ b/nextjs/src/app/globals.css @@ -78,16 +78,16 @@ --labour: 4, 100%, 58%; --conservative: 222, 100%, 58%; --foreground: var(--meep-gray-200); - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card: var(--meep-gray-600); + --card-foreground: var(--meep-gray-200); --popover: var(--meep-gray-800); --popover-foreground: 0, 0%, 100%; --primary: 222, 14%, 34%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: var(--meep-gray-600); - --muted: 210 40% 96.1%; - --muted-foreground: 222, 14%, 64%; + --muted: var(--meep-gray-600); + --muted-foreground: var(--meep-gray-200); --accent: var(--meep-gray-600); --accent-foreground: var(--meep-gray-200); --border: 0, 0%, 100%, 0.32; @@ -111,6 +111,26 @@ --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; + --chart-1: 222 69% 65%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --chart-6: 220 70% 50%; + --chart-7: 160 60% 45%; + --chart-8: 30 80% 55%; + --chart-9: 280 65% 60%; + --chart-10: 340 75% 55%; + --chart-11: 220 70% 50%; + --chart-12: 160 60% 45%; + --chart-13: 30 80% 55%; + --chart-14: 280 65% 60%; + --chart-15: 340 75% 55%; + --chart-16: 220 70% 50%; + --chart-17: 160 60% 45%; + --chart-18: 30 80% 55%; + --chart-19: 280 65% 60%; + --chart-20: 340 75% 55%; } .dark { --sidebar-background: 240 5.9% 10%; @@ -121,6 +141,22 @@ --sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; + + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --chart-6: 220 70% 50%; + --chart-7: 160 60% 45%; + --chart-8: 30 80% 55%; + --chart-9: 280 65% 60%; + --chart-10: 340 75% 55%; + --chart-11: 220 70% 50%; + --chart-12: 160 60% 45%; + --chart-13: 30 80% 55%; + --chart-14: 280 65% 60%; + --chart-15: 340 75% 55%; } } diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index 345e6c848..166865f02 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -1,4 +1,5 @@ import { AnalyticalAreaType } from '@/__generated__/graphql' +import { INITIAL_VIEW_STATES } from '@/components/LocalisedMap' import { useLoadedMap } from '@/lib/map' import { useAtom } from 'jotai' import React, { useEffect } from 'react' @@ -66,6 +67,30 @@ const PoliticalChoropleths: React.FC = ({ } }, [map.loaded, dataByBoundary, report]) + // When the selected boundary changes, fly to it + useEffect(() => { + if (selectedBoundary) { + const coordinates = dataByBoundary.find((d) => d.gss === selectedBoundary) + ?.gssArea?.point?.geometry?.coordinates + map.loadedMap?.flyTo({ + center: (coordinates as [number, number]) || [ + INITIAL_VIEW_STATES.uk.longitude, + INITIAL_VIEW_STATES.uk.latitude, + ], + zoom: 11 || INITIAL_VIEW_STATES.uk.zoom, + }) + } + if (!selectedBoundary) { + map.loadedMap?.flyTo({ + center: [ + INITIAL_VIEW_STATES.uk.longitude, + INITIAL_VIEW_STATES.uk.latitude, + ], + zoom: INITIAL_VIEW_STATES.uk.zoom, + }) + } + }, [selectedBoundary]) + if (!map.loaded) return null if (!dataByBoundary || !tileset) return null diff --git a/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx b/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx index 560d2ec4c..a6f298393 100644 --- a/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MembersListPointMarkers.tsx @@ -1,7 +1,8 @@ 'use client' import { BACKEND_URL } from '@/env' -import { selectedSourceMarkerAtom, useLoadedMap } from '@/lib/map' +import { layerColour, selectedSourceMarkerAtom, useLoadedMap } from '@/lib/map' +import { Point } from 'geojson' import { useAtom } from 'jotai' import { MapMouseEvent } from 'mapbox-gl' import { useEffect } from 'react' @@ -43,8 +44,16 @@ export function MembersListPointMarkers({ const handleClick = (event: MapMouseEvent) => { const feature = event.features?.[0] - if (feature?.properties?.id) { - setSelectedSourceMarker(feature) + if (feature?.properties?.id && feature.geometry.type === 'Point') { + const pointGeometry = feature.geometry as Point + const [longitude, latitude] = pointGeometry.coordinates + if (longitude !== undefined && latitude !== undefined) { + setSelectedSourceMarker(feature) + map.flyTo({ + center: [longitude, latitude], + zoom: 16, + }) + } } } diff --git a/nextjs/src/app/reports/[id]/(components)/ReportDisplaySettings.tsx b/nextjs/src/app/reports/[id]/(components)/ReportDisplaySettings.tsx index 67a8aff48..38cdb3c04 100644 --- a/nextjs/src/app/reports/[id]/(components)/ReportDisplaySettings.tsx +++ b/nextjs/src/app/reports/[id]/(components)/ReportDisplaySettings.tsx @@ -8,22 +8,13 @@ import { isConstituencyPanelOpenAtom } from '@/lib/map' import { MixerHorizontalIcon } from '@radix-ui/react-icons' import { useAtomValue } from 'jotai' import React from 'react' -import { NAVBAR_HEIGHT } from './ReportNavbar' import ReportConfigLegacyControls from './_ReportConfigLegacyControls' const ReportDisplaySettings: React.FC = () => { const isConstituencyPanelOpen = useAtomValue(isConstituencyPanelOpenAtom) return ( -
+
-
+
+ +
+
+ +
) } diff --git a/nextjs/src/app/reports/[id]/(components)/ReportPage.tsx b/nextjs/src/app/reports/[id]/(components)/ReportPage.tsx index 71ee5a3e6..d7f0285f2 100644 --- a/nextjs/src/app/reports/[id]/(components)/ReportPage.tsx +++ b/nextjs/src/app/reports/[id]/(components)/ReportPage.tsx @@ -2,50 +2,100 @@ import LocalisedMap from '@/components/LocalisedMap' import { PlaceholderLayer } from '@/components/PlaceholderLayer' -import { ConstituenciesPanel } from './ConstituenciesPanel' +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from '@/components/ui/resizable' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { MapIcon } from 'lucide-react' +import { useRef } from 'react' +import { MapRef } from 'react-map-gl' +import ReportDashboard from '../dashboard/ReportDashboard' import PoliticalChoropleths from './MapLayers/PoliticalChoropleths' import ReportMapMarkers from './MapLayers/ReportMapMarkers' +import ReportHexMap from './ReportHexMap' import { useReport } from './ReportProvider' +import { ReportSidebarLeft } from './ReportSidebarLeft' export const PLACEHOLDER_LAYER_ID_CHOROPLETH = 'choropleths' export const PLACEHOLDER_LAYER_ID_MARKERS = 'markers' export default function ReportPage() { + const mapRef = useRef(null) const { report } = useReport() const boundaryType = report.displayOptions?.dataVisualisation?.boundaryType const tileset = report.politicalBoundaries.find( (boundary) => boundary.boundaryType === boundaryType )?.tileset + // Handle panel resize + const handlePanelResize = () => { + if (mapRef.current) { + // Force map resize after panel animation completes + setTimeout(() => { + mapRef.current?.resize() + }, 10) // matches transition duration + } + } + return (
- - - {/* We load and populate all available political boundaries first, then toggle their visibility later. + + +
+ + + + + GEO + + HEX + + + + + {/* We load and populate all available political boundaries first, then toggle their visibility later. This prevents re-rendering and re-initialisting the layers and re-calculating stats when a user just wants to change the visible boundary type */} - {/* {report.politicalBoundaries.map(({ boundaryType, tileset }) => ( */} - {tileset && boundaryType && ( - - )} - {/* ))} */} - - - + {/* {report.politicalBoundaries.map(({ boundaryType, tileset }) => ( */} + {tileset && boundaryType && ( + + )} + {/* ))} */} + + + + + + + + +
+
+ + + + +
-
) } diff --git a/nextjs/src/app/reports/[id]/(components)/ReportSidebarLeft.tsx b/nextjs/src/app/reports/[id]/(components)/ReportSidebarLeft.tsx index 7b2d6e00e..01f1d3740 100644 --- a/nextjs/src/app/reports/[id]/(components)/ReportSidebarLeft.tsx +++ b/nextjs/src/app/reports/[id]/(components)/ReportSidebarLeft.tsx @@ -14,9 +14,11 @@ import { ReportDataSources } from './ReportDataSources' import { NAVBAR_HEIGHT } from './ReportNavbar' import { useReport } from './ReportProvider' -const classes = { +export const TabTriggerClasses = { + tabsList: + 'w-full justify-start text-white rounded-none px-4 border border-b-meepGray-800 pt-4 pb-0 h-fit flex gap-6 overflow-x-auto scroll', tabsTrigger: - 'pb-2 bg-transparent px-0 data-[state=active]:bg-transparent data-[state=active]:text-white data-[state=active]:border-b border-white rounded-none', + 'pb-2 bg-transparent px-0 data-[state=active]:bg-transparent text-meepGray-400 border-b border-b-meepGray-600 data-[state=active]:text-white data-[state=active]:border-b-meepGray-200 hover:border-b hover:border-meepGray-400 rounded-none', } export function ReportSidebarLeft() { @@ -30,7 +32,7 @@ export function ReportSidebarLeft() { style={{ top: NAVBAR_HEIGHT + 'px', }} - className="border border-r-meepGray-800" + className="border border-r-meepGray-800 " > - + Data Sources {} @@ -62,7 +61,7 @@ export function ReportSidebarLeft() { Configuration diff --git a/nextjs/src/app/reports/[id]/(components)/reportsConstituencyItem.tsx b/nextjs/src/app/reports/[id]/(components)/reportsConstituencyItem.tsx index 68d6df4cb..f4f65f837 100644 --- a/nextjs/src/app/reports/[id]/(components)/reportsConstituencyItem.tsx +++ b/nextjs/src/app/reports/[id]/(components)/reportsConstituencyItem.tsx @@ -16,10 +16,11 @@ import { import { Card, CardContent, + CardDescription, CardFooter, CardHeader, CardTitle, -} from '@/components/ui/card' +} from '@/components/ChartCard' import { Tooltip, TooltipContent, @@ -76,128 +77,154 @@ export const ConstituencyElectionDeepDive = ({ .filter((l) => !!l.source.importedDataCountForConstituency?.count) return ( -
-

- {data.constituency.name} -

- {data.constituency.mp && showMPs && ( -
-
- MP -
- -
- )} - {!!data.constituency.lastElection && showLastElectionData && ( -
-
- {/* First and second parties */} -
-
- 1st in {getYear(data.constituency.lastElection.stats.date)} -
-
- {data.constituency.lastElection.stats.firstPartyResult.party} -
-
-
-
-
-
+ + + {data.constituency.name} + + {data.mapReport.importedDataCountForConstituency?.count ? ( + <> + {format(',')( + data.mapReport.importedDataCountForConstituency?.count + )}{' '} + {pluralize( + 'member', + data.mapReport.importedDataCountForConstituency?.count || 0 + )} + + ) : ( +
You have no member data in this constituency
+ )} +
+
+ + {data.constituency.mp && showMPs && ( +
+
+ MP +
+ +
+ )} + {!!data.constituency.lastElection && showLastElectionData && ( +
+
+ {/* First and second parties */} +
- 2nd in {getYear(data.constituency.lastElection.stats.date)} + 1st in {getYear(data.constituency.lastElection.stats.date)}
- {data.constituency.lastElection.stats.secondPartyResult.party} + {data.constituency.lastElection.stats.firstPartyResult.party}
+
+
+
-
-
-
-
- {/* Voting stats */} -
-
- Majority -
-
- {format(',')(data.constituency.lastElection.stats.majority)} -
-
-
-
- Swing to lose -
-
- {format('.2%')( - data.constituency.lastElection.stats.majority / - data.constituency.lastElection.stats.electorate - )} -
-
-
-
- Electorate -
-
- {format(',')(data.constituency.lastElection.stats.electorate)} -
-
-
-
Turnout
-
- {format('.2%')( - data.constituency.lastElection.stats.validVotes / - data.constituency.lastElection.stats.electorate - )} -
-
+
+
+ 2nd in {getYear(data.constituency.lastElection.stats.date)} +
+
+ { + data.constituency.lastElection.stats.secondPartyResult + .party + } +
+
+
+
+
+
+ {/* Voting stats */} +
+
+ Majority +
+
+ {format(',')(data.constituency.lastElection.stats.majority)} +
+
+
+
+ Swing to lose +
+
+ {format('.2%')( + data.constituency.lastElection.stats.majority / + data.constituency.lastElection.stats.electorate + )} +
+
+
+
+ Electorate +
+
+ {format(',')(data.constituency.lastElection.stats.electorate)} +
+
+
+
+ Turnout +
+
+ {format('.2%')( + data.constituency.lastElection.stats.validVotes / + data.constituency.lastElection.stats.electorate + )} +
+
+
- - )} - {!!data.mapReport.importedDataCountForConstituency && ( - - )} -
+ )} + {!!data.mapReport.importedDataCountForConstituency && ( + + )} + + ) } diff --git a/nextjs/src/app/reports/[id]/dashboard/ReportDashboard.tsx b/nextjs/src/app/reports/[id]/dashboard/ReportDashboard.tsx new file mode 100644 index 000000000..72217089f --- /dev/null +++ b/nextjs/src/app/reports/[id]/dashboard/ReportDashboard.tsx @@ -0,0 +1,203 @@ +import { + ConstituencyStatsOverviewQuery, + ConstituencyStatsOverviewQueryVariables, +} from '@/__generated__/graphql' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { gql, useQuery } from '@apollo/client' +import { useAtom } from 'jotai' +import { + CalendarIcon, + FileIcon, + MapPinIcon, + MessageSquareIcon, + UsersIcon, +} from 'lucide-react' +import { useReport } from '../(components)/ReportProvider' +import { TabTriggerClasses } from '../(components)/ReportSidebarLeft' +import { ConstituencyElectionDeepDive } from '../(components)/reportsConstituencyItem' +import { selectedBoundaryAtom } from '../useSelectBoundary' +import ReportDashboardChat from './ReportDashboardChat' +import ReportDashboardList from './ReportDashboardList' +import ReportDashboardMPs from './ReportDashboardMPs' +import ReportDashboardMemberCount from './ReportDashboardMemberCount' +import ReportDashboardMembersOverTime from './ReportDashboardMembersOverTime' +import ReportMembers from './ReportMembers' + +const IconClasses = 'w-4 h-4 stroke-meepGray-400 stroke-1 mr-1' + +const dashboardTabItems = [ + { + label: 'Overview', + value: 'overview', + bold: true, + }, + { + label: 'Members', + value: 'members', + icon: , + }, + { + label: 'Locations', + value: 'locations', + icon: , + }, + { + label: 'Groups', + value: 'groups', + icon: , + }, + { + label: 'Events', + value: 'events', + icon: , + }, + { + label: 'Articles ', + value: 'articles', + icon: , + }, + { + label: 'Chat', + value: 'chat', + icon: , + }, +] + +export default function ReportDashboard() { + const [selectedBoundary, setSelectedBoundary] = useAtom(selectedBoundaryAtom) + const { + report: { + id, + displayOptions: { + dataVisualisation: { + boundaryType: analyticalAreaType, + dataSource, + } = {}, + } = {}, + }, + } = useReport() + + const constituencyAnalytics = useQuery< + ConstituencyStatsOverviewQuery, + ConstituencyStatsOverviewQueryVariables + >(CONSTITUENCY_STATS_OVERVIEW, { + variables: { + reportID: id, + analyticalAreaType: analyticalAreaType!, + layerIds: [dataSource!], + }, + }) + + const constituencies = + constituencyAnalytics.data?.mapReport.importedDataCountByConstituency + .filter((constituency) => constituency.gssArea) + .sort((a, b) => b.count - a.count) // Sort by count in descending order + + if (constituencyAnalytics.loading) { + return
Loading...
+ } + + return ( +
+ + + {dashboardTabItems.map((item) => ( + + {item.icon} + {item.label} + + ))} + + +
+ {constituencies && !selectedBoundary && ( + <> + + + + + + )} + {selectedBoundary && analyticalAreaType && ( + + )} +
+
+ + + + Foodbanks Data goes here + Groups Data goes here + Events Data goes here + Articles Data goes here + + + +
+
+ ) +} + +// Same query from TopConstituencies +const CONSTITUENCY_STATS_OVERVIEW = gql` + query ConstituencyStatsOverview( + $reportID: ID! + $analyticalAreaType: AnalyticalAreaType! + $layerIds: [String!]! + ) { + mapReport(pk: $reportID) { + id + importedDataCountByConstituency: importedDataCountByArea( + analyticalAreaType: $analyticalAreaType + layerIds: $layerIds + ) { + label + gss + count + gssArea { + id + name + fitBounds + mp: person(filters: { personType: "MP" }) { + id + name + photo { + url + } + party: personDatum(filters: { dataType_Name: "party" }) { + name: data + } + } + lastElection { + stats { + date + majority + electorate + firstPartyResult { + party + shade + votes + } + secondPartyResult { + party + shade + votes + } + } + } + } + } + } + } +` diff --git a/nextjs/src/app/reports/[id]/dashboard/ReportDashboardChat.tsx b/nextjs/src/app/reports/[id]/dashboard/ReportDashboardChat.tsx new file mode 100644 index 000000000..47886631e --- /dev/null +++ b/nextjs/src/app/reports/[id]/dashboard/ReportDashboardChat.tsx @@ -0,0 +1,18 @@ +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import { MessageSquareIcon } from 'lucide-react' + +export default function ReportDashboardChat() { + return ( +
+
+ +

Ask AI about your Data

+
+
+