diff --git a/src/app/portfolio/[slug]/(portfolio)/decent-homes/DecentHomesDashboard.tsx b/src/app/portfolio/[slug]/(portfolio)/decent-homes/DecentHomesDashboard.tsx new file mode 100644 index 00000000..3f9d3341 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/decent-homes/DecentHomesDashboard.tsx @@ -0,0 +1,276 @@ +"use client"; + +import React, { useState } from "react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, +} from "@/app/shadcn_components/ui/drawer"; + +interface PropertySummary { + id: bigint; + uprn: bigint; + address: string; + postcode: string; + has_condition_data: boolean; + passes_decent_homes: boolean; + category1: Record; + category2: Record; +} + +interface Props { + data: PropertySummary[]; + portfolioId: string; +} + +type ReasonKey = + | "damp_passes" + | "structure_passes" + | "heating_passes" + | "sap_passes" + | "thermal_comfort_passes" + | "defects_pass" + | "windows_pass" + | "kitchen_pass" + | "bathroom_pass" + | "space_pass"; + +const reasonLabelMap: Record = { + damp_passes: "Damp", + structure_passes: "Structural Issues", + heating_passes: "Heating", + sap_passes: "SAP Rating", + thermal_comfort_passes: "Thermal Comfort", + defects_pass: "General Disrepair", + windows_pass: "Windows", + kitchen_pass: "Kitchen", + bathroom_pass: "Bathroom", + space_pass: "Space Standard", +}; + +const DecentHomesDashboard: React.FC = ({ data, portfolioId }) => { + console.log("Data received:", data); + const [open, setOpen] = useState(false); + const [selectedGroup, setSelectedGroup] = useState([]); + const [selectedTitle, setSelectedTitle] = useState(""); + + const total = data.length; + const passed = data.filter((p) => p.passes_decent_homes); + const failedC1 = data.filter( + (p) => + p.has_condition_data && + Object.values(p.category1).some((v) => v === false) + ); + const failedC2 = data.filter( + (p) => + p.has_condition_data && + Object.values(p.category2).some((v) => v === false) + ); + const missing = data.filter((p) => !p.has_condition_data); + + const openDrawer = (group: PropertySummary[], title: string) => { + setSelectedGroup(group); + setSelectedTitle(title); + setOpen(true); + }; + + const failedReasons = new Map(); + + data.forEach((p) => { + if (!p.has_condition_data || p.passes_decent_homes) return; + + const reasons = Object.entries({ ...p.category1, ...p.category2 }) + .filter(([_, v]) => v === false) + .map(([key]) => key); + + reasons.forEach((r) => { + failedReasons.set(r, (failedReasons.get(r) ?? 0) + 1); + }); + }); + + const sortedReasons = Array.from(failedReasons.entries()).sort( + (a, b) => b[1] - a[1] + ); + const top3Reasons = sortedReasons.slice(0, 3); + const dataCompleteness = ( + (data.filter((p) => p.has_condition_data).length / total) * + 100 + ).toFixed(1); + + return ( +
+

Decent Homes Summary

+
+
+
+ +
+ Overall Pass Rate +
+
+ {((passed.length / total) * 100).toFixed(1)}% +
+
+ + {passed.length} + {" "} + out of{" "} + {total}{" "} + homes meet the Decent Homes Standard +
+ +

+ Data completeness: {dataCompleteness}% +

+
+
+
+
+ +
+ + openDrawer(failedC1, "Properties with Category 1 Risks") + } + className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10" + > + +
+ Category 1 Risks +
+
+ {failedC1.length} +
+
+
+ + + openDrawer(failedC2, "Properties with Category 2 Risks") + } + className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10" + > + +
+ Category 2 Risks +
+
+ {failedC2.length} +
+
+
+ + + openDrawer(missing, "Properties Missing Condition Data") + } + className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10" + > + +
+ No Condition Data +
+
+ {missing.length} +
+
+
+ + openDrawer(passed, "Properties Passing Decent Homes")} + className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out" + > + +
+ Passes Decent Homes +
+
+ {passed.length} +
+
+
+
+ +
+

+ Top Reasons for Failing +

+
+ {top3Reasons.map(([reason, count]) => ( +
+
+
🚫
+

+ {reasonLabelMap[reason as ReasonKey] ?? + reason.replace(/_/g, " ")} +

+

+ Failing in {count}{" "} + properties +

+
+
+ ))} +
+
+ + + + + {selectedTitle} + +
    + {selectedGroup.map((p) => ( +
  • +
    + {p.address} ({p.postcode}) +
    +
    + UPRN: {String(p.uprn)} +
    + {p.has_condition_data ? ( + <> + {Object.entries({ ...p.category1, ...p.category2 }) + .filter(([_, value]) => value === false) + .map(([key]) => ( +
    + Issue:{" "} + {reasonLabelMap[key as ReasonKey] ?? + key.replace(/_/g, " ")} +
    + ))} + + ) : ( +
    + No condition data available +
    + )} + +
  • + ))} +
+
+
+
+ ); +}; + +export default DecentHomesDashboard; diff --git a/src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx b/src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx new file mode 100644 index 00000000..1ebcf8d7 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx @@ -0,0 +1,189 @@ +import { db } from "@/app/db/db"; +import { property } from "@/app/db/schema/property"; +import { inArray, eq, and } from "drizzle-orm"; +import { surveyDB } from "@/app/db/surveyDB/connection"; +import { uploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; +import { + getEnergyAssessmentFromS3, + getConditionReport, + getPropertyMeta, +} from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils"; +import { + getAllRoomData, + getRoomsWithDamp, + getRoomsWithDefects, + getRoomsWithBadWindows, + areAllWindowsOk, + getElevationsWithIssues, + hasSufficientSpace, + meetsSapThreshold, + hasEfficientHeatingSystem, + isInsulationAdequate, +} from "@/app/portfolio/[slug]/building-passport/[propertyId]/assessment/decent_homes_utils"; +import DecentHomesDashboard from "./DecentHomesDashboard"; + +async function getPropertiesWithUprn( + portfolioId: string, + limit = 1000, + offset = 0 +): Promise { + const data = await db.query.property.findMany({ + limit, + offset, + columns: { + id: true, + uprn: true, + address: true, + postcode: true, + }, + where: eq(property.portfolioId, BigInt(portfolioId)), + }); + return data; +} + +async function getDocumentsForUprns( + uprns: (string | number)[] +): Promise> { + const rows = await surveyDB.query.uploadedFiles.findMany({ + where: and( + inArray(uploadedFiles.uprn, uprns.map(String)), + eq(uploadedFiles.docType, "ECO_CONDITION_REPORT") + ), + orderBy: (uploadedFiles, { desc }) => [ + desc(uploadedFiles.s3JsonUploadTimestamp), + ], + }); + + const latestByUprn = new Map(); + for (const row of rows) { + if (!latestByUprn.has(row.uprn)) { + latestByUprn.set(row.uprn, row); + } + } + + return latestByUprn; +} + +export default async function DecentHomesPage({ + params, + searchParams, +}: { + params: Promise<{ slug: string }>; + searchParams: Promise<{ + [key: string]: string | string[] | undefined | number; + }>; +}) { + const { slug: portfolioId } = await params; + const properties = await getPropertiesWithUprn(portfolioId, 1000, 0); + const uprns = properties.map((p) => String(p.uprn)); + const documentsMap = await getDocumentsForUprns(uprns); + + const summaryResults = await Promise.all( + properties.map(async (property) => { + const result: any = { + id: property.id, + uprn: property.uprn, + address: property.address, + postcode: property.postcode, + has_condition_data: false, + passes_decent_homes: false, + category1: {}, + category2: {}, + }; + + const propertyMeta = await getPropertyMeta(property.id); + + const conditionReportMeta = documentsMap.get(String(propertyMeta.uprn)); + + if (!conditionReportMeta || !conditionReportMeta.s3JsonUri) { + return result; + } + + result.has_condition_data = true; + + const conditionReport = await getEnergyAssessmentFromS3( + conditionReportMeta.s3JsonUri + ); + const conditionData = await getConditionReport(String(property.id)); + + if (!conditionReport || !conditionData) return result; + + const allRoomData = getAllRoomData(conditionReport.rooms); + const roomsWithDamp = getRoomsWithDamp(allRoomData); + const roomsWithDefects = getRoomsWithDefects(allRoomData); + const roomsWithBadWindows = getRoomsWithBadWindows(allRoomData); + const windowsOk = areAllWindowsOk(allRoomData); + const elevationsWithIssues = getElevationsWithIssues(conditionReport); + + const { + isSufficient: enoughSpace, + occupantsToUse, + numberOfBedrooms, + areaPerPerson, + } = hasSufficientSpace( + conditionReport.occupant_info, + conditionData.totalFloorArea ?? 0, + allRoomData + ); + + const sapOk = meetsSapThreshold(propertyMeta.currentSapPoints ?? 0); + const efficientHeating = hasEfficientHeatingSystem( + conditionData.heatingRating + ); + const insulationOk = isInsulationAdequate( + conditionData.roofRating, + conditionData.wallsRating + ); + const thermalComfortOk = efficientHeating && insulationOk; + + const kitchenOk = + conditionReport.rooms.kitchen?.room_info + ?.overall_condition_of_the_room === "Good"; + const bathroomsOk = + Array.isArray(conditionReport.rooms.bathrooms) && + conditionReport.rooms.bathrooms.length > 0 && + conditionReport.rooms.bathrooms.every( + (b: any) => b?.room_info?.overall_condition_of_the_room === "Good" + ); + + const hasDefects = roomsWithDefects.length > 0; + + result.category1 = { + damp_passes: roomsWithDamp.length === 0, + structure_passes: elevationsWithIssues.length === 0, + heating_passes: efficientHeating, + sap_passes: sapOk, + thermal_comfort_passes: thermalComfortOk, + }; + + result.category2 = { + defects_pass: !hasDefects, + windows_pass: windowsOk, + kitchen_pass: kitchenOk, + bathroom_pass: bathroomsOk, + space_pass: enoughSpace, + }; + + // Overall result + result.passes_decent_homes = + result.category1.damp_passes && + result.category1.structure_passes && + result.category1.heating_passes && + result.category1.sap_passes && + result.category1.thermal_comfort_passes && + result.category2.defects_pass && + result.category2.windows_pass && + result.category2.kitchen_pass && + result.category2.bathroom_pass && + result.category2.space_pass; + + return result; + }) + ); + + return ( +
+ +
+ ); +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx index b5951bab..9c7709d0 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx @@ -1,15 +1,7 @@ "use client"; -import { - Card, - CardContent, - CardHeader, -} from "@/app/shadcn_components/ui/card"; -import { - CheckCircle, - XCircle, - HelpCircle, -} from "lucide-react"; +import { Card, CardContent, CardHeader } from "@/app/shadcn_components/ui/card"; +import { CheckCircle, XCircle, HelpCircle } from "lucide-react"; import { Accordion, AccordionContent, @@ -32,6 +24,16 @@ import { meetsSapThreshold, } from "./decent_homes_utils"; +type Rating = 1 | 2 | 3 | 4 | 5; + +const RatingMap: Record = { + 1: "Very Poor", + 2: "Poor", + 3: "Average", + 4: "Good", + 5: "Very Good", +}; + function ChecklistItem({ label, passed, @@ -45,18 +47,19 @@ function ChecklistItem({ alert?: boolean; roomsWithIssues?: [string, any][]; }) { - const icon = passed === true ? ( - - ) : passed === false ? ( - - ) : ( - - ); + const icon = + passed === true ? ( + + ) : passed === false ? ( + + ) : ( + + ); return (
@@ -73,7 +76,10 @@ function ChecklistItem({
{roomsWithIssues.length > 0 && ( - + {roomsWithIssues.map(([roomName, room]: any) => ( {formatRoomName(roomName)} @@ -81,14 +87,17 @@ function ChecklistItem({
{room.room_info?.overall_condition_of_the_room && (
- Condition: {room.room_info.overall_condition_of_the_room} + Condition:{" "} + {room.room_info.overall_condition_of_the_room}
)} {room.room_info?.does_the_room_have_any_defects === "Yes" && (
-
Defect reported
+
+ Defect reported +
{room.room_info?.description_of_defect && (
{room.room_info.description_of_defect} @@ -97,11 +106,16 @@ function ChecklistItem({
)} - {room.room_info?.ventilation_info?.location_of_any_damp_or_mould && ( + {room.room_info?.ventilation_info + ?.location_of_any_damp_or_mould && (
- Damp/Mould Location: {room.room_info.ventilation_info.location_of_any_damp_or_mould} + Damp/Mould Location:{" "} + { + room.room_info.ventilation_info + .location_of_any_damp_or_mould + } (Severity: Average) @@ -140,7 +154,7 @@ export default function ConditionReport({ conditionReport: { rooms: Record; [key: string]: any; - }, + }; totalFloorArea: number; currentSapPoints: number; conditionData: PropertyDetailsEpc; @@ -151,22 +165,32 @@ export default function ConditionReport({ const roomsWithBadWindows = getRoomsWithBadWindows(allRoomData); const windowsOk = areAllWindowsOk(allRoomData); - const elevationsWithIssues = getElevationsWithIssues(conditionReport.access_and_elevations); + const elevationsWithIssues = getElevationsWithIssues( + conditionReport.access_and_elevations + ); const { isSufficient: enoughSpace, occupantsToUse, numberOfBedrooms, areaPerPerson, - } = hasSufficientSpace(conditionReport.occupant_info, totalFloorArea, allRoomData); + } = hasSufficientSpace(conditionReport, totalFloorArea, allRoomData); const sapOk = meetsSapThreshold(currentSapPoints); - const efficientHeating = hasEfficientHeatingSystem(conditionData.heating || ""); - const insulationOk = isInsulationAdequate(conditionData.heating || "", conditionData.roofRating, conditionData.wallsRating); + const efficientHeating = hasEfficientHeatingSystem( + conditionData.heatingRating + ); + const insulationOk = isInsulationAdequate( + conditionData.roofRating, + conditionData.wallsRating + ); const thermalComfortOk = efficientHeating && insulationOk; - const kitchenOk = conditionReport.rooms.kitchen?.room_info?.overall_condition_of_the_room === "Good"; - const bathroomsOk = Array.isArray(conditionReport.rooms.bathrooms) && + const kitchenOk = + conditionReport.rooms.kitchen?.room_info?.overall_condition_of_the_room === + "Good"; + const bathroomsOk = + Array.isArray(conditionReport.rooms.bathrooms) && conditionReport.rooms.bathrooms.length > 0 && conditionReport.rooms.bathrooms.every( (b: any) => b?.room_info?.overall_condition_of_the_room === "Good" @@ -210,26 +234,41 @@ export default function ConditionReport({
@@ -239,7 +278,11 @@ export default function ConditionReport({ @@ -272,7 +323,7 @@ export default function ConditionReport({ label="Sufficient space for number of occupants" passed={enoughSpace} alert={!enoughSpace} - note={`${occupantsToUse} occupants, ${numberOfBedrooms} bedrooms. ${areaPerPerson}m² per person`} + note={`${occupantsToUse} occupants, ${numberOfBedrooms} bedrooms. ${areaPerPerson.toFixed(1)}m² per person`} />
diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/decent_homes_utils.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/decent_homes_utils.tsx new file mode 100644 index 00000000..f81a78fc --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/decent_homes_utils.tsx @@ -0,0 +1,137 @@ +// utils/decentHomeChecks.ts + +export function getAllRoomData(rooms: Record) { + return [ + ...Object.entries(rooms).filter(([_, v]) => v?.room_info), + ...(rooms.bedrooms || []).map((b: any, i: number) => [ + "Bedroom " + (i + 1), + b, + ]), + ...(rooms.bathrooms || []).map((b: any, i: number) => [ + "Bathroom " + (i + 1), + b, + ]), + ]; +} + +export function getRoomsWithDamp(allRoomData: [string, any][]) { + return allRoomData.filter( + ([, room]: any) => + room.room_info?.ventilation_info + ?.are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room + ); +} + +export function getRoomsWithDefects(allRoomData: [string, any][]) { + return allRoomData.filter( + ([, room]: any) => room.room_info?.does_the_room_have_any_defects === "Yes" + ); +} + +export function getRoomsWithBadWindows(allRoomData: [string, any][]) { + return allRoomData.filter(([, room]: any) => { + const wi = room.room_info?.windows_info; + return ( + wi?.does_the_room_have_any_windows && + wi.condition_of_the_windows !== "Good condition" + ); + }); +} + +export function areAllWindowsOk(allRoomData: [string, any][]) { + return allRoomData.every(([, room]: any) => { + const wi = room.room_info?.windows_info; + return wi?.does_the_room_have_any_windows + ? wi?.condition_of_the_windows === "Good condition" + : true; + }); +} + +export function getElevationsWithIssues(conditionReport: any): [string, any][] { + const frontElevation = + conditionReport.access_and_elevations?.external_elevation_front + ?.external_elevation; + const applyFrontDefaults = ( + label: string, + elevationKey: string + ): [string, any] => { + const section = conditionReport.access_and_elevations?.[elevationKey]; + if (section?.do_all_answers_for_the_front_elevation_apply_to_this_wall) { + return [label, frontElevation]; + } + return [label, section?.external_elevation ?? null]; + }; + + const allElevations: [string, any][] = [ + ["Front Elevation", frontElevation], + applyFrontDefaults("Back Elevation", "external_elevation_back"), + applyFrontDefaults("Gable One", "external_elevation_gable_one"), + applyFrontDefaults("Gable Two", "external_elevation_gable_two"), + ]; + + return allElevations.filter( + ([_, elevation]) => + elevation && + (elevation.does_any_structural_defect_need_resolving_before_retrofit === + true || + elevation.are_there_any_signs_of_water_penetration_caused_by_failed_rainwater_goods_or_pipework === + true || + elevation.are_there_any_visible_signs_of_movement === true || + elevation.are_there_any_visible_signs_of_cracking_to_the_existing_external_finish === + true) + ); +} + +export function hasSufficientSpace( + conditionReport: any, + totalFloorArea: number, + allRoomData: [string, any][] +) { + const totalOccupants = + conditionReport.occupant_info?.total_number_of_occupants ?? 0; + const totalAdults = conditionReport.occupant_info?.no_of_adult_occupants ?? 0; + const totalChildren = + conditionReport.occupant_info?.no_of_child_occupants ?? 0; + const occupantsToUse = Math.max(totalAdults + totalChildren, totalOccupants); + + console.log("totalOccupants", totalOccupants); + console.log("occupantsToUse", occupantsToUse); + + const numberOfBedrooms = allRoomData.filter(([label]) => + label.includes("Bedroom") + ).length; + + const getRecommendedOccupants = (bedrooms: number) => { + if (bedrooms <= 0) return 0; + if (bedrooms === 1) return 2; + if (bedrooms === 2) return 4; + if (bedrooms === 3) return 6; + return 7; + }; + + const maxOccupants = getRecommendedOccupants(numberOfBedrooms); + const areaPerPerson = totalFloorArea / occupantsToUse; + const isSufficient = occupantsToUse <= maxOccupants && areaPerPerson >= 20; + + return { isSufficient, occupantsToUse, numberOfBedrooms, areaPerPerson }; +} + +export function hasEfficientHeatingSystem( + heatingRating: number | null +): boolean { + return heatingRating != null && heatingRating >= 3; +} + +export function isInsulationAdequate( + roofRating: number | null, + wallRating: number | null +): boolean { + return ( + (roofRating != null && roofRating >= 3) || + (wallRating != null && wallRating >= 3) + ); +} + +export function meetsSapThreshold(currentSapPoints: number): boolean { + return currentSapPoints >= 35; +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx index 6a506103..6fe81848 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx @@ -21,7 +21,7 @@ import { getSpatialData, getNonIntrusiveSurvey, getDocument, - getEnergyAssessmentFromS3 + getEnergyAssessmentFromS3, } from "../utils"; import ConditionReport from "@/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport"; @@ -122,11 +122,9 @@ const formatDate = (dateString: Date) => { }); }; -export default async function PreAssessmentReport( - props: { - params: Promise<{ slug: string; propertyId: string }>; - } -) { +export default async function PreAssessmentReport(props: { + params: Promise<{ slug: string; propertyId: string }>; +}) { const params = await props.params; const propertyMeta = await getPropertyMeta(params.propertyId); const conditionReportData = await getConditionReport(params.propertyId); @@ -135,14 +133,19 @@ export default async function PreAssessmentReport( conditionReportData, propertyMeta.propertyType ); - const conditionReportMeta = await getDocument( - { uprn: String(propertyMeta.uprn), documentType: "ECO_CONDITION_REPORT" } - ); + const conditionReportMeta = await getDocument({ + uprn: String(propertyMeta.uprn), + documentType: "ECO_CONDITION_REPORT", + }); let conditionReport = { rooms: {} }; if (conditionReportMeta && conditionReportMeta.s3JsonUri) { - conditionReport = await getEnergyAssessmentFromS3(conditionReportMeta.s3JsonUri); + conditionReport = await getEnergyAssessmentFromS3( + conditionReportMeta.s3JsonUri + ); } + console.log("conditionReport", conditionReport); + const nonIntrusiveSurvey = await getNonIntrusiveSurvey(propertyMeta.uprn); const retrofitFeatures = formatRetrofitFeatures(conditionReportData); @@ -176,15 +179,14 @@ export default async function PreAssessmentReport( - { - Object.keys(conditionReportMeta).length > 0 && - 0 && ( + - } + )} {nonIntrusiveSurvey && (
diff --git a/src/app/shadcn_components/ui/drawer.tsx b/src/app/shadcn_components/ui/drawer.tsx new file mode 100644 index 00000000..6a0ef53d --- /dev/null +++ b/src/app/shadcn_components/ui/drawer.tsx @@ -0,0 +1,118 @@ +"use client" + +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}