diff --git a/src/app/components/building-passport/Toolbar.tsx b/src/app/components/building-passport/Toolbar.tsx index 39004c2e..329c33a1 100644 --- a/src/app/components/building-passport/Toolbar.tsx +++ b/src/app/components/building-passport/Toolbar.tsx @@ -56,7 +56,7 @@ const navigationMenuTriggerStyle = cva( "data-[active]:bg-accent/50", "data-[state=open]:bg-gray-200", "text-gray-900", - ].join(" ") + ].join(" "), ); export function Toolbar({ @@ -88,16 +88,6 @@ export function Toolbar({ ); - const solarAnalysisButton = ( - - - Solar - - ); - const recommendationsButton = ( 0 && decentHomes.uprn && decentHomesButton} - {solarAnalysisButton} {recommendationsButton} {documentsButton} 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 deleted file mode 100644 index 701c4a13..00000000 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/solar-analysis/page.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { - SunIcon, - BoltIcon, - 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 PropertyMapWrapper from "./PropertyMapWrapper"; -import SolarSimulationWrapper from "./SolarSimulationWrapper"; - -// ── 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 (!solarData) { - return ( -
-
-
- -
-

No Solar Data Available

-

Please check back later for updates.

-
-
- ); - } - - const solarScenarioData = await getSolarScenarioData(String(solarData.id)); - - const { - panelWidthMeters, - panelHeightMeters, - panelCapacityWatts, - panelLifetimeYears, - maxSunshineHoursPerYear, - carbonOffsetFactorKgPerMwh, - roofSegmentStats, - solarPanelConfigs, - maxArrayPanelsCount, - maxArrayAreaMeters2, - } = solarData.googleApiResponse.solarPotential; - - 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 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 ( -
- - {/* ── 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."} -

-
- - {/* 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 ─────────────────────────────────────────── */} - - - - - - - - -
- ); -}