project ui live

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-31 23:18:21 +00:00
parent 031b5dacc1
commit 03c6425fd7
5 changed files with 302 additions and 178 deletions

View file

@ -23,13 +23,13 @@ const EpcBarChart = ({
index="name"
categories={["G", "F", "E", "D", "C", "B", "A"]} // Each treated as a separate series
colors={[
"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'
"#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'
]}
valueFormatter={dataFormatter}
yAxisWidth={48}

View file

@ -1,5 +1,3 @@
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@tailwind base;
@tailwind components;
@tailwind utilities;
@ -120,129 +118,3 @@
.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%);
}
}
}

View file

@ -9,6 +9,8 @@ import {
} from "@/app/shadcn_components/ui/card";
import { BarChart, DonutChart } from "@tremor/react";
import { formatNumber } from "@/app/utils";
import { PoundSterling, Leaf, Zap, Home } from "lucide-react";
import { motion } from "framer-motion";
const mappedTitles: Record<string, string> = {
solar_eco4: "Solar ECO4 project metrics",
@ -71,19 +73,10 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
? 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-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Chart */}
<Card className="lg:col-span-2 border border-gray-200 bg-white">
<Card className="lg:col-span-2 border border-gray-100 bg-gradient-to-br from-white to-gray-50/40 shadow-sm">
<CardHeader>
<CardTitle className="text-base font-medium text-brandblue">
Homes by Work Type
@ -95,7 +88,7 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
data={grouped}
index="planType"
categories={["count"]}
colors={domnaPalette}
colors={["#2d348f", "#14163d", "#3943b7", "#5d6be0"]}
valueFormatter={(v) => v.toString()}
onValueChange={(v) =>
setSelectedType(
@ -111,7 +104,7 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
data={grouped}
category="count"
index="planType"
colors={["midblue"]}
colors={["#2d348f", "#14163d", "#3943b7", "#5d6be0"]}
valueFormatter={(v) => `${v} home${v === 1 ? "" : "s"}`}
/>
)}
@ -130,7 +123,7 @@ export function ProjectProposal({ plans }: { plans: any[] }) {
<p className="text-sm text-gray-500 mb-1">
Total client contribution
</p>
<p className="text-2xl font-semibold text-brandblue">
<p className="text-2xl font-semibold text-brandbrown">
£{formatNumber(selectedData?.totalClientContribution || 0)}
</p>
<p className="text-xs text-gray-500">
@ -177,49 +170,81 @@ export function DashboardSummary({ plans }: { plans: any[] }) {
);
const planCount = plans.length;
const cards = [
const cards: {
title: string;
value: string | number;
subtitle: string;
icon: React.ElementType;
}[] = [
{
title: "Total Funding",
value: `£${formatNumber(totalFunding)}`,
subtitle: "Domna will help you unlock this much funding.",
icon: PoundSterling, // ✅ no <PoundSterling />
},
{
title: "Carbon Savings",
value: `${(totalCarbon * 1000).toFixed(2)} kgCO₂e`,
subtitle: "Your projects total estimated CO₂e savings.",
icon: Leaf,
},
{
title: "Bill Savings",
value: `£${formatNumber(totalBills)}`,
subtitle: "Expected total bill reductions across all homes.",
icon: Zap,
},
{
title: "Number of Homes",
value: planCount,
subtitle: "Properties included across your project plans.",
icon: Home,
},
];
return (
<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">
{c.title}
</CardTitle>
</CardHeader>
<CardContent>
<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>
))}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{cards.map((c) => {
const Icon = c.icon;
return (
<Card
key={c.title}
className="border border-gray-100 bg-gradient-to-br from-white to-brandlightblue/10 hover:shadow-lg hover:-translate-y-0.5 transition-all duration-300"
>
<CardHeader className="flex flex-row items-center gap-2 pb-1">
<div className="p-1.5 rounded-md bg-brandlightblue/40">
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="p-1.5 rounded-md bg-brandlightblue/40"
>
<Icon
className={`h-4 w-4 ${
c.title.includes("Funding")
? "text-brandbrown"
: c.title.includes("Carbon")
? "text-emerald-600"
: c.title.includes("Bill")
? "text-amber-600"
: "text-brandblue"
}`}
/>
</motion.div>
</div>
<CardTitle className="text-lg font-medium text-gray-600">
{c.title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-semibold text-transparent bg-clip-text bg-gradient-to-r from-brandblue to-midblue mb-1">
{c.value}
</div>
<p className="text-xs text-gray-500">{c.subtitle}</p>
</CardContent>
</Card>
);
})}
</div>
);
}

View file

@ -12,21 +12,22 @@ export default async function YourProjectsPage({
const latestPlans = await getPlansWithTotals(portfolioId);
return (
<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.
<div className="max-w-7xl mx-auto px-6 py-10 space-y-4">
<div className="mb-6">
<header className="text-3xl font-semibold text-brandblue">
Project Overview
</header>
<p className="text-sm text-gray-500">
Summary of funding, carbon savings, and household metrics.
</p>
</header>
<div className="h-px bg-gray-200 mt-2" />
</div>
<DashboardSummary plans={latestPlans} />
<ProjectProposal plans={latestPlans} />
<section>
<h2 className="text-xl font-semibold text-brandblue mb-3">
<h2 className="text-xl font-semibold text-brandblue mb-1">
Your Homes
</h2>
<DataTable data={latestPlans} columns={planColumns} />

View file

@ -28,6 +28,64 @@ 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",
@ -88,6 +146,11 @@ 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],
},
@ -134,6 +197,44 @@ 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: {
@ -169,6 +270,131 @@ 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]",
// blue colours for graphs - eff6fc
"bg-[#eff6fc]",
"border-[#eff6fc]",
"hover:bg-[#eff6fc]",
"hover:border-[#eff6fc]",
"hover:text-[#eff6fc]",
"fill-[#eff6fc]",
"ring-[#eff6fc]",
"stroke-[#eff6fc]",
"text-[#eff6fc]",
"ui-selected:bg-[#eff6fc]",
"ui-selected:border-[#eff6fc]",
"ui-selected:text-[#eff6fc]",
// brand blues for Tremor charts
"bg-[#14163d]",
"border-[#14163d]",
"fill-[#14163d]",
"stroke-[#14163d]",
"text-[#14163d]",
"bg-[#2d348f]",
"border-[#2d348f]",
"fill-[#2d348f]",
"stroke-[#2d348f]",
"text-[#2d348f]",
"bg-[#3943b7]",
"border-[#3943b7]",
"fill-[#3943b7]",
"stroke-[#3943b7]",
"text-[#3943b7]",
"bg-[#5d6be0]",
"border-[#5d6be0]",
"fill-[#5d6be0]",
"stroke-[#5d6be0]",
"text-[#5d6be0]",
"bg-[#1f3abdff]",
"border-[#1f3abdff]",
"fill-[#1f3abdff]",
"stroke-[#1f3abdff]",
"text-[#1f3abdff]",
],
plugins: [
function ({ addVariant }) {