mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
Before Tailwind upgrade
This commit is contained in:
parent
6a98a530ed
commit
031b5dacc1
15 changed files with 628 additions and 329 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -31,7 +31,7 @@
|
|||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"@tanstack/react-table": "^8.9.3",
|
||||
"@tremor/react": "^3.16.0",
|
||||
"@tremor/react": "^3.18.7",
|
||||
"@types/node": "20.2.3",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.1",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"@tanstack/react-table": "^8.9.3",
|
||||
"@tremor/react": "^3.16.0",
|
||||
"@tremor/react": "^3.18.7",
|
||||
"@types/node": "20.2.3",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.1",
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ const EpcBarChart = ({
|
|||
index="name"
|
||||
categories={["G", "F", "E", "D", "C", "B", "A"]} // Each treated as a separate series
|
||||
colors={[
|
||||
"#e41e3b", // Color for 'G'
|
||||
"#ef8026", // Color for 'F'
|
||||
"#f3a96a", // Color for 'E'
|
||||
"#f7cd14", // Color for 'D'
|
||||
"#8dbd40", // Color for 'C'
|
||||
"#2da55c", // Color for 'B'
|
||||
"#117d58", // Color for 'A'
|
||||
"epc_g", // Color for 'G'
|
||||
"epc_f", // Color for 'F'
|
||||
"epc_e", // Color for 'E'
|
||||
"epc_d", // Color for 'D'
|
||||
"epc_c", // Color for 'C'
|
||||
"epc_b", // Color for 'B'
|
||||
"epc_a", // Color for 'A'
|
||||
]}
|
||||
valueFormatter={dataFormatter}
|
||||
yAxisWidth={48}
|
||||
|
|
|
|||
13
src/app/domna/financials.ts
Normal file
13
src/app/domna/financials.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { PlanTypeEnum } from "@/app/db/schema/recommendations";
|
||||
|
||||
// Fixed Domna costs per delivery type
|
||||
export const DOMNA_COST_MAP: Record<PlanTypeEnum, number> & {
|
||||
default: number;
|
||||
} = {
|
||||
solar_eco4: 2250,
|
||||
solar_hhrsh_eco4: 2250,
|
||||
empty_cavity_eco: 800,
|
||||
partial_cavity_eco: 800,
|
||||
extraction_eco: 800,
|
||||
default: 800,
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/forms";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -76,7 +78,9 @@
|
|||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
font-feature-settings:
|
||||
"rlig" 1,
|
||||
"calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,3 +120,129 @@
|
|||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--animate-hide: hide 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-slide-down-and-fade: slideDownAndFade 150ms
|
||||
cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-slide-left-and-fade: slideLeftAndFade 150ms
|
||||
cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-slide-up-and-fade: slideUpAndFade 150ms
|
||||
cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-slide-right-and-fade: slideRightAndFade 150ms
|
||||
cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-accordion-open: accordionOpen 150ms cubic-bezier(0.87, 0, 0.13, 1);
|
||||
--animate-accordion-close: accordionClose 150ms cubic-bezier(0.87, 0, 0.13, 1);
|
||||
--animate-dialog-overlay-show: dialogOverlayShow 150ms
|
||||
cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-dialog-content-show: dialogContentShow 150ms
|
||||
cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-drawer-slide-left-and-fade: drawerSlideLeftAndFade 150ms
|
||||
cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-drawer-slide-right-and-fade: drawerSlideRightAndFade 150ms ease-in;
|
||||
|
||||
@keyframes hide {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes slideDownAndFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideLeftAndFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideUpAndFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideRightAndFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes accordionOpen {
|
||||
from {
|
||||
height: 0px;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
@keyframes accordionClose {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
@keyframes dialogOverlayShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes dialogContentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -45%) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes drawerSlideLeftAndFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes drawerSlideRightAndFade {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { cache } from "react";
|
|||
import { Inter } from "next/font/google";
|
||||
import { Toaster } from "@/app/shadcn_components/ui/toaster";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
// If loading a variable font, you don't need to specify the font weight
|
||||
const inter = Inter({
|
||||
|
|
|
|||
|
|
@ -3,23 +3,7 @@ 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 { getEnergyAssessmentFromS3 } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils";
|
||||
import DecentHomesDashboard from "./DecentHomesDashboard";
|
||||
|
||||
async function getPropertiesWithUprn(
|
||||
|
|
|
|||
|
|
@ -7,14 +7,21 @@ import {
|
|||
CardTitle,
|
||||
CardContent,
|
||||
} from "@/app/shadcn_components/ui/card";
|
||||
import { BarChart } from "@tremor/react";
|
||||
import { BarChart, DonutChart } from "@tremor/react";
|
||||
import { formatNumber } from "@/app/utils";
|
||||
import { Leaf, PoundSterling, Zap, FileSpreadsheet } from "lucide-react";
|
||||
|
||||
const mappedTitles: Record<string, string> = {
|
||||
solar_eco4: "Solar ECO4 project metrics",
|
||||
solar_hhrsh_eco4: "Solar HHRSH ECO4 project metrics",
|
||||
empty_cavity_eco: "Empty Cavity Insulation metrics",
|
||||
partial_cavity_eco: "Partial Cavity Insulation metrics",
|
||||
extraction_eco: "Extraction & Refill project metrics",
|
||||
default: "Select a work type to view metrics",
|
||||
};
|
||||
|
||||
export function ProjectProposal({ plans }: { plans: any[] }) {
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null);
|
||||
|
||||
// Group by planType
|
||||
const grouped = useMemo(() => {
|
||||
const map: Record<string, any[]> = {};
|
||||
for (const plan of plans) {
|
||||
|
|
@ -23,89 +30,135 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
|
|||
map[plan.planType].push(plan);
|
||||
}
|
||||
|
||||
// Summaries for the chart
|
||||
return Object.entries(map).map(([type, list]) => ({
|
||||
planType: type,
|
||||
count: list.length,
|
||||
avgClientContribution:
|
||||
list.reduce((sum, p) => sum + (p.totalFunding ?? 0) * 0.1, 0) /
|
||||
list.length, // placeholder calc
|
||||
totalFunding: list.reduce((sum, p) => sum + (p.totalFunding ?? 0), 0),
|
||||
totalCarbon: list.reduce(
|
||||
return Object.entries(map).map(([type, list]) => {
|
||||
const totalFunding = list.reduce(
|
||||
(sum, p) => sum + (p.totalFunding ?? 0),
|
||||
0
|
||||
);
|
||||
const totalClientContribution = list.reduce(
|
||||
(sum, p) => sum + (p.clientContribution ?? 0),
|
||||
0
|
||||
);
|
||||
const totalCarbon = list.reduce(
|
||||
(sum, p) => sum + (p.totalCarbonSavings ?? 0),
|
||||
0
|
||||
),
|
||||
totalBills: list.reduce((sum, p) => sum + (p.totalBillSavings ?? 0), 0),
|
||||
}));
|
||||
);
|
||||
const totalBills = list.reduce(
|
||||
(sum, p) => sum + (p.totalBillSavings ?? 0),
|
||||
0
|
||||
);
|
||||
return {
|
||||
planType: type,
|
||||
count: list.length,
|
||||
avgClientContribution: totalClientContribution / list.length,
|
||||
totalClientContribution,
|
||||
totalFunding,
|
||||
totalCarbon,
|
||||
totalBills,
|
||||
};
|
||||
});
|
||||
}, [plans]);
|
||||
|
||||
const selectedData = selectedType
|
||||
? grouped.find((d) => d.planType === selectedType)
|
||||
: null;
|
||||
useMemo(() => {
|
||||
if (grouped.length === 1 && !selectedType)
|
||||
setSelectedType(grouped[0].planType);
|
||||
}, [grouped, selectedType]);
|
||||
|
||||
const selectedData =
|
||||
selectedType && grouped.length
|
||||
? grouped.find((d) => d.planType === selectedType)
|
||||
: grouped.length === 1
|
||||
? grouped[0]
|
||||
: null;
|
||||
|
||||
const domnaPalette = [
|
||||
"#14163d", // brandblue (deep navy)
|
||||
"#2d348f", // midblue
|
||||
"#3943b7", // brandmidblue
|
||||
"#c4a47c", // brandbrown
|
||||
"#d3b488", // brandtan
|
||||
"#eff6fc", // brandlightblue (for subtle items)
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 my-8">
|
||||
{/* Left: Chart */}
|
||||
<Card className="lg:col-span-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Chart */}
|
||||
<Card className="lg:col-span-2 border border-gray-200 bg-white">
|
||||
<CardHeader>
|
||||
<CardTitle>Plans by Work Type</CardTitle>
|
||||
<CardTitle className="text-base font-medium text-brandblue">
|
||||
Homes by Work Type
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BarChart
|
||||
data={grouped}
|
||||
index="planType"
|
||||
categories={["count"]}
|
||||
colors={["blue"]}
|
||||
valueFormatter={(v) => v.toString()}
|
||||
onValueChange={(v) => setSelectedType(String(v) || null)}
|
||||
className="h-64"
|
||||
/>
|
||||
{grouped.length > 1 ? (
|
||||
<BarChart
|
||||
data={grouped}
|
||||
index="planType"
|
||||
categories={["count"]}
|
||||
colors={domnaPalette}
|
||||
valueFormatter={(v) => v.toString()}
|
||||
onValueChange={(v) =>
|
||||
setSelectedType(
|
||||
v && typeof v === "object" && "planType" in v
|
||||
? String((v as any).planType)
|
||||
: null
|
||||
)
|
||||
}
|
||||
className="h-64"
|
||||
/>
|
||||
) : (
|
||||
<DonutChart
|
||||
data={grouped}
|
||||
category="count"
|
||||
index="planType"
|
||||
colors={["midblue"]}
|
||||
valueFormatter={(v) => `${v} home${v === 1 ? "" : "s"}`}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Right: Details */}
|
||||
<Card>
|
||||
{/* Metrics */}
|
||||
<Card className="border border-gray-200 bg-white">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{selectedType
|
||||
? selectedType.replaceAll("_", " ")
|
||||
: "Select a work type"}
|
||||
<CardTitle className="text-lg font-semibold text-brandblue">
|
||||
{mappedTitles[selectedType || "default"]}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{selectedType && selectedData ? (
|
||||
<>
|
||||
<div className="text-sm text-gray-600">
|
||||
Average client contribution
|
||||
</div>
|
||||
<div className="text-xl font-semibold">
|
||||
£{formatNumber(selectedData.avgClientContribution || 0)}
|
||||
</div>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 mb-1">
|
||||
Total client contribution
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-brandblue">
|
||||
£{formatNumber(selectedData?.totalClientContribution || 0)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Avg per home £
|
||||
{formatNumber(selectedData?.avgClientContribution || 0)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600">Carbon savings</div>
|
||||
<div className="text-xl font-semibold">
|
||||
{(selectedData.totalCarbon * 1000).toFixed(2)} kgCO₂e
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600">Bill savings</div>
|
||||
<div className="text-xl font-semibold">
|
||||
£{formatNumber(selectedData.totalBills)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600">
|
||||
Total estimated contribution
|
||||
</div>
|
||||
<div className="text-xl font-semibold">
|
||||
£
|
||||
{formatNumber(
|
||||
selectedData.totalFunding +
|
||||
(selectedType.includes("cavity") ? 1500 : 500) // example extra cost rule
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-gray-500 italic">Click a bar to view details</p>
|
||||
)}
|
||||
<div className="grid grid-cols-3 gap-4 text-center border-t pt-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Funding</p>
|
||||
<p className="text-sm font-medium text-brandblue">
|
||||
£{formatNumber(selectedData?.totalFunding || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Carbon</p>
|
||||
<p className="text-sm font-medium text-brandblue">
|
||||
{((selectedData?.totalCarbon || 0) * 1000).toFixed(2)} kgCO₂e
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Bills</p>
|
||||
<p className="text-sm font-medium text-brandblue">
|
||||
£{formatNumber(selectedData?.totalBills || 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -114,11 +167,11 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
|
|||
|
||||
export function DashboardSummary({ plans }: { plans: any[] }) {
|
||||
const totalFunding = plans.reduce((sum, p) => sum + (p.totalFunding || 0), 0);
|
||||
const totalCarbonSavings = plans.reduce(
|
||||
const totalCarbon = plans.reduce(
|
||||
(sum, p) => sum + (p.totalCarbonSavings || 0),
|
||||
0
|
||||
);
|
||||
const totalBillSavings = plans.reduce(
|
||||
const totalBills = plans.reduce(
|
||||
(sum, p) => sum + (p.totalBillSavings || 0),
|
||||
0
|
||||
);
|
||||
|
|
@ -128,39 +181,42 @@ export function DashboardSummary({ plans }: { plans: any[] }) {
|
|||
{
|
||||
title: "Total Funding",
|
||||
value: `£${formatNumber(totalFunding)}`,
|
||||
icon: <PoundSterling className="h-6 w-6 text-brandblue" />,
|
||||
subtitle: "Domna will help you unlock this much funding.",
|
||||
},
|
||||
{
|
||||
title: "Total Carbon Savings",
|
||||
value: `${(totalCarbonSavings * 1000).toFixed(2)} kgCO₂e`,
|
||||
icon: <Leaf className="h-6 w-6 text-green-600" />,
|
||||
title: "Carbon Savings",
|
||||
value: `${(totalCarbon * 1000).toFixed(2)} kgCO₂e`,
|
||||
subtitle: "Your projects’ total estimated CO₂e savings.",
|
||||
},
|
||||
{
|
||||
title: "Total Bill Savings",
|
||||
value: `£${formatNumber(totalBillSavings)}`,
|
||||
icon: <Zap className="h-6 w-6 text-yellow-500" />,
|
||||
title: "Bill Savings",
|
||||
value: `£${formatNumber(totalBills)}`,
|
||||
subtitle: "Expected total bill reductions across all homes.",
|
||||
},
|
||||
{
|
||||
title: "Number of Plans",
|
||||
title: "Number of Homes",
|
||||
value: planCount,
|
||||
icon: <FileSpreadsheet className="h-6 w-6 text-brandbrown" />,
|
||||
subtitle: "Properties included across your project plans.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 my-6">
|
||||
{cards.map((card) => (
|
||||
<Card key={card.title} className="shadow-sm border border-gray-200">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{cards.map((c) => (
|
||||
<Card
|
||||
key={c.title}
|
||||
className="border border-gray-200 bg-white hover:bg-brandlightblue/10 transition-colors"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
{card.title}
|
||||
{c.title}
|
||||
</CardTitle>
|
||||
{card.icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold text-gray-900">
|
||||
{card.value}
|
||||
<div className="text-3xl font-semibold text-brandblue mb-1">
|
||||
{c.value}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{c.subtitle}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ArrowUpDown, Leaf, PoundSterling, Zap } from "lucide-react";
|
||||
import { ArrowUpDown } from "lucide-react";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { formatNumber } from "@/app/utils";
|
||||
import StatusBadge from "@/app/components/StatusBadge";
|
||||
import { PlanWithTotals } from "./utils";
|
||||
|
||||
export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
||||
{
|
||||
accessorKey: "landlordPropertyId",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Property Reference
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-gray-700 text-center">
|
||||
{row.original.landlordPropertyId || "—"}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "address",
|
||||
header: ({ column }) => (
|
||||
|
|
@ -20,7 +37,9 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-gray-700">{row.original.address || "—"}</div>
|
||||
<div className="text-gray-700 text-center text-sm">
|
||||
{row.original.address || "—"}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -35,7 +54,9 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-gray-700">{row.original.postcode || "—"}</div>
|
||||
<div className="text-gray-700 text-center">
|
||||
{row.original.postcode || "—"}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
|
|
@ -50,7 +71,7 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
isProperty={false}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-500">None</span>
|
||||
<span className="text-gray-500 text-center">None</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
|
@ -67,7 +88,7 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium text-gray-800">
|
||||
<div className="font-medium text-gray-800 text-center">
|
||||
{String(row.original.planType).replaceAll("_", " ")}
|
||||
</div>
|
||||
),
|
||||
|
|
@ -77,7 +98,6 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
header: () => <div className="text-center">Total Funding</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<PoundSterling className="h-4 w-4 text-brandblue" />
|
||||
<span className="font-medium">
|
||||
£{formatNumber(row.original.totalFunding || 0)}
|
||||
</span>
|
||||
|
|
@ -89,8 +109,7 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
header: () => <div className="text-center">Carbon Savings</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Leaf className="h-4 w-4 text-green-600" />
|
||||
<span className="font-medium">
|
||||
<span className="font-medium text-center">
|
||||
{((row.original.totalCarbonSavings || 0) * 1000).toFixed(2)} kgCO₂e
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -101,11 +120,22 @@ export const planColumns: ColumnDef<PlanWithTotals>[] = [
|
|||
header: () => <div className="text-center">Bill Savings</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Zap className="h-4 w-4 text-yellow-500" />
|
||||
<span className="font-medium">
|
||||
<span className="font-medium text-center">
|
||||
£{formatNumber(row.original.totalBillSavings || 0)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "clientContribution",
|
||||
header: () => <div className="text-center">Investment</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="font-semibold text-center">
|
||||
£{formatNumber(row.original.clientContribution || 0)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
sortingFn: "alphanumeric",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -11,18 +11,26 @@ export default async function YourProjectsPage({
|
|||
const { slug: portfolioId } = await params;
|
||||
const latestPlans = await getPlansWithTotals(portfolioId);
|
||||
|
||||
console.log("latestPlans", latestPlans);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 space-y-8">
|
||||
<div className="max-w-7xl mx-auto px-6 py-10 space-y-10">
|
||||
<header>
|
||||
<h1 className="text-3xl font-semibold text-brandblue mb-2">
|
||||
Your Retrofit Projects
|
||||
</h1>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Review project performance and funding insights across your portfolio.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<DashboardSummary plans={latestPlans} />
|
||||
<ProjectProposal plans={latestPlans} />
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-2">
|
||||
Plans Overview
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-brandblue mb-3">
|
||||
Your Homes
|
||||
</h2>
|
||||
<DataTable data={latestPlans} columns={planColumns} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { db } from "@/app/db/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { DOMNA_COST_MAP } from "@/app/domna/financials";
|
||||
import { PlanTypeEnum } from "@/app/db/schema/recommendations";
|
||||
|
||||
export interface PlanWithTotals extends Record<string, unknown> {
|
||||
planId: string;
|
||||
|
|
@ -14,6 +16,9 @@ export interface PlanWithTotals extends Record<string, unknown> {
|
|||
totalFunding: number | null;
|
||||
totalCarbonSavings: number | null;
|
||||
totalBillSavings: number | null;
|
||||
totalRecommendationCost?: number | null;
|
||||
surveyCost?: number;
|
||||
clientContribution?: number;
|
||||
}
|
||||
|
||||
export async function getPlansWithTotals(
|
||||
|
|
@ -30,7 +35,7 @@ export async function getPlansWithTotals(
|
|||
p.address AS "address",
|
||||
p.postcode AS "postcode",
|
||||
fp.scheme AS "fundingScheme",
|
||||
COALESCE(SUM(r.estimated_cost), 0) AS "totalFunding",
|
||||
COALESCE(fp.project_funding, 0) AS "totalFunding",
|
||||
COALESCE(SUM(r.co2_equivalent_savings), 0) AS "totalCarbonSavings",
|
||||
COALESCE(SUM(r.energy_cost_savings), 0) AS "totalBillSavings",
|
||||
COALESCE(SUM(r.estimated_cost), 0) AS "totalRecommendationCost"
|
||||
|
|
@ -61,9 +66,30 @@ export async function getPlansWithTotals(
|
|||
p.landlord_property_id,
|
||||
p.address,
|
||||
p.postcode,
|
||||
fp.scheme
|
||||
fp.scheme,
|
||||
fp.project_funding
|
||||
ORDER BY pl.created_at DESC;
|
||||
`);
|
||||
|
||||
return result.rows;
|
||||
const data = result.rows.map((plan) => {
|
||||
const planType = plan.planType as PlanTypeEnum | null;
|
||||
|
||||
const surveyCost = planType
|
||||
? (DOMNA_COST_MAP[planType] ?? DOMNA_COST_MAP.default)
|
||||
: DOMNA_COST_MAP.default;
|
||||
|
||||
const totalCost = plan.totalRecommendationCost ?? 0;
|
||||
const funding = plan.totalFunding ?? 0;
|
||||
|
||||
const rawContribution = totalCost + surveyCost - funding;
|
||||
const clientContribution = rawContribution > 0 ? rawContribution : 0;
|
||||
|
||||
return {
|
||||
...plan,
|
||||
surveyCost,
|
||||
clientContribution,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export default async function RemoteAssessmentPage(props: {
|
|||
const params = await props.params;
|
||||
const portfolioId = params.slug;
|
||||
|
||||
// 🔹 Replace this with your real Drizzle query
|
||||
const scenarios = await getPortfolioScenarios(portfolioId);
|
||||
|
||||
return (
|
||||
|
|
|
|||
204
src/lib/chartUtils.ts
Normal file
204
src/lib/chartUtils.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// Tremor Raw chartColors [v0.1.0]
|
||||
|
||||
export type ColorUtility = "bg" | "stroke" | "fill" | "text";
|
||||
|
||||
export const chartColors = {
|
||||
blue: {
|
||||
bg: "bg-blue-500",
|
||||
stroke: "stroke-blue-500",
|
||||
fill: "fill-blue-500",
|
||||
text: "text-blue-500",
|
||||
},
|
||||
emerald: {
|
||||
bg: "bg-emerald-500",
|
||||
stroke: "stroke-emerald-500",
|
||||
fill: "fill-emerald-500",
|
||||
text: "text-emerald-500",
|
||||
},
|
||||
violet: {
|
||||
bg: "bg-violet-500",
|
||||
stroke: "stroke-violet-500",
|
||||
fill: "fill-violet-500",
|
||||
text: "text-violet-500",
|
||||
},
|
||||
amber: {
|
||||
bg: "bg-amber-500",
|
||||
stroke: "stroke-amber-500",
|
||||
fill: "fill-amber-500",
|
||||
text: "text-amber-500",
|
||||
},
|
||||
gray: {
|
||||
bg: "bg-gray-500",
|
||||
stroke: "stroke-gray-500",
|
||||
fill: "fill-gray-500",
|
||||
text: "text-gray-500",
|
||||
},
|
||||
cyan: {
|
||||
bg: "bg-cyan-500",
|
||||
stroke: "stroke-cyan-500",
|
||||
fill: "fill-cyan-500",
|
||||
text: "text-cyan-500",
|
||||
},
|
||||
pink: {
|
||||
bg: "bg-pink-500",
|
||||
stroke: "stroke-pink-500",
|
||||
fill: "fill-pink-500",
|
||||
text: "text-pink-500",
|
||||
},
|
||||
lime: {
|
||||
bg: "bg-lime-500",
|
||||
stroke: "stroke-lime-500",
|
||||
fill: "fill-lime-500",
|
||||
text: "text-lime-500",
|
||||
},
|
||||
fuchsia: {
|
||||
bg: "bg-fuchsia-500",
|
||||
stroke: "stroke-fuchsia-500",
|
||||
fill: "fill-fuchsia-500",
|
||||
text: "text-fuchsia-500",
|
||||
},
|
||||
brandblue: {
|
||||
bg: "bg-[#14163d]",
|
||||
stroke: "stroke-[#14163d]",
|
||||
fill: "fill-[#14163d]",
|
||||
text: "text-[#14163d]",
|
||||
},
|
||||
midblue: {
|
||||
bg: "bg-[#2d348f]",
|
||||
stroke: "stroke-[#2d348f]",
|
||||
fill: "fill-[#2d348f]",
|
||||
text: "text-[#2d348f]",
|
||||
},
|
||||
brandmidblue: {
|
||||
bg: "bg-[#3943b7]",
|
||||
stroke: "stroke-[#3943b7]",
|
||||
fill: "fill-[#3943b7]",
|
||||
text: "text-[#3943b7]",
|
||||
},
|
||||
brandbrown: {
|
||||
bg: "bg-[#c4a47c]",
|
||||
stroke: "stroke-[#c4a47c]",
|
||||
fill: "fill-[#c4a47c]",
|
||||
text: "text-[#c4a47c]",
|
||||
},
|
||||
brandtan: {
|
||||
bg: "bg-[#d3b488]",
|
||||
stroke: "stroke-[#d3b488]",
|
||||
fill: "fill-[#d3b488]",
|
||||
text: "text-[#d3b488]",
|
||||
},
|
||||
brandlightblue: {
|
||||
bg: "bg-[#eff6fc]",
|
||||
stroke: "stroke-[#eff6fc]",
|
||||
fill: "fill-[#eff6fc]",
|
||||
text: "text-[#eff6fc]",
|
||||
},
|
||||
epc_a: {
|
||||
bg: "bg-[#117d58]",
|
||||
stroke: "stroke-[#117d58]",
|
||||
fill: "fill-[#117d58]",
|
||||
text: "text-[#117d58]",
|
||||
},
|
||||
epc_b: {
|
||||
bg: "bg-[#2da55c]",
|
||||
stroke: "stroke-[#2da55c]",
|
||||
fill: "fill-[#2da55c]",
|
||||
text: "text-[#2da55c]",
|
||||
},
|
||||
epc_c: {
|
||||
bg: "bg-[#8dbd40]",
|
||||
stroke: "stroke-[#8dbd40]",
|
||||
fill: "fill-[#8dbd40]",
|
||||
text: "text-[#8dbd40]",
|
||||
},
|
||||
epc_d: {
|
||||
bg: "bg-[#f7cd14]",
|
||||
stroke: "stroke-[#f7cd14]",
|
||||
fill: "fill-[#f7cd14]",
|
||||
text: "text-[#f7cd14]",
|
||||
},
|
||||
epc_e: {
|
||||
bg: "bg-[#f3a96a]",
|
||||
stroke: "stroke-[#f3a96a]",
|
||||
fill: "fill-[#f3a96a]",
|
||||
text: "text-[#f3a96a]",
|
||||
},
|
||||
epc_f: {
|
||||
bg: "bg-[#ef8026]",
|
||||
stroke: "stroke-[#ef8026]",
|
||||
fill: "fill-[#ef8026]",
|
||||
text: "text-[#ef8026]",
|
||||
},
|
||||
epc_g: {
|
||||
bg: "bg-[#e41e3b]",
|
||||
stroke: "stroke-[#e41e3b]",
|
||||
fill: "fill-[#e41e3b]",
|
||||
text: "text-[#e41e3b]",
|
||||
},
|
||||
} as const satisfies {
|
||||
[color: string]: {
|
||||
[key in ColorUtility]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AvailableChartColorsKeys = keyof typeof chartColors;
|
||||
|
||||
export const AvailableChartColors: AvailableChartColorsKeys[] = Object.keys(
|
||||
chartColors
|
||||
) as Array<AvailableChartColorsKeys>;
|
||||
|
||||
export const constructCategoryColors = (
|
||||
categories: string[],
|
||||
colors: AvailableChartColorsKeys[]
|
||||
): Map<string, AvailableChartColorsKeys> => {
|
||||
const categoryColors = new Map<string, AvailableChartColorsKeys>();
|
||||
categories.forEach((category, index) => {
|
||||
categoryColors.set(category, colors[index % colors.length]);
|
||||
});
|
||||
return categoryColors;
|
||||
};
|
||||
|
||||
export const getColorClassName = (
|
||||
color: AvailableChartColorsKeys,
|
||||
type: ColorUtility
|
||||
): string => {
|
||||
const fallbackColor = {
|
||||
bg: "bg-gray-500",
|
||||
stroke: "stroke-gray-500",
|
||||
fill: "fill-gray-500",
|
||||
text: "text-gray-500",
|
||||
};
|
||||
return chartColors[color]?.[type] ?? fallbackColor[type];
|
||||
};
|
||||
|
||||
// Tremor Raw getYAxisDomain [v0.0.0]
|
||||
|
||||
export const getYAxisDomain = (
|
||||
autoMinValue: boolean,
|
||||
minValue: number | undefined,
|
||||
maxValue: number | undefined
|
||||
) => {
|
||||
const minDomain = autoMinValue ? "auto" : (minValue ?? 0);
|
||||
const maxDomain = maxValue ?? "auto";
|
||||
return [minDomain, maxDomain];
|
||||
};
|
||||
|
||||
// Tremor Raw hasOnlyOneValueForKey [v0.1.0]
|
||||
|
||||
export function hasOnlyOneValueForKey(
|
||||
array: any[],
|
||||
keyToCheck: string
|
||||
): boolean {
|
||||
const val: any[] = [];
|
||||
|
||||
for (const obj of array) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, keyToCheck)) {
|
||||
val.push(obj[keyToCheck]);
|
||||
if (val.length > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,6 +1,42 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn (...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
// Tremor Raw cx [v0.0.0]
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function cx(...args: ClassValue[]) {
|
||||
return twMerge(clsx(...args));
|
||||
}
|
||||
|
||||
// Tremor focusInput [v0.0.2]
|
||||
|
||||
export const focusInput = [
|
||||
// base
|
||||
"focus:ring-2",
|
||||
// ring color
|
||||
"focus:ring-blue-200 dark:focus:ring-blue-700/30",
|
||||
// border color
|
||||
"focus:border-blue-500 dark:focus:border-blue-700",
|
||||
];
|
||||
|
||||
// Tremor Raw focusRing [v0.0.1]
|
||||
|
||||
export const focusRing = [
|
||||
// base
|
||||
"outline outline-offset-2 outline-0 focus-visible:outline-2",
|
||||
// outline color
|
||||
"outline-blue-500 dark:outline-blue-500",
|
||||
];
|
||||
|
||||
// Tremor Raw hasErrorInput [v0.0.1]
|
||||
|
||||
export const hasErrorInput = [
|
||||
// base
|
||||
"ring-2",
|
||||
// border color
|
||||
"border-red-500 dark:border-red-700",
|
||||
// ring color
|
||||
"ring-red-200 dark:ring-red-700/30",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -28,64 +28,6 @@ module.exports = {
|
|||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
colors: {
|
||||
tremor: {
|
||||
brand: {
|
||||
faint: "colors.blue[50]",
|
||||
muted: "colors.blue[200]",
|
||||
subtle: "colors.blue[400]",
|
||||
DEFAULT: "colors.blue[500]",
|
||||
emphasis: "colors.blue[700]",
|
||||
inverted: "colors.white",
|
||||
},
|
||||
background: {
|
||||
muted: "colors.gray[50]",
|
||||
subtle: "colors.gray[100]",
|
||||
DEFAULT: "colors.white",
|
||||
emphasis: "colors.gray[700]",
|
||||
},
|
||||
border: {
|
||||
DEFAULT: "colors.gray[200]",
|
||||
},
|
||||
ring: {
|
||||
DEFAULT: "colors.gray[200]",
|
||||
},
|
||||
content: {
|
||||
subtle: "colors.gray[400]",
|
||||
DEFAULT: "colors.gray[500]",
|
||||
emphasis: "colors.gray[700]",
|
||||
strong: "colors.gray[900]",
|
||||
inverted: "colors.white",
|
||||
},
|
||||
},
|
||||
"dark-tremor": {
|
||||
brand: {
|
||||
faint: "#0B1229",
|
||||
muted: "colors.blue[950]",
|
||||
subtle: "colors.blue[800]",
|
||||
DEFAULT: "colors.blue[500]",
|
||||
emphasis: "colors.blue[400]",
|
||||
inverted: "colors.blue[950]",
|
||||
},
|
||||
background: {
|
||||
muted: "#131A2B",
|
||||
subtle: "colors.gray[800]",
|
||||
DEFAULT: "colors.gray[900]",
|
||||
emphasis: "colors.gray[300]",
|
||||
},
|
||||
border: {
|
||||
DEFAULT: "colors.gray[800]",
|
||||
},
|
||||
ring: {
|
||||
DEFAULT: "colors.gray[800]",
|
||||
},
|
||||
content: {
|
||||
subtle: "colors.gray[600]",
|
||||
DEFAULT: "colors.gray[500]",
|
||||
emphasis: "colors.gray[200]",
|
||||
strong: "colors.gray[50]",
|
||||
inverted: "colors.gray[950]",
|
||||
},
|
||||
},
|
||||
epc_a: "#117d58",
|
||||
epc_b: "#2da55c",
|
||||
epc_c: "#8dbd40",
|
||||
|
|
@ -146,11 +88,6 @@ module.exports = {
|
|||
brandmidblue: "#3943b7",
|
||||
brandlightblue: "#00a9f4",
|
||||
},
|
||||
borderRadius: {
|
||||
"tremor-small": "0.375rem",
|
||||
"tremor-default": "0.5rem",
|
||||
"tremor-full": "9999px",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
|
|
@ -197,44 +134,6 @@ module.exports = {
|
|||
maxWidth: {
|
||||
"8xl": "90rem",
|
||||
},
|
||||
boxShadow: {
|
||||
"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||
"tremor-card":
|
||||
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||
"tremor-dropdown":
|
||||
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
||||
"dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||
"dark-tremor-card":
|
||||
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||
"dark-tremor-dropdown":
|
||||
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
||||
},
|
||||
fontSize: {
|
||||
"tremor-label": [
|
||||
"0.75rem",
|
||||
{
|
||||
lineHeight: "1rem",
|
||||
},
|
||||
],
|
||||
"tremor-default": [
|
||||
"0.875rem",
|
||||
{
|
||||
lineHeight: "1.25rem",
|
||||
},
|
||||
],
|
||||
"tremor-title": [
|
||||
"1.125rem",
|
||||
{
|
||||
lineHeight: "1.75rem",
|
||||
},
|
||||
],
|
||||
"tremor-metric": [
|
||||
"1.875rem",
|
||||
{
|
||||
lineHeight: "2.25rem",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
|
|
@ -270,91 +169,6 @@ module.exports = {
|
|||
pattern:
|
||||
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||
},
|
||||
// This enables the EPC colours for tremor. They're listed from EPC G -> A
|
||||
"bg-[#e41e3b]",
|
||||
"border-[#e41e3b]",
|
||||
"hover:bg-[#e41e3b]",
|
||||
"hover:border-[#e41e3b]",
|
||||
"hover:text-[#e41e3b]",
|
||||
"fill-[#e41e3b]",
|
||||
"ring-[#e41e3b]",
|
||||
"stroke-[#e41e3b]",
|
||||
"text-[#e41e3b]",
|
||||
"ui-selected:bg-[#e41e3b]",
|
||||
"ui-selected:border-[#e41e3b]",
|
||||
"ui-selected:text-[#e41e3b]",
|
||||
"bg-[#ef8026]",
|
||||
"border-[#ef8026]",
|
||||
"hover:bg-[#ef8026]",
|
||||
"hover:border-[#ef8026]",
|
||||
"hover:text-[#ef8026]",
|
||||
"fill-[#ef8026]",
|
||||
"ring-[#ef8026]",
|
||||
"stroke-[#ef8026]",
|
||||
"text-[#ef8026]",
|
||||
"ui-selected:bg-[#ef8026]",
|
||||
"ui-selected:border-[#ef8026]",
|
||||
"ui-selected:text-[#ef8026]",
|
||||
"bg-[#f3a96a]",
|
||||
"border-[#f3a96a]",
|
||||
"hover:bg-[#f3a96a]",
|
||||
"hover:border-[#f3a96a]",
|
||||
"hover:text-[#f3a96a]",
|
||||
"fill-[#f3a96a]",
|
||||
"ring-[#f3a96a]",
|
||||
"stroke-[#f3a96a]",
|
||||
"text-[#f3a96a]",
|
||||
"ui-selected:bg-[#f3a96a]",
|
||||
"ui-selected:border-[#f3a96a]",
|
||||
"ui-selected:text-[#f3a96a]",
|
||||
"bg-[#f7cd14]",
|
||||
"border-[#f7cd14]",
|
||||
"hover:bg-[#f7cd14]",
|
||||
"hover:border-[#f7cd14]",
|
||||
"hover:text-[#f7cd14]",
|
||||
"fill-[#f7cd14]",
|
||||
"ring-[#f7cd14]",
|
||||
"stroke-[#f7cd14]",
|
||||
"text-[#f7cd14]",
|
||||
"ui-selected:bg-[#f7cd14]",
|
||||
"ui-selected:border-[#f7cd14]",
|
||||
"ui-selected:text-[#f7cd14]",
|
||||
"bg-[#8dbd40]",
|
||||
"border-[#8dbd40]",
|
||||
"hover:bg-[#8dbd40]",
|
||||
"hover:border-[#8dbd40]",
|
||||
"hover:text-[#8dbd40]",
|
||||
"fill-[#8dbd40]",
|
||||
"ring-[#8dbd40]",
|
||||
"stroke-[#8dbd40]",
|
||||
"text-[#8dbd40]",
|
||||
"ui-selected:bg-[#8dbd40]",
|
||||
"ui-selected:border-[#8dbd40]",
|
||||
"ui-selected:text-[#8dbd40]",
|
||||
"bg-[#2da55c]",
|
||||
"border-[#2da55c]",
|
||||
"hover:bg-[#2da55c]",
|
||||
"hover:border-[#2da55c]",
|
||||
"hover:text-[#2da55c]",
|
||||
"fill-[#2da55c]",
|
||||
"ring-[#2da55c]",
|
||||
"stroke-[#2da55c]",
|
||||
"text-[#2da55c]",
|
||||
"ui-selected:bg-[#2da55c]",
|
||||
"ui-selected:border-[#2da55c]",
|
||||
"ui-selected:text-[#2da55c]",
|
||||
"bg-[#117d58]",
|
||||
"border-[#117d58]",
|
||||
"hover:bg-[#117d58]",
|
||||
"hover:border-[#117d58]",
|
||||
"hover:text-[#117d58]",
|
||||
"fill-[#117d58]",
|
||||
"ring-[#117d58]",
|
||||
"stroke-[#117d58]",
|
||||
"text-[#117d58]",
|
||||
"ui-selected:bg-[#117d58]",
|
||||
"ui-selected:border-[#117d58]",
|
||||
"ui-selected:text-[#117d58]",
|
||||
],
|
||||
plugins: [
|
||||
function ({ addVariant }) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue