improving the solar ui and adding a map

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-08 05:13:59 +00:00
parent 0660ebf646
commit ff07111edd
4 changed files with 695 additions and 206 deletions

View file

@ -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
View file

@ -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"
}
}
}

View file

@ -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",

View file

@ -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)}` },
{ label: "Ground area", value: `${groundAreaMeters2.toFixed(1)}` },
{ 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)}`}
/>
</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>
);
}