mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
improving the solar ui and adding a map
This commit is contained in:
parent
0660ebf646
commit
ff07111edd
4 changed files with 695 additions and 206 deletions
|
|
@ -9,7 +9,7 @@ const nextConfig = {
|
|||
],
|
||||
},
|
||||
allowedDevOrigins: ['local-origin.dev', '*.local-origin.dev'],
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
// use next-axiom for full stack monitoring
|
||||
|
|
|
|||
381
package-lock.json
generated
381
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<string, {
|
||||
gradient: string;
|
||||
border: string;
|
||||
badge: string;
|
||||
label: string;
|
||||
dot: string;
|
||||
}> = {
|
||||
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 (
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-brandblue tracking-tight">{title}</h2>
|
||||
<p className="text-sm text-gray-400 mt-0.5 leading-relaxed">{description}</p>
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function InputRow({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-3 group">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="shrink-0 w-7 h-7 rounded-md bg-brandblue/5 flex items-center justify-center text-brandblue/60 group-hover:bg-brandblue/10 transition-colors">
|
||||
{icon}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">{label}</span>
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-gray-800 tabular-nums">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`rounded-2xl border bg-gradient-to-br ${theme.gradient} ${theme.border} shadow-sm overflow-hidden`}>
|
||||
{/* Card header strip */}
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-gray-400 mb-1">
|
||||
Segment {index + 1}
|
||||
</p>
|
||||
<h3 className={`text-lg font-bold leading-tight ${theme.label}`}>
|
||||
{dir.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 text-xs font-bold px-2.5 py-1 rounded-full border ${theme.badge}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${theme.dot}`} />
|
||||
{dir.short}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sunshine bar */}
|
||||
{medianSunshine !== null && (
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between text-[10px] text-gray-400 mb-1">
|
||||
<span>Median sunshine</span>
|
||||
<span className="font-semibold text-gray-600">{Math.round(medianSunshine)} hrs/yr</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-white/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${theme.dot} opacity-70`}
|
||||
style={{ width: `${Math.min(100, (medianSunshine / 1600) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="opacity-30" />
|
||||
|
||||
{/* Stats grid */}
|
||||
<dl className="grid grid-cols-2 gap-px bg-black/5 text-sm">
|
||||
{[
|
||||
{ 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 ? (
|
||||
<div key={i} className="bg-white/40 px-4 py-3 hover:bg-white/60 transition-colors">
|
||||
<dt className="text-[10px] uppercase tracking-wide text-gray-400 font-medium mb-0.5">{label}</dt>
|
||||
<dd className="font-semibold text-gray-700 tabular-nums">{value}</dd>
|
||||
</div>
|
||||
) : <div key={i} className="bg-white/20" />
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="text-2xl font-semibold mb-4">
|
||||
No Solar Analysis Data Available
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gray-100 flex items-center justify-center mx-auto mb-4">
|
||||
<SunIcon className="w-8 h-8 text-gray-300" />
|
||||
</div>
|
||||
<p className="text-lg">Please check back later for updates.</p>
|
||||
<p className="text-base font-semibold text-gray-600">No Solar Data Available</p>
|
||||
<p className="text-sm text-gray-400 mt-1">Please check back later for updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -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<string, string> = {
|
||||
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 (
|
||||
<div className="leading-loose tracking-wider">
|
||||
<div className="py-8 max-w-7xl mx-auto">
|
||||
<div className="flex items-center text-gray-700 font-semibold mt-4 text-lg mb-4">
|
||||
<InformationCircleIcon className="w-6 h-6 text-brandgold mr-2" />
|
||||
{solarScenarioData.scenrioType === "building"
|
||||
? "These figures are for the building as a whole"
|
||||
: "These figures are for the individual property"}
|
||||
</div>
|
||||
<div className="max-w-6xl mx-auto px-4 py-10 space-y-14">
|
||||
|
||||
<div className="bg-white shadow-md rounded-lg p-6 mb-8 w-full">
|
||||
<h2 className="text-l font-semibold text-gray-800 mb-4">
|
||||
Simulation Panel Configuration
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="flex items-center p-4 bg-blue-50 rounded-lg shadow w-full">
|
||||
<InformationCircleIcon className="w-6 h-6 text-blue-500 mr-2" />
|
||||
<p className="text-lg text-gray-700">
|
||||
Dimensions: {panelWidthMeters}m x {panelHeightMeters}m
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center p-4 bg-green-50 rounded-lg shadow w-full">
|
||||
<LightBulbIcon className="w-6 h-6 text-green-500 mr-2" />
|
||||
<p className="text-lg text-gray-700">
|
||||
Wattage: {panelCapacityWatts}W
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center p-4 bg-yellow-50 rounded-lg shadow w-full">
|
||||
<SunIcon className="w-6 h-6 text-yellow-500 mr-2" />
|
||||
<p className="text-lg text-gray-700">
|
||||
Lifetime: {panelLifetimeYears} years
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center p-4 bg-orange-50 rounded-lg shadow w-full">
|
||||
<FlagIcon className="w-6 h-6 text-orange-500 mr-2" />
|
||||
<p className="text-lg text-gray-700">
|
||||
Region: {solarData.googleApiResponse.regionCode}
|
||||
</p>
|
||||
{/* ── Hero header ─────────────────────────────────────────────────── */}
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2.5 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brandgold/30 to-brandgold/10 border border-brandgold/30 flex items-center justify-center shrink-0">
|
||||
<SunIcon className="w-5 h-5 text-brandgold" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-brandblue tracking-tight">
|
||||
Solar Potential Analysis
|
||||
</h1>
|
||||
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full border ${qualityBadge}`}>
|
||||
{qualityText}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 max-w-xl leading-relaxed">
|
||||
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."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow-md rounded-lg p-6 mb-8 w-full">
|
||||
<h2 className="text-l font-semibold text-gray-800 mb-4">
|
||||
Weather and Environmental Data
|
||||
</h2>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center p-4 bg-indigo-50 rounded-lg shadow w-full">
|
||||
<CloudIcon className="w-6 h-6 text-indigo-500 mr-2" />
|
||||
<p className="text-lg text-gray-700">
|
||||
Max Sunshine Hours per Year: {maxSunshineHoursPerYear} hours
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center p-4 bg-teal-50 rounded-lg shadow w-full">
|
||||
<SparklesIcon className="w-6 h-6 text-teal-500 mr-2" />
|
||||
<p className="text-lg text-gray-700">
|
||||
Carbon Offset Factor: {carbonOffsetFactorKgPerMwh} kg/MWh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow-md rounded-lg p-6 mb-8 w-full">
|
||||
<h2 className="text-l font-semibold text-gray-800 mb-4">
|
||||
Roof Segments Data
|
||||
</h2>
|
||||
<FeatureTable
|
||||
data={transformedRoofSegmentStats}
|
||||
columns={roofSegmentsColumns}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow-md rounded-lg p-6 mb-8 w-full">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col space-y-4 w-1/2">
|
||||
<h2 className="text-l font-semibold text-gray-800 mb-4">
|
||||
Solar PV Simulation
|
||||
</h2>
|
||||
<ul className="list-none pl-5 text-gray-700">
|
||||
<li className="flex items-center">
|
||||
<HomeIcon className="w-5 h-5 text-blue-500 mr-2" />
|
||||
Number of panels: {solarScenarioData.numberPanels}
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<BoltIcon className="w-5 h-5 text-yellow-500 mr-2" />
|
||||
Array output: {solarScenarioData.arrayKwhp} kWp
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<SunIcon className="w-5 h-5 text-orange-500 mr-2" />
|
||||
Lifetime DC energy:{" "}
|
||||
{Math.round(solarScenarioData.lifetimeDcKwh)} kWh
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<SunIcon className="w-5 h-5 text-orange-500 mr-2" />
|
||||
Yearly DC energy: {Math.round(
|
||||
solarScenarioData.yearlyDcKwh
|
||||
)}{" "}
|
||||
kWh
|
||||
</li>
|
||||
{solarScenarioData.lifetimeAcKwh !== null && (
|
||||
<li className="flex items-center">
|
||||
<SunIcon className="w-5 h-5 text-orange-500 mr-2" />
|
||||
Lifetime AC energy:{" "}
|
||||
{Math.round(solarScenarioData.lifetimeAcKwh)} kWh
|
||||
</li>
|
||||
)}
|
||||
{solarScenarioData.yearlyAcKwh !== null && (
|
||||
<li className="flex items-center">
|
||||
<SunIcon className="w-5 h-5 text-orange-500 mr-2" />
|
||||
Yearly AC energy:{" "}
|
||||
{Math.round(solarScenarioData.yearlyAcKwh)} kWh
|
||||
</li>
|
||||
)}
|
||||
<li className="flex items-center">
|
||||
<CurrencyDollarIcon className="w-5 h-5 text-green-500 mr-2" />
|
||||
Cost: £{formatNumber(solarScenarioData.cost)}
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<ArrowTrendingUpIcon className="w-5 h-5 text-purple-500 mr-2" />
|
||||
Expected payback years:{" "}
|
||||
{solarScenarioData.expectedPaybackYears}
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<HomeIcon className="w-5 h-5 text-blue-500 mr-2" />
|
||||
Panelled roof area:{" "}
|
||||
{solarScenarioData.panelledRoofArea.toFixed(1)} m²
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-1/2 flex items-center justify-center bg-gray-500">
|
||||
<div>Map not currently available</div>
|
||||
{/* <img
|
||||
src="/pfp_solar_image.png"
|
||||
alt="Solar Image"
|
||||
className="w-full h-full"
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
{/* Key summary pills */}
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
{[
|
||||
{ icon: <MapPinIcon className="w-3.5 h-3.5" />, text: regionCode },
|
||||
{ icon: <CalendarIcon className="w-3.5 h-3.5" />, text: `Imagery: ${imageryDateStr}` },
|
||||
{ icon: <BoltIcon className="w-3.5 h-3.5" />, text: `Up to ${maxAnnualKwh.toLocaleString()} kWh/yr` },
|
||||
{ icon: <Squares2X2Icon className="w-3.5 h-3.5" />, text: `Up to ${maxArrayPanelsCount} panels` },
|
||||
].map(({ icon, text }, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-white border border-gray-200 text-gray-500 font-medium shadow-sm">
|
||||
<span className="text-brandblue/60">{icon}</span>
|
||||
{text}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Map + Inputs ─────────────────────────────────────────────────── */}
|
||||
<PageSection
|
||||
title="Property Overview"
|
||||
description="Location and the key parameters used to model this property's solar potential."
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-5">
|
||||
|
||||
{/* Map */}
|
||||
<div className="lg:col-span-3 rounded-2xl overflow-hidden border border-gray-200 shadow-sm" style={{ height: "360px" }}>
|
||||
<PropertyMapWrapper
|
||||
latitude={buildingCenter.latitude}
|
||||
longitude={buildingCenter.longitude}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Inputs card */}
|
||||
<Card className="lg:col-span-2 shadow-sm border-gray-200/80">
|
||||
<CardHeader className="pb-1 pt-5 px-5">
|
||||
<CardTitle className="text-sm font-bold text-brandblue">Analysis Parameters</CardTitle>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
Inputs used to model solar output for this property.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="px-5 pb-5 pt-1">
|
||||
<div className="divide-y divide-gray-100">
|
||||
<InputRow
|
||||
icon={<Squares2X2Icon className="w-3.5 h-3.5" />}
|
||||
label="Panel dimensions"
|
||||
value={`${panelWidthMeters} m × ${panelHeightMeters} m`}
|
||||
/>
|
||||
<InputRow
|
||||
icon={<BoltIcon className="w-3.5 h-3.5" />}
|
||||
label="Panel capacity"
|
||||
value={`${panelCapacityWatts} W`}
|
||||
/>
|
||||
<InputRow
|
||||
icon={<ClockIcon className="w-3.5 h-3.5" />}
|
||||
label="Panel lifetime"
|
||||
value={`${panelLifetimeYears} years`}
|
||||
/>
|
||||
<InputRow
|
||||
icon={<SunIcon className="w-3.5 h-3.5" />}
|
||||
label="Max sunshine"
|
||||
value={`${Math.round(maxSunshineHoursPerYear).toLocaleString()} hrs/yr`}
|
||||
/>
|
||||
<InputRow
|
||||
icon={<SparklesIcon className="w-3.5 h-3.5" />}
|
||||
label="Carbon offset factor"
|
||||
value={`${Math.round(carbonOffsetFactorKgPerMwh)} kg/MWh`}
|
||||
/>
|
||||
<InputRow
|
||||
icon={<GlobeAltIcon className="w-3.5 h-3.5" />}
|
||||
label="Maximum array"
|
||||
value={`${maxArrayPanelsCount} panels · ${maxArrayAreaMeters2.toFixed(0)} m²`}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
{/* ── Roof profile ─────────────────────────────────────────────────── */}
|
||||
<PageSection
|
||||
title="Roof Profile"
|
||||
description={`${roofSegmentStats.length} roof face${roofSegmentStats.length !== 1 ? "s" : ""} identified. Each is assessed independently — south-facing segments with low pitch typically yield the highest solar output.`}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{roofSegmentStats.map((seg: any, i: number) => (
|
||||
<RoofSegmentCard
|
||||
key={i}
|
||||
index={i}
|
||||
azimuthDegrees={seg.azimuthDegrees}
|
||||
pitchDegrees={seg.pitchDegrees}
|
||||
areaMeters2={seg.stats.areaMeters2}
|
||||
groundAreaMeters2={seg.stats.groundAreaMeters2}
|
||||
sunshineQuantiles={seg.stats.sunshineQuantiles}
|
||||
planeHeightAtCenterMeters={seg.planeHeightAtCenterMeters}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
{/* ── Solar configurations ─────────────────────────────────────────── */}
|
||||
<PageSection
|
||||
title="Solar Configurations"
|
||||
description="All array sizes modelled for this property. The efficiency curve reveals how output scales with system size — useful for understanding which roof faces are doing the most work."
|
||||
>
|
||||
<Card className="shadow-sm border-gray-200/80">
|
||||
<CardContent className="pt-6 px-6 pb-6">
|
||||
<SolarSimulationWrapper
|
||||
solarPanelConfigs={solarPanelConfigs}
|
||||
panelCapacityWatts={panelCapacityWatts}
|
||||
panelLifetimeYears={panelLifetimeYears}
|
||||
panelWidthMeters={panelWidthMeters}
|
||||
panelHeightMeters={panelHeightMeters}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSection>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue