From ff07111edd7b4f0942903d486072669e251147a7 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 8 Apr 2026 05:13:59 +0000 Subject: [PATCH] improving the solar ui and adding a map --- next.config.js | 2 +- package-lock.json | 381 ++++++++++++- package.json | 3 + .../[propertyId]/solar-analysis/page.tsx | 515 ++++++++++++------ 4 files changed, 695 insertions(+), 206 deletions(-) diff --git a/next.config.js b/next.config.js index 60882170..a1b6a71c 100644 --- a/next.config.js +++ b/next.config.js @@ -9,7 +9,7 @@ const nextConfig = { ], }, allowedDevOrigins: ['local-origin.dev', '*.local-origin.dev'], - reactStrictMode: true, + reactStrictMode: false, }; // use next-axiom for full stack monitoring diff --git a/package-lock.json b/package-lock.json index 115a8d52..7b251ec5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", + "@react-google-maps/api": "^2.20.8", "@remixicon/react": "^4.2.0", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-query": "^4.29.12", @@ -48,6 +49,7 @@ "esbuild": "^0.25.8", "eslint-config-next": "13.4.3", "framer-motion": "^12.23.24", + "geotiff": "^3.0.5", "lucide-react": "^0.233.0", "next": "^15.5.7", "next-auth": "^4.22.1", @@ -60,6 +62,7 @@ "react-confetti": "^6.4.0", "react-dom": "18.3.1", "react-hook-form": "^7.53.2", + "recharts": "^3.8.1", "tailwind-merge": "^1.13.2", "tailwindcss": "^3.4.3", "tailwindcss-animate": "^1.0.6", @@ -2300,6 +2303,22 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@googlemaps/js-api-loader": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz", + "integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==", + "license": "Apache-2.0" + }, + "node_modules/@googlemaps/markerclusterer": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz", + "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==", + "license": "Apache-2.0", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "supercluster": "^8.0.1" + } + }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -3209,6 +3228,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "license": "MIT" + }, "node_modules/@puppeteer/browsers": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", @@ -4460,6 +4485,36 @@ "node": ">=6" } }, + "node_modules/@react-google-maps/api": { + "version": "2.20.8", + "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.8.tgz", + "integrity": "sha512-wtLYFtCGXK3qbIz1H5to3JxbosPnKsvjDKhqGylXUb859EskhzR7OpuNt0LqdLarXUtZCJTKzPn3BNaekNIahg==", + "license": "MIT", + "dependencies": { + "@googlemaps/js-api-loader": "1.16.8", + "@googlemaps/markerclusterer": "2.5.3", + "@react-google-maps/infobox": "2.20.0", + "@react-google-maps/marker-clusterer": "2.20.0", + "@types/google.maps": "3.58.1", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19", + "react-dom": "^16.8 || ^17 || ^18 || ^19" + } + }, + "node_modules/@react-google-maps/infobox": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.20.0.tgz", + "integrity": "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==", + "license": "MIT" + }, + "node_modules/@react-google-maps/marker-clusterer": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.20.0.tgz", + "integrity": "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==", + "license": "MIT" + }, "node_modules/@react-stately/flags": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", @@ -4490,6 +4545,42 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remixicon/react": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@remixicon/react/-/react-4.8.0.tgz", @@ -5247,7 +5338,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@swc/helpers": { @@ -5524,6 +5620,50 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@tremor/react/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@tremor/react/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/@tremor/react/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/@tremor/react/node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "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/@tremor/react/node_modules/tailwind-merge": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", @@ -5534,6 +5674,28 @@ "url": "https://github.com/sponsors/dcastil" } }, + "node_modules/@tremor/react/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/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -5614,6 +5776,12 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -5700,6 +5868,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -8379,6 +8553,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -8952,9 +9136,9 @@ "license": "MIT" }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/events": { @@ -9437,6 +9621,25 @@ "node": ">= 0.4" } }, + "node_modules/geotiff": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-3.0.5.tgz", + "integrity": "sha512-OWcL9S9+yDZ6iAlXMt32T1iwUApJM8UiD47xbm6ZP1h33d10fqkPs14EG/ttT5EnefpZSx3G15iDFC5FxUNUwA==", + "license": "MIT", + "dependencies": { + "@petamoriken/float16": "^3.9.3", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.5.0", + "xml-utils": "^1.10.2", + "zstddec": "^0.2.0" + }, + "engines": { + "node": ">=10.19" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -9880,6 +10083,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -9965,6 +10178,15 @@ "node": ">=12" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -10657,6 +10879,12 @@ "node": ">=4.0" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10694,6 +10922,12 @@ "node": "> 0.8" } }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "license": "Apache-2.0" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11629,6 +11863,12 @@ "node": ">= 14" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11641,6 +11881,12 @@ "node": ">=6" } }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -12352,6 +12598,18 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -12426,9 +12684,31 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -12561,26 +12841,33 @@ } }, "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "@reduxjs/toolkit": "^1.9.0 || 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" }, "engines": { - "node": ">=14" + "node": ">=18" }, "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" + "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" } }, "node_modules/recharts-scale": { @@ -12601,12 +12888,21 @@ "node": ">=6" } }, - "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==", + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12675,6 +12971,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -13609,6 +13911,15 @@ "node": ">= 6" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -14905,9 +15216,9 @@ } }, "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==", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -14946,6 +15257,12 @@ "node": ">=20.0.0" } }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -15168,6 +15485,12 @@ "node": ">=0.8" } }, + "node_modules/xml-utils": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", + "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==", + "license": "CC0-1.0" + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", @@ -15275,6 +15598,12 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zstddec": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0.tgz", + "integrity": "sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==", + "license": "MIT AND BSD-3-Clause" } } } diff --git a/package.json b/package.json index e5fabc67..e01c7ac5 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", + "@react-google-maps/api": "^2.20.8", "@remixicon/react": "^4.2.0", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-query": "^4.29.12", @@ -54,6 +55,7 @@ "esbuild": "^0.25.8", "eslint-config-next": "13.4.3", "framer-motion": "^12.23.24", + "geotiff": "^3.0.5", "lucide-react": "^0.233.0", "next": "^15.5.7", "next-auth": "^4.22.1", @@ -66,6 +68,7 @@ "react-confetti": "^6.4.0", "react-dom": "18.3.1", "react-hook-form": "^7.53.2", + "recharts": "^3.8.1", "tailwind-merge": "^1.13.2", "tailwindcss": "^3.4.3", "tailwindcss-animate": "^1.0.6", diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/solar-analysis/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/solar-analysis/page.tsx index 7f876b8f..701c4a13 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/solar-analysis/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/solar-analysis/page.tsx @@ -1,39 +1,201 @@ import { - FlagIcon, - LightBulbIcon, SunIcon, - InformationCircleIcon, - CloudIcon, - SparklesIcon, BoltIcon, - CurrencyDollarIcon, - ArrowTrendingUpIcon, - HomeIcon, + ClockIcon, + Squares2X2Icon, + SparklesIcon, + MapPinIcon, + CalendarIcon, + GlobeAltIcon, } from "@heroicons/react/24/outline"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/app/shadcn_components/ui/card"; +import { Badge } from "@/app/shadcn_components/ui/badge"; +import { Separator } from "@/app/shadcn_components/ui/separator"; import { getPropertyMeta } from "../utils"; import { getSolarData, getSolarScenarioData } from "./utils"; -import FeatureTable from "@/app/components/building-passport/FeatureTable"; -import { roofSegmentsColumns } from "./roof-segments-table"; -import { formatNumber } from "@/app/utils"; +import PropertyMapWrapper from "./PropertyMapWrapper"; +import SolarSimulationWrapper from "./SolarSimulationWrapper"; -export default async function SolarAnalysisPage( - props: { - params: Promise<{ slug: string; propertyId: string }>; - } -) { +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function getDirectionLabel(az: number): { label: string; short: string } { + // Standard compass: 0/360=N, 90=E, 180=S, 270=W + const norm = ((az % 360) + 360) % 360; + if (norm >= 337.5 || norm < 22.5) return { short: "N", label: "North" }; + if (norm < 67.5) return { short: "NE", label: "North-East" }; + if (norm < 112.5) return { short: "E", label: "East" }; + if (norm < 157.5) return { short: "SE", label: "South-East" }; + if (norm < 202.5) return { short: "S", label: "South" }; + if (norm < 247.5) return { short: "SW", label: "South-West" }; + if (norm < 292.5) return { short: "W", label: "West" }; + return { short: "NW", label: "North-West" }; +} + +// Warm = south-facing (best solar); cool = north-facing (worst) +const SEGMENT_THEMES: Record = { + S: { gradient: "from-amber-50 to-orange-50/60", border: "border-amber-200/80", badge: "bg-amber-100 text-amber-800 border-amber-300", label: "text-amber-900", dot: "bg-amber-400" }, + SE: { gradient: "from-orange-50 to-amber-50/60", border: "border-orange-200/80", badge: "bg-orange-100 text-orange-800 border-orange-300", label: "text-orange-900", dot: "bg-orange-400" }, + SW: { gradient: "from-orange-50 to-amber-50/60", border: "border-orange-200/80", badge: "bg-orange-100 text-orange-800 border-orange-300", label: "text-orange-900", dot: "bg-orange-400" }, + E: { gradient: "from-sky-50 to-blue-50/40", border: "border-sky-200/80", badge: "bg-sky-100 text-sky-800 border-sky-300", label: "text-sky-900", dot: "bg-sky-400" }, + W: { gradient: "from-sky-50 to-blue-50/40", border: "border-sky-200/80", badge: "bg-sky-100 text-sky-800 border-sky-300", label: "text-sky-900", dot: "bg-sky-400" }, + N: { gradient: "from-slate-50 to-gray-50/40", border: "border-slate-200/80", badge: "bg-slate-100 text-slate-700 border-slate-300", label: "text-slate-800", dot: "bg-slate-400" }, + NE: { gradient: "from-slate-50 to-gray-50/40", border: "border-slate-200/80", badge: "bg-slate-100 text-slate-700 border-slate-300", label: "text-slate-800", dot: "bg-slate-400" }, + NW: { gradient: "from-slate-50 to-gray-50/40", border: "border-slate-200/80", badge: "bg-slate-100 text-slate-700 border-slate-300", label: "text-slate-800", dot: "bg-slate-400" }, +}; + +// ── Sub-components ──────────────────────────────────────────────────────────── + +function PageSection({ title, description, children }: { + title: string; + description: string; + children: React.ReactNode; +}) { + return ( +
+
+

{title}

+

{description}

+
+ {children} +
+ ); +} + +function InputRow({ + icon, + label, + value, +}: { + icon: React.ReactNode; + label: string; + value: React.ReactNode; +}) { + return ( +
+
+ + {icon} + + {label} +
+ {value} +
+ ); +} + +function RoofSegmentCard({ + index, + azimuthDegrees, + pitchDegrees, + areaMeters2, + groundAreaMeters2, + sunshineQuantiles, + planeHeightAtCenterMeters, +}: { + index: number; + azimuthDegrees: number; + pitchDegrees: number; + areaMeters2: number; + groundAreaMeters2: number; + sunshineQuantiles: number[]; + planeHeightAtCenterMeters: number; +}) { + const dir = getDirectionLabel(azimuthDegrees); + const theme = SEGMENT_THEMES[dir.short] ?? SEGMENT_THEMES["N"]; + const medianSunshine = sunshineQuantiles?.[4] ?? null; + const peakSunshine = sunshineQuantiles?.[8] ?? null; + + return ( +
+ {/* Card header strip */} +
+
+
+

+ Segment {index + 1} +

+

+ {dir.label} +

+
+
+ + {dir.short} +
+
+ + {/* Sunshine bar */} + {medianSunshine !== null && ( +
+
+ Median sunshine + {Math.round(medianSunshine)} hrs/yr +
+
+
+
+
+ )} +
+ + + + {/* Stats grid */} +
+ {[ + { label: "Roof area", value: `${areaMeters2.toFixed(1)} m²` }, + { label: "Ground area", value: `${groundAreaMeters2.toFixed(1)} m²` }, + { label: "Pitch", value: `${pitchDegrees.toFixed(1)}°` }, + { label: "Azimuth", value: `${azimuthDegrees.toFixed(1)}°` }, + { label: "Height", value: `${planeHeightAtCenterMeters.toFixed(1)} m` }, + peakSunshine !== null + ? { label: "Peak (P90)", value: `${Math.round(peakSunshine)} hrs` } + : { label: "", value: "" }, + ].map(({ label, value }, i) => + label ? ( +
+
{label}
+
{value}
+
+ ) :
+ )} +
+
+ ); +} + +// ── Page ────────────────────────────────────────────────────────────────────── + +export default async function SolarAnalysisPage(props: { + params: Promise<{ slug: string; propertyId: string }>; +}) { const params = await props.params; const propertyMeta = await getPropertyMeta(params.propertyId); const solarData = await getSolarData(Number(propertyMeta.uprn)); - // If there's no solar data, we cannot display the page if (!solarData) { return ( -
-
-
- No Solar Analysis Data Available +
+
+
+
-

Please check back later for updates.

+

No Solar Data Available

+

Please check back later for updates.

); @@ -49,176 +211,171 @@ export default async function SolarAnalysisPage( maxSunshineHoursPerYear, carbonOffsetFactorKgPerMwh, roofSegmentStats, + solarPanelConfigs, + maxArrayPanelsCount, + maxArrayAreaMeters2, } = solarData.googleApiResponse.solarPotential; - const getDirectionFromAzimuth = (azimuth: number): string => { - if ((azimuth >= 330 && azimuth <= 360) || (azimuth >= 0 && azimuth < 30)) - return "N"; - if (azimuth >= 30 && azimuth < 60) return "NE"; - if (azimuth >= 60 && azimuth < 120) return "E"; - if (azimuth >= 120 && azimuth < 150) return "SE"; - if (azimuth >= 150 && azimuth < 210) return "S"; - if (azimuth >= 210 && azimuth < 240) return "SW"; - if (azimuth >= 240 && azimuth < 300) return "W"; - if (azimuth >= 300 && azimuth < 330) return "NW"; - return ""; - }; + const buildingCenter = solarData.googleApiResponse.center; + const { imageryQuality, imageryDate, regionCode } = solarData.googleApiResponse; + const imageryDateStr = `${imageryDate.year}-${String(imageryDate.month).padStart(2, "0")}-${String(imageryDate.day).padStart(2, "0")}`; - const transformedRoofSegmentStats = roofSegmentStats.map( - ({ segmentIndex, stats, center, azimuthDegrees, ...rest }) => ({ - ...rest, - areaMeters2: stats.areaMeters2.toFixed(1), - groundAreaMeters2: stats.groundAreaMeters2.toFixed(1), - azimuthDegrees: azimuthDegrees.toFixed(1), - center: { - latitude: center.latitude.toFixed(6), - longitude: center.longitude.toFixed(6), - }, - sunshineQuantiles: stats.sunshineQuantiles.join(", "), // Join sunshineQuantiles into a string - direction: getDirectionFromAzimuth(azimuthDegrees), - }) + const qualityColors: Record = { + HIGH: "bg-emerald-50 text-emerald-700 border-emerald-200", + MEDIUM: "bg-amber-50 text-amber-700 border-amber-200", + LOW: "bg-gray-50 text-gray-600 border-gray-200", + }; + const qualityBadge = qualityColors[imageryQuality] ?? qualityColors.LOW; + const qualityText = imageryQuality === "HIGH" ? "High quality" + : imageryQuality === "MEDIUM" ? "Medium quality" + : "Base quality"; + + const maxAnnualKwh = Math.round( + solarPanelConfigs[solarPanelConfigs.length - 1].yearlyEnergyDcKwh ); return ( -
-
-
- - {solarScenarioData.scenrioType === "building" - ? "These figures are for the building as a whole" - : "These figures are for the individual property"} -
+
-
-

- Simulation Panel Configuration -

-
-
- -

- Dimensions: {panelWidthMeters}m x {panelHeightMeters}m -

-
-
- -

- Wattage: {panelCapacityWatts}W -

-
-
- -

- Lifetime: {panelLifetimeYears} years -

-
-
- -

- Region: {solarData.googleApiResponse.regionCode} -

+ {/* ── Hero header ─────────────────────────────────────────────────── */} +
+
+
+
+
+

+ Solar Potential Analysis +

+ + {qualityText} +
+

+ Ara uses high-resolution aerial imagery and rooftop geometry data to estimate + suitable solar PV packages for retrofit plans.{" "} + {solarScenarioData.scenrioType === "building" + ? "Figures are for the building as a whole." + : "Figures are for this individual unit."} +

-
-

- Weather and Environmental Data -

-
-
- -

- Max Sunshine Hours per Year: {maxSunshineHoursPerYear} hours -

-
-
- -

- Carbon Offset Factor: {carbonOffsetFactorKgPerMwh} kg/MWh -

-
-
-
- -
-

- Roof Segments Data -

- -
- -
-
-
-

- Solar PV Simulation -

-
    -
  • - - Number of panels: {solarScenarioData.numberPanels} -
  • -
  • - - Array output: {solarScenarioData.arrayKwhp} kWp -
  • -
  • - - Lifetime DC energy:{" "} - {Math.round(solarScenarioData.lifetimeDcKwh)} kWh -
  • -
  • - - Yearly DC energy: {Math.round( - solarScenarioData.yearlyDcKwh - )}{" "} - kWh -
  • - {solarScenarioData.lifetimeAcKwh !== null && ( -
  • - - Lifetime AC energy:{" "} - {Math.round(solarScenarioData.lifetimeAcKwh)} kWh -
  • - )} - {solarScenarioData.yearlyAcKwh !== null && ( -
  • - - Yearly AC energy:{" "} - {Math.round(solarScenarioData.yearlyAcKwh)} kWh -
  • - )} -
  • - - Cost: £{formatNumber(solarScenarioData.cost)} -
  • -
  • - - Expected payback years:{" "} - {solarScenarioData.expectedPaybackYears} -
  • -
  • - - Panelled roof area:{" "} - {solarScenarioData.panelledRoofArea.toFixed(1)} m² -
  • -
-
-
-
Map not currently available
- {/* Solar Image */} -
-
+ {/* Key summary pills */} +
+ {[ + { icon: , text: regionCode }, + { icon: , text: `Imagery: ${imageryDateStr}` }, + { icon: , text: `Up to ${maxAnnualKwh.toLocaleString()} kWh/yr` }, + { icon: , text: `Up to ${maxArrayPanelsCount} panels` }, + ].map(({ icon, text }, i) => ( + + {icon} + {text} + + ))}
+ + {/* ── Map + Inputs ─────────────────────────────────────────────────── */} + +
+ + {/* Map */} +
+ +
+ + {/* Inputs card */} + + + Analysis Parameters +

+ Inputs used to model solar output for this property. +

+
+ +
+ } + label="Panel dimensions" + value={`${panelWidthMeters} m × ${panelHeightMeters} m`} + /> + } + label="Panel capacity" + value={`${panelCapacityWatts} W`} + /> + } + label="Panel lifetime" + value={`${panelLifetimeYears} years`} + /> + } + label="Max sunshine" + value={`${Math.round(maxSunshineHoursPerYear).toLocaleString()} hrs/yr`} + /> + } + label="Carbon offset factor" + value={`${Math.round(carbonOffsetFactorKgPerMwh)} kg/MWh`} + /> + } + label="Maximum array" + value={`${maxArrayPanelsCount} panels · ${maxArrayAreaMeters2.toFixed(0)} m²`} + /> +
+
+
+
+
+ + {/* ── Roof profile ─────────────────────────────────────────────────── */} + +
+ {roofSegmentStats.map((seg: any, i: number) => ( + + ))} +
+
+ + {/* ── Solar configurations ─────────────────────────────────────────── */} + + + + + + + +
); }