trying to get the skeleton working

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-10 22:35:32 +00:00
parent 2341741270
commit f8ca195b83
3 changed files with 83 additions and 44 deletions

View file

@ -1,6 +1,7 @@
"use client";
import { useState, useTransition, useMemo } from "react";
import { useRouter } from "next/navigation";
import {
Recommendation,
RecommendationType,
@ -97,6 +98,7 @@ export default function RecommendationContainer({
Record<string, AugmentedRec | null>
>({});
const router = useRouter();
const [isPending, startTransition] = useTransition();
// Sort: included first, then not-included, then already-installed
@ -160,6 +162,7 @@ export default function RecommendationContainer({
}),
);
await updatePlanMetrics(planId, currentSapPoints, slug, propertyId);
router.refresh();
});
}

View file

@ -17,14 +17,22 @@ import {
function getEpcHex(letter: string | null | undefined): string {
switch (letter?.toUpperCase()) {
case "A": return "#117d58";
case "B": return "#2da55c";
case "C": return "#8dbd40";
case "D": return "#f7cd14";
case "E": return "#f3a96a";
case "F": return "#ef8026";
case "G": return "#e41e3b";
default: return "#9ca3af";
case "A":
return "#117d58";
case "B":
return "#2da55c";
case "C":
return "#8dbd40";
case "D":
return "#f7cd14";
case "E":
return "#f3a96a";
case "F":
return "#ef8026";
case "G":
return "#e41e3b";
default:
return "#9ca3af";
}
}
@ -34,7 +42,15 @@ const EPC_LETTERS = ["G", "F", "E", "D", "C", "B", "A"];
// Returns the horizontal centre position (%) of each EPC band in the 7-segment bar
function epcToBandCenter(letter: string | null | undefined): number {
if (!letter) return 0;
const map: Record<string, number> = { G: 0, F: 1, E: 2, D: 3, C: 4, B: 5, A: 6 };
const map: Record<string, number> = {
G: 0,
F: 1,
E: 2,
D: 3,
C: 4,
B: 5,
A: 6,
};
const idx = map[letter.toUpperCase()] ?? 0;
return ((idx + 0.5) / 7) * 100;
}
@ -44,12 +60,13 @@ export default async function PlanDetail(props: {
}) {
const params = await props.params;
const propertyMeta = await getPropertyMeta(params.propertyId);
const [recommendations, planMeta, installedMeasures, scenarioData] = await Promise.all([
getRecommendations(params.planId),
getPlanMeta(params.planId),
getInstalledMeasuresByUprn(propertyMeta.uprn),
getScenario(params.planId),
]);
const [recommendations, planMeta, installedMeasures, scenarioData] =
await Promise.all([
getRecommendations(params.planId),
getPlanMeta(params.planId),
getInstalledMeasuresByUprn(propertyMeta.uprn),
getScenario(params.planId),
]);
const currentEpc = propertyMeta.currentEpcRating;
const targetEpc =
@ -59,7 +76,10 @@ export default async function PlanDetail(props: {
const valuationLabel = (() => {
if (planMeta.valuationIncreaseAverage)
return `+${planMeta.valuationIncreaseAverage.toFixed(1)}%`;
if (planMeta.valuationIncreaseLowerBound && planMeta.valuationIncreaseUpperBound)
if (
planMeta.valuationIncreaseLowerBound &&
planMeta.valuationIncreaseUpperBound
)
return `+${planMeta.valuationIncreaseLowerBound.toFixed(0)}${planMeta.valuationIncreaseUpperBound.toFixed(0)}%`;
if (planMeta.valuationIncrease)
return `+${planMeta.valuationIncrease.toFixed(1)}%`;
@ -68,21 +88,15 @@ export default async function PlanDetail(props: {
return (
<div className="max-w-[1400px] mx-auto py-10 space-y-12">
{/* ── Header ─────────────────────────────────────────────────── */}
<div>
<h1 className="text-4xl md:text-5xl font-black font-manrope tracking-tighter text-brandblue mb-4">
<h1 className="text-brandblue text-4xl md:text-5xl font-black font-manrope tracking-tighter text-brandblue mb-4">
Plan Details: {planMeta.name ?? "Retrofit Plan"}
</h1>
<p className="text-gray-600 max-w-2xl text-lg leading-relaxed">
A comprehensive transformation strategy designed to improve energy
efficiency, reduce running costs, and maximise property value.
</p>
</div>
{/* ── Executive Summary Bento ──────────────────────────────────── */}
<section className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-6 gap-6">
{/* EPC Rating card — redesigned with SAP IMPROVEMENT label */}
<div className="md:col-span-2 lg:col-span-2 bg-white p-7 rounded-2xl border border-gray-100 shadow-sm flex flex-col gap-5">
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">
@ -92,7 +106,9 @@ export default async function PlanDetail(props: {
{/* Current → Target letter badges */}
<div className="flex items-end gap-3">
<div className="flex flex-col items-center gap-1.5">
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Current</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Current
</span>
<span
className="text-5xl font-black font-manrope leading-none"
style={{ color: getEpcHex(currentEpc) }}
@ -102,12 +118,21 @@ export default async function PlanDetail(props: {
</div>
<svg
className="w-5 h-5 text-gray-300 mb-2 shrink-0"
fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg>
<div className="flex flex-col items-center gap-1.5">
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Target</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Target
</span>
<span
className="text-7xl font-black font-manrope leading-none drop-shadow-lg"
style={{ color: getEpcHex(targetEpc) }}
@ -146,7 +171,8 @@ export default async function PlanDetail(props: {
className="text-[8px] font-bold"
style={{
color:
l === currentEpc?.toUpperCase() || l === targetEpc?.toUpperCase()
l === currentEpc?.toUpperCase() ||
l === targetEpc?.toUpperCase()
? getEpcHex(l)
: "#d1d5db",
}}
@ -175,7 +201,6 @@ export default async function PlanDetail(props: {
{/* 4 stat tiles — cool blue background, icon at top */}
<div className="md:col-span-2 lg:col-span-4 grid grid-cols-2 gap-4">
<div className="bg-blue-50 p-5 rounded-xl flex flex-col justify-between gap-3 border border-blue-100">
<CloudIcon className="w-8 h-8 text-brandmidblue" />
<p className="text-2xl font-bold font-manrope text-brandblue">
@ -221,13 +246,11 @@ export default async function PlanDetail(props: {
Valuation Boost
</p>
</div>
</div>
</section>
{/* ── Financial + Recommendations ──────────────────────────────── */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
{/* Left: Financial Overview — sticky so it stays visible while scrolling recs */}
<div className="lg:col-span-4 space-y-6 lg:sticky lg:top-8">
<h3 className="text-xs font-bold uppercase tracking-widest text-gray-500">
@ -251,7 +274,8 @@ export default async function PlanDetail(props: {
Contingency
</p>
<p className="text-xl font-bold font-manrope text-brandbrown">
{planMeta.contingencyCost != null && planMeta.contingencyCost > 0
{planMeta.contingencyCost != null &&
planMeta.contingencyCost > 0
? `£${formatNumber(planMeta.contingencyCost)}`
: "—"}
</p>
@ -261,7 +285,9 @@ export default async function PlanDetail(props: {
<InformationCircleIcon className="w-5 h-5 text-white/40 group-hover:text-white/80 transition-colors" />
<div className="absolute bottom-full right-0 mb-3 w-64 bg-gray-900 text-white text-xs rounded-xl p-3.5 shadow-2xl z-20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
<p className="leading-relaxed text-white/90">
A contingency buffer is added on top of estimated costs to account for unexpected variations in materials or labour. Rates vary per measure type.
A contingency buffer is added on top of estimated costs to
account for unexpected variations in materials or labour.
Rates vary per measure type.
</p>
<div className="absolute top-full right-4 border-4 border-transparent border-t-gray-900" />
</div>
@ -286,15 +312,18 @@ export default async function PlanDetail(props: {
<ul className="space-y-1.5">
<li className="text-xs text-gray-600 leading-relaxed flex items-start gap-1.5">
<span className="text-brandblue mt-0.5 shrink-0">·</span>
Costs modelled using current market rates and SAP 10.2 methodology.
Costs modelled using current market rates and SAP 10.2
methodology.
</li>
<li className="text-xs text-gray-600 leading-relaxed flex items-start gap-1.5">
<span className="text-brandblue mt-0.5 shrink-0">·</span>
Annual savings are projected based on typical occupancy patterns.
Annual savings are projected based on typical occupancy
patterns.
</li>
<li className="text-xs text-gray-600 leading-relaxed flex items-start gap-1.5">
<span className="text-brandblue mt-0.5 shrink-0">·</span>
Final quotes confirmed after a technical survey of the property.
Final quotes confirmed after a technical survey of the
property.
</li>
</ul>
</div>
@ -310,17 +339,23 @@ export default async function PlanDetail(props: {
{scenarioData.name && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Scenario</span>
<span className="font-semibold text-gray-800">{scenarioData.name}</span>
<span className="font-semibold text-gray-800">
{scenarioData.name}
</span>
</div>
)}
<div className="flex justify-between text-sm">
<span className="text-gray-500">Housing type</span>
<span className="font-semibold text-gray-800">{scenarioData.housingType}</span>
<span className="font-semibold text-gray-800">
{scenarioData.housingType}
</span>
</div>
{scenarioData.budget != null && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Budget</span>
<span className="font-semibold text-gray-800">£{formatNumber(scenarioData.budget)}</span>
<span className="font-semibold text-gray-800">
£{formatNumber(scenarioData.budget)}
</span>
</div>
)}
{scenarioData.goal && (
@ -328,7 +363,9 @@ export default async function PlanDetail(props: {
<span className="text-gray-500">Goal</span>
<span className="font-semibold text-gray-800 text-right">
{scenarioData.goal}
{scenarioData.goalValue ? `${scenarioData.goalValue}` : ""}
{scenarioData.goalValue
? `${scenarioData.goalValue}`
: ""}
</span>
</div>
)}

View file

@ -1,8 +1,7 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"target": "es5",
// "target": "ESNext",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@ -10,8 +9,8 @@
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"module": "preserve",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",