diff --git a/package-lock.json b/package-lock.json index a207d462..b35812e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4bbcb1e4..c43a2d8d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/components/portfolio/summary/EpcBarChart.tsx b/src/app/components/portfolio/summary/EpcBarChart.tsx index f111afa2..6c26bab5 100644 --- a/src/app/components/portfolio/summary/EpcBarChart.tsx +++ b/src/app/components/portfolio/summary/EpcBarChart.tsx @@ -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} diff --git a/src/app/domna/financials.ts b/src/app/domna/financials.ts new file mode 100644 index 00000000..538f8c1d --- /dev/null +++ b/src/app/domna/financials.ts @@ -0,0 +1,13 @@ +import { PlanTypeEnum } from "@/app/db/schema/recommendations"; + +// Fixed Domna costs per delivery type +export const DOMNA_COST_MAP: Record & { + default: number; +} = { + solar_eco4: 2250, + solar_hhrsh_eco4: 2250, + empty_cavity_eco: 800, + partial_cavity_eco: 800, + extraction_eco: 800, + default: 800, +}; diff --git a/src/app/globals.css b/src/app/globals.css index 804366f3..6d892a40 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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%); + } + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index adc6ba69..c5f03c0b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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({ diff --git a/src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx b/src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx index 0e37944e..cde7d797 100644 --- a/src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx @@ -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( diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProjectProposal.tsx b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProjectProposal.tsx index a9ba8618..c71a2732 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProjectProposal.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProjectProposal.tsx @@ -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 = { + 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(null); - // Group by planType const grouped = useMemo(() => { const map: Record = {}; 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 ( -
- {/* Left: Chart */} - +
+ {/* Chart */} + - Plans by Work Type + + Homes by Work Type + - v.toString()} - onValueChange={(v) => setSelectedType(String(v) || null)} - className="h-64" - /> + {grouped.length > 1 ? ( + v.toString()} + onValueChange={(v) => + setSelectedType( + v && typeof v === "object" && "planType" in v + ? String((v as any).planType) + : null + ) + } + className="h-64" + /> + ) : ( + `${v} home${v === 1 ? "" : "s"}`} + /> + )} - {/* Right: Details */} - + {/* Metrics */} + - - {selectedType - ? selectedType.replaceAll("_", " ") - : "Select a work type"} + + {mappedTitles[selectedType || "default"]} - - {selectedType && selectedData ? ( - <> -
- Average client contribution -
-
- £{formatNumber(selectedData.avgClientContribution || 0)} -
+ +
+

+ Total client contribution +

+

+ £{formatNumber(selectedData?.totalClientContribution || 0)} +

+

+ Avg per home £ + {formatNumber(selectedData?.avgClientContribution || 0)} +

+
-
Carbon savings
-
- {(selectedData.totalCarbon * 1000).toFixed(2)} kgCO₂e -
- -
Bill savings
-
- £{formatNumber(selectedData.totalBills)} -
- -
- Total estimated contribution -
-
- £ - {formatNumber( - selectedData.totalFunding + - (selectedType.includes("cavity") ? 1500 : 500) // example extra cost rule - )} -
- - ) : ( -

Click a bar to view details

- )} +
+
+

Funding

+

+ £{formatNumber(selectedData?.totalFunding || 0)} +

+
+
+

Carbon

+

+ {((selectedData?.totalCarbon || 0) * 1000).toFixed(2)} kgCO₂e +

+
+
+

Bills

+

+ £{formatNumber(selectedData?.totalBills || 0)} +

+
+
@@ -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: , + subtitle: "Domna will help you unlock this much funding.", }, { - title: "Total Carbon Savings", - value: `${(totalCarbonSavings * 1000).toFixed(2)} kgCO₂e`, - icon: , + 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: , + 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: , + subtitle: "Properties included across your project plans.", }, ]; return ( -
- {cards.map((card) => ( - - +
+ {cards.map((c) => ( + + - {card.title} + {c.title} - {card.icon} -
- {card.value} +
+ {c.value}
+

{c.subtitle}

))} diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProposalColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProposalColumns.tsx index a53df76b..b6949a79 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProposalColumns.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/ProposalColumns.tsx @@ -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[] = [ + { + accessorKey: "landlordPropertyId", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.landlordPropertyId || "—"} +
+ ), + }, { accessorKey: "address", header: ({ column }) => ( @@ -20,7 +37,9 @@ export const planColumns: ColumnDef[] = [ ), cell: ({ row }) => ( -
{row.original.address || "—"}
+
+ {row.original.address || "—"} +
), }, { @@ -35,7 +54,9 @@ export const planColumns: ColumnDef[] = [ ), cell: ({ row }) => ( -
{row.original.postcode || "—"}
+
+ {row.original.postcode || "—"} +
), }, @@ -50,7 +71,7 @@ export const planColumns: ColumnDef[] = [ isProperty={false} /> ) : ( - None + None )}
), @@ -67,7 +88,7 @@ export const planColumns: ColumnDef[] = [ ), cell: ({ row }) => ( -
+
{String(row.original.planType).replaceAll("_", " ")}
), @@ -77,7 +98,6 @@ export const planColumns: ColumnDef[] = [ header: () =>
Total Funding
, cell: ({ row }) => (
- £{formatNumber(row.original.totalFunding || 0)} @@ -89,8 +109,7 @@ export const planColumns: ColumnDef[] = [ header: () =>
Carbon Savings
, cell: ({ row }) => (
- - + {((row.original.totalCarbonSavings || 0) * 1000).toFixed(2)} kgCO₂e
@@ -101,11 +120,22 @@ export const planColumns: ColumnDef[] = [ header: () =>
Bill Savings
, cell: ({ row }) => (
- - + £{formatNumber(row.original.totalBillSavings || 0)}
), }, + { + accessorKey: "clientContribution", + header: () =>
Investment
, + cell: ({ row }) => ( +
+ + £{formatNumber(row.original.clientContribution || 0)} + +
+ ), + sortingFn: "alphanumeric", + }, ]; diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/page.tsx b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/page.tsx index dc03a3c1..e15eb0bc 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/page.tsx @@ -11,18 +11,26 @@ export default async function YourProjectsPage({ const { slug: portfolioId } = await params; const latestPlans = await getPlansWithTotals(portfolioId); - console.log("latestPlans", latestPlans); - return ( -
+
+
+

+ Your Retrofit Projects +

+

+ Review project performance and funding insights across your portfolio. +

+
+ -
-

- Plans Overview + +
+

+ Your Homes

-

+
); } diff --git a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/utils.ts b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/utils.ts index ae2274aa..8adb9074 100644 --- a/src/app/portfolio/[slug]/(portfolio)/temp-reporting/utils.ts +++ b/src/app/portfolio/[slug]/(portfolio)/temp-reporting/utils.ts @@ -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 { planId: string; @@ -14,6 +16,9 @@ export interface PlanWithTotals extends Record { 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; } diff --git a/src/app/portfolio/[slug]/remote-assessment/page.tsx b/src/app/portfolio/[slug]/remote-assessment/page.tsx index 0491ad6e..01d75c44 100644 --- a/src/app/portfolio/[slug]/remote-assessment/page.tsx +++ b/src/app/portfolio/[slug]/remote-assessment/page.tsx @@ -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 ( diff --git a/src/lib/chartUtils.ts b/src/lib/chartUtils.ts new file mode 100644 index 00000000..87bae050 --- /dev/null +++ b/src/lib/chartUtils.ts @@ -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; + +export const constructCategoryColors = ( + categories: string[], + colors: AvailableChartColorsKeys[] +): Map => { + const categoryColors = new Map(); + 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; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6c7dcee6..9cf48c6e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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", +]; diff --git a/tailwind.config.js b/tailwind.config.js index 30f16286..317049d6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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 }) {