new decent homes ui

This commit is contained in:
Khalim Conn-Kowlessar 2025-09-23 20:43:08 +00:00
parent e6b0b70270
commit 65ce0ecb2a
5 changed files with 780 additions and 0 deletions

160
package-lock.json generated
View file

@ -19,9 +19,11 @@
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@remixicon/react": "^4.2.0",
@ -3444,6 +3446,73 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz",
@ -4022,6 +4091,97 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz",

View file

@ -25,9 +25,11 @@
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@remixicon/react": "^4.2.0",

View file

@ -0,0 +1,515 @@
import {
getPropertyMeta,
getDocument,
getEnergyAssessmentFromS3,
} from "../utils";
import {
Card,
CardHeader,
CardTitle,
CardContent,
} from "@/app/shadcn_components/ui/card";
import { Badge } from "@/app/shadcn_components/ui/badge";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@/app/shadcn_components/ui/tabs";
import { Wrench } from "lucide-react";
const DISPLAY_NAMES: Record<string, string> = {
// Criterion A - HHSRS hazards
damp_and_mould_growth: "Damp and Mould Growth",
excess_cold: "Excess Cold",
excess_heat: "Excess Heat",
asbestos_and_mm_fibres: "Asbestos and MM Fibres",
biocides: "Biocides",
carbon_monoxide: "Carbon Monoxide",
lead: "Lead",
radiation: "Radiation",
uncombusted_fuel_gas: "Uncombusted Fuel Gas",
volatile_organic_compounds: "Volatile Organic Compounds",
crowding_and_space: "Crowding and Space",
entry_by_intruders: "Entry by Intruders",
lighting: "Lighting",
noise: "Noise",
domestic_hygiene_pests_and_refuse: "Domestic Hygiene, Pests, and Refuse",
food_safety: "Food Safety",
personal_hygiene_sanitation_and_drainage:
"Personal Hygiene, Sanitation, and Drainage",
water_supply: "Water Supply",
falls_associated_with_baths: "Falls Associated with Baths",
falls_on_level_surfaces: "Falls on Level Surfaces",
falls_on_stairs_and_steps: "Falls on Stairs and Steps",
falls_between_levels: "Falls Between Levels",
electrical_hazards: "Electrical Hazards",
fire: "Fire",
flames_hot_surfaces_and_materials: "Flames, Hot Surfaces, and Materials",
collision_and_entrapment: "Collision and Entrapment",
explosions: "Explosions",
ergonomics: "Ergonomics",
structural_collapse_and_falling_elements:
"Structural Collapse and Falling Elements",
// Criterion B - Key building components
wall_structure: "Wall Structure",
lintels: "Lintels",
wall_finish: "Wall Finish",
roof_structure: "Roof Structure",
roof_finish: "Roof Finish",
chimneys: "Chimneys",
windows: "Windows",
external_doors: "External Doors",
heating_other: "Other Heating Systems",
electrical_systems: "Electrical Systems",
kitchen: "Kitchen",
bathroom: "Bathroom",
// Criterion C - Modern facilities
kitchen_less_than_20_years_old: "Kitchen Less Than 20 Years Old",
kitchen_adequate_space_and_layout: "Kitchen Has Adequate Space and Layout",
bathroom_less_than_30_years_old: "Bathroom Less Than 30 Years Old",
bathroom_wc_appropriately_located: "Bathroom/WC Appropriately Located",
adequate_external_noise_insulation: "Adequate External Noise Insulation",
// Criterion D - Thermal comfort
efficient_heating_system_type: "Efficient Heating System Type",
efficient_heating_distribution: "Efficient Heating Distribution",
loft_insulation_sufficient: "Loft Insulation Sufficient",
wall_insulation_sufficient: "Wall Insulation Sufficient",
};
const SUB_ITEMS_TEXT: Record<string, string> = {
// Criterion A - Hazards (keep as-is, not replacements)
damp_and_mould_growth: "Damp and Mould Growth",
excess_cold: "Excess Cold",
excess_heat: "Excess Heat",
asbestos_and_mm_fibres: "Asbestos and MM Fibres",
biocides: "Biocides",
carbon_monoxide: "Carbon Monoxide",
lead: "Lead",
radiation: "Radiation",
uncombusted_fuel_gas: "Uncombusted Fuel Gas",
volatile_organic_compounds: "Volatile Organic Compounds",
crowding_and_space: "Crowding and Space",
entry_by_intruders: "Entry by Intruders",
lighting: "Lighting",
noise: "Noise",
domestic_hygiene_pests_and_refuse: "Domestic Hygiene, Pests, and Refuse",
food_safety: "Food Safety",
personal_hygiene_sanitation_and_drainage:
"Personal Hygiene, Sanitation, and Drainage",
water_supply: "Water Supply",
falls_associated_with_baths: "Falls Associated with Baths",
falls_on_level_surfaces: "Falls on Level Surfaces",
falls_on_stairs_and_steps: "Falls on Stairs and Steps",
falls_between_levels: "Falls Between Levels",
electrical_hazards: "Electrical Hazards",
fire: "Fire",
flames_hot_surfaces_and_materials: "Flames, Hot Surfaces, and Materials",
collision_and_entrapment: "Collision and Entrapment",
explosions: "Explosions",
ergonomics: "Ergonomics",
structural_collapse_and_falling_elements:
"Structural Collapse and Falling Elements",
// Criterion B - Key components
"Wall Structure in External Area": "Wall Structure Renewal",
"Lintels in External Area": "Lintel Renewal",
"Wall Finish 1 in External Area": "Wall Finish Renewal",
"Brickwork Pointing in External Area": "Brickwork Pointing Renewal",
"Roof Structure 1 in External Area": "Roof Structure Renewal",
"Fascia / Soffit / Bargeboard in External Area":
"Fascia / Soffit / Bargeboard Renewal",
"Gutters in External Area": "Gutter Renewal",
"Downpipes in External Area": "Downpipe Renewal",
"Roof Covering 1 in External Area": "Roof Covering Replacement",
"Chimneys in External Area": "Chimney Renewal",
"Windows in Property": "Window Replacement",
"Windows 1 in External Area": "Window Replacement",
"Type and Location of Front Door in Property": "Front Door Replacement",
"Back and Side Doors 1 in External Area": "Door Replacement",
"Back and Side Doors 2 in External Area": "Door Replacement",
"Type of Water Heating in Property": "Water Heating System Replacement",
"Electrics Required in Property": "Electrical System Renewal",
"Adequacy of Kitchen and Type in Property": "Kitchen Renewal",
"Adequacy of Bathroom Location in Property": "Bathroom Renewal",
// Criterion C - Modern facilities
kitchen_less_than_20_years_old: "Kitchen Replacement",
kitchen_adequate_space_and_layout: "Kitchen Layout Upgrade",
bathroom_less_than_30_years_old: "Bathroom Replacement",
bathroom_wc_appropriately_located: "Bathroom/WC Layout Upgrade",
adequate_external_noise_insulation: "Noise Insulation Upgrade",
// Criterion D - Thermal comfort
efficient_heating_system_type: "Heating System Upgrade",
efficient_heating_distribution: "Heating Distribution Upgrade",
loft_insulation_sufficient: "Loft Insulation Upgrade",
wall_insulation_sufficient: "Wall Insulation Upgrade",
};
const LABEL_MAP: Record<string, string> = {
pass: "Pass",
fail: "Fail",
no_data: "Not Assessed",
};
function StatusBadge({ status }: { status: string }) {
const colors =
status === "pass"
? "bg-green-600 hover:bg-green-700"
: status === "fail"
? "bg-red-700 hover:bg-red-800"
: "bg-gray-500 hover:bg-gray-600";
return (
<Badge className={`${colors} text-white text-xs px-2 py-1`}>
{LABEL_MAP[status]}
</Badge>
);
}
// urgency badge
function UrgencyBadge({ label }: { label: string }) {
const colorMap: Record<string, string> = {
Overdue: "bg-red-600",
"06 months": "bg-orange-500",
"612 months": "bg-yellow-500",
">12 months": "bg-green-600",
};
return (
<Badge className={`${colorMap[label]} text-white text-xs px-2 py-1`}>
{label}
</Badge>
);
}
function CriterionContent({
title,
items,
}: {
title: string;
items: { sub_variable: string; result: string }[];
}) {
const sortedItems = [...items].sort((a, b) => {
const order = { fail: 0, no_data: 1, pass: 2 };
return (
order[a.result as keyof typeof order] -
order[b.result as keyof typeof order]
);
});
return (
<Card className="h-96 flex flex-col relative overflow-hidden">
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent className="flex-1 overflow-y-scroll pr-2 scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100">
<ul className="space-y-2 pb-6">
{sortedItems.map((item, idx) => (
<li
key={idx}
className="flex justify-between items-center border-b last:border-0 pb-1"
>
<span className="text-gray-700">
{DISPLAY_NAMES[item.sub_variable] ?? item.sub_variable}
</span>
<StatusBadge status={item.result} />
</li>
))}
</ul>
</CardContent>
<div className="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-gray-100 to-transparent pointer-events-none" />
</Card>
);
}
function ReplacementsContent({
items,
}: {
items: { sub_variable: string; expiry_date: string | null }[];
}) {
const today = new Date();
const groups: Record<
string,
{ sub_variable: string; expiry: Date; remaining: string }[]
> = {
Overdue: [],
"06 months": [],
"612 months": [],
">12 months": [],
};
items.forEach((item) => {
if (!item.expiry_date) return;
const expiry = new Date(item.expiry_date);
const diffMs = expiry.getTime() - today.getTime();
const diffMonths = diffMs / (1000 * 60 * 60 * 24 * 30);
const years = Math.floor(diffMonths / 12);
const months = Math.floor(diffMonths % 12);
const remaining =
diffMs < 0
? "Expired"
: `${years > 0 ? `${years}y ` : ""}${months}m remaining`;
if (diffMs < 0) {
groups.Overdue.push({
sub_variable: SUB_ITEMS_TEXT[item.sub_variable],
expiry,
remaining,
});
} else if (diffMonths <= 6) {
groups["06 months"].push({
sub_variable: SUB_ITEMS_TEXT[item.sub_variable],
expiry,
remaining,
});
} else if (diffMonths <= 12) {
groups["612 months"].push({
sub_variable: SUB_ITEMS_TEXT[item.sub_variable],
expiry,
remaining,
});
} else {
groups[">12 months"].push({
sub_variable: SUB_ITEMS_TEXT[item.sub_variable],
expiry,
remaining,
});
}
});
// sort each group by expiry date ascending (soonest first)
Object.values(groups).forEach((comps) => {
comps.sort((a, b) => a.expiry.getTime() - b.expiry.getTime());
});
// order of groups
const groupOrder: (keyof typeof groups)[] = [
"Overdue",
"06 months",
"612 months",
">12 months",
];
return (
<Card className="h-96 flex flex-col relative overflow-hidden">
<CardHeader>
<CardTitle>Upcoming Replacements</CardTitle>
</CardHeader>
<CardContent className="flex-1 overflow-y-scroll pr-2 scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100">
{groupOrder.map((urgency) =>
groups[urgency].length > 0 ? (
<div key={urgency} className="mb-4">
<div className="flex items-center space-x-2 mb-2">
<UrgencyBadge label={urgency} />
<span className="text-gray-700 font-medium">
{groups[urgency].length} items
</span>
</div>
<ul className="space-y-1 ml-2">
{groups[urgency].map((comp, idx) => (
<li
key={idx}
className="flex justify-between border-b pb-1 last:border-0"
>
<span>
{DISPLAY_NAMES[comp.sub_variable] ?? comp.sub_variable}
</span>
<span className="text-sm text-gray-600">
{comp.remaining}
</span>
</li>
))}
</ul>
</div>
) : null
)}
</CardContent>
<div className="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-gray-100 to-transparent pointer-events-none" />
</Card>
);
}
function DecentHomesSummary({
decentHomes,
decentHomesMeta,
}: {
decentHomes: {
uprn: number;
creation_date: string;
criterion_a: string;
criterion_b: string;
criterion_c: string;
criterion_d: string;
decent_homes: string;
};
decentHomesMeta: {
criteria: string;
sub_variable: string;
result: string;
expiry_date?: string | null;
}[];
}) {
const overallPass = decentHomes.decent_homes === "pass";
const lastUpdated = new Date(decentHomes.creation_date).toLocaleDateString(
"en-GB",
{
day: "numeric",
month: "long",
year: "numeric",
}
);
const criteriaGroups: Record<
string,
{ sub_variable: string; result: string }[]
> = {
A: [],
B: [],
C: [],
D: [],
};
const replacements: { sub_variable: string; expiry_date: string | null }[] =
[];
decentHomesMeta.forEach((item) => {
if (criteriaGroups[item.criteria]) {
criteriaGroups[item.criteria].push({
sub_variable: item.sub_variable,
result: item.result,
});
}
if (item.expiry_date) {
replacements.push({
sub_variable: item.sub_variable,
expiry_date: item.expiry_date,
});
}
});
const soonCount = replacements.filter((r) => {
if (!r.expiry_date) return false;
const expiry = new Date(r.expiry_date);
const diffMs = expiry.getTime() - new Date().getTime();
return diffMs < 1000 * 60 * 60 * 24 * 365;
}).length;
return (
<div className="flex flex-col items-center mt-10 space-y-6">
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle className="text-center text-2xl font-semibold">
Decent Homes Assessment
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-1">
<Badge
className={`px-4 py-2 text-lg ${
overallPass
? "bg-green-600 hover:bg-green-700"
: "bg-red-700 hover:bg-red-800"
} text-white`}
>
{overallPass ? "Pass" : "Fail"}
</Badge>
<p className="text-sm text-gray-500">Last updated: {lastUpdated}</p>
</CardContent>
</Card>
<Tabs defaultValue="A" className="w-full max-w-4xl">
<TabsList className="grid grid-cols-5 w-full">
<TabsTrigger value="A">Criterion A</TabsTrigger>
<TabsTrigger value="B">Criterion B</TabsTrigger>
<TabsTrigger value="C">Criterion C</TabsTrigger>
<TabsTrigger value="D">Criterion D</TabsTrigger>
<TabsTrigger
value="replacements"
className="relative flex items-center space-x-2 text-orange-700 font-medium
data-[state=active]:bg-brandbrown data-[state=active]:rounded-md data-[state=active]:text-gray-100"
>
<Wrench className="w-4 h-4" />
<span>Replacements</span>
{soonCount > 0 && (
<span className="absolute -top-1 -right-2 bg-red-600 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{soonCount}
</span>
)}
</TabsTrigger>
</TabsList>
<TabsContent value="A" className="mt-4">
<CriterionContent
title="Criterion A: The home meets the current statutory minimum standard for housing"
items={criteriaGroups["A"]}
/>
</TabsContent>
<TabsContent value="B" className="mt-4">
<CriterionContent
title="Criterion B: The home is in a reasonable state of repair"
items={criteriaGroups["B"]}
/>
</TabsContent>
<TabsContent value="C" className="mt-4">
<CriterionContent
title="Criterion C: The home has reasonable modern facilities and services"
items={criteriaGroups["C"]}
/>
</TabsContent>
<TabsContent value="D" className="mt-4">
<CriterionContent
title="Criterion D: The home provides a reasonable degree of thermal comfort"
items={criteriaGroups["D"]}
/>
</TabsContent>
<TabsContent value="replacements" className="mt-4">
<ReplacementsContent items={replacements} />
</TabsContent>
</Tabs>
</div>
);
}
export default async function DecentHomesPage(props: {
params: Promise<{ slug: string; propertyId: string }>;
}) {
const params = await props.params;
const propertyMeta = await getPropertyMeta(params.propertyId);
const decentHomesSummary = await getDocument({
uprn: String(propertyMeta.uprn),
documentType: "DECENT_HOMES_SUMMARY",
});
const decentPropertyMeta = await getDocument({
uprn: String(propertyMeta.uprn),
documentType: "DECENT_HOMES_PROPERTY_META",
});
if (
Object.keys(decentHomesSummary).length === 0 ||
Object.keys(decentPropertyMeta).length === 0 ||
!decentHomesSummary.s3JsonUri ||
!decentPropertyMeta.s3JsonUri
) {
throw new Error("Decent Homes data is missing");
}
const decentHomesMeta = await getEnergyAssessmentFromS3(
decentPropertyMeta.s3JsonUri
);
const decentHomes = await getEnergyAssessmentFromS3(
decentHomesSummary.s3JsonUri
);
return (
<DecentHomesSummary
decentHomes={decentHomes}
decentHomesMeta={decentHomesMeta}
/>
);
}

View file

@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View file

@ -0,0 +1,55 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };