From 60c837b26f8d45ece45383c2db21397d20bc3eee Mon Sep 17 00:00:00 2001 From: onehanddev Date: Tue, 19 Nov 2024 17:38:21 +0530 Subject: [PATCH 1/7] feature: Added Project details drawer / sheet for project list and its dependent components --- .../project-details/bar-chart/index.tsx | 73 ++++ .../overview/project-details/index.tsx | 386 ++++++++++++++++++ .../project-details/parameters/index.tsx | 109 +++++ .../overview/table/view/overview/index.tsx | 20 +- client/src/lib/format.tsx | 18 + client/tailwind.config.ts | 10 + 6 files changed, 612 insertions(+), 4 deletions(-) create mode 100644 client/src/containers/overview/project-details/bar-chart/index.tsx create mode 100644 client/src/containers/overview/project-details/index.tsx create mode 100644 client/src/containers/overview/project-details/parameters/index.tsx diff --git a/client/src/containers/overview/project-details/bar-chart/index.tsx b/client/src/containers/overview/project-details/bar-chart/index.tsx new file mode 100644 index 00000000..b80de9d3 --- /dev/null +++ b/client/src/containers/overview/project-details/bar-chart/index.tsx @@ -0,0 +1,73 @@ +interface BarChartProps { + total: number; + segments: { + value: number; + label: string; + colorClass: string; + }[]; + orientation?: "horizontal" | "vertical"; +} + +const BarChart = ({ + total, + segments, + orientation = "horizontal", +}: BarChartProps) => { + const getSize = (value: number) => { + const percentage = (value / total) * 100; + return `${Math.max(percentage, 0)}%`; + }; + + const formatAmount = (value: number) => { + return `${(value / 1_000_000).toFixed(1)}M`; + }; + + if (orientation === "horizontal") { + return ( +
+
+ {segments.map((segment, index) => ( +
+
+
+ ${formatAmount(segment.value)} +
+
+
+ ))} +
+
+ ); + } + + return ( +
+
+ {segments.map((segment, index) => ( +
+
+
+ ${formatAmount(segment.value)} +
+
+
+ ))} +
+
+ ); +}; + +export default BarChart; diff --git a/client/src/containers/overview/project-details/index.tsx b/client/src/containers/overview/project-details/index.tsx new file mode 100644 index 00000000..1b5869e2 --- /dev/null +++ b/client/src/containers/overview/project-details/index.tsx @@ -0,0 +1,386 @@ +import { ChevronUp, ChevronDown, Plus } from "lucide-react"; + +import { parseCurrency, formatCurrency } from "@/lib/format"; + +import BarChart from "@/containers/overview/project-details/bar-chart"; +import ParametersProjects from "@/containers/overview/project-details/parameters"; + +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import InfoButton from "@/components/ui/info-button"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; + +const Currency = ({ value }: { value: number }) => { + const [symbol, amount] = parseCurrency(value); + return ( + + {symbol} {amount} + + ); +}; + +//////// ScoreIndicator component //////// +interface ScoreIndicatorProps { + score: "High" | "Medium" | "Low"; + className?: string; +} + +const ScoreIndicator = ({ score, className = "" }: ScoreIndicatorProps) => { + const bgColorClass = { + High: "bg-high", + Medium: "bg-medium", + Low: "bg-low", + }[score]; + + return ( +
+ {score} +
+ ); +}; + +//////// Legend component //////// +const Legend = ({ + name, + textColor, + bgColor, +}: { + name: string; + textColor: string; + bgColor: string; +}) => { + return ( +
+
+
+ {name} +
+
+ ); +}; + +// Mock data - to be replaced with API data later +const projectData = { + name: "Australian Mangrove Conservation", + size: "Small", + carbonPricingType: "Market (30$)", + cost: "NPV", + totalCost: 38023789, + capEx: 1500000, + opEx: 36500000, + leftover: 4106132, + totalRevenue: 40600000, + opExRevenue: 36500000, + abatement: 962991, + overallScore: "Medium", + scorecard: [ + { name: "Economic feasibility", rating: "High" }, + { name: "Legal feasibility", rating: "Low" }, + { name: "Implementation risk", rating: "High" }, + { name: "Social feasibility", rating: "Medium" }, + { name: "Security risk", rating: "Medium" }, + { name: "Value for money", rating: "Low" }, + { name: "Overall", rating: "Medium" }, + ], + costEstimates: [ + { + name: "Capital expenditure", + value: 1514218, + items: [ + { name: "Feasibility analysis", value: 70000 }, + { name: "Conservation planning and admin", value: 629559 }, + { name: "Data collection and field costs", value: 76963 }, + { name: "Community representation", value: 286112 }, + { name: "Blue carbon project planning", value: 111125 }, + { name: "Establishing carbon rights", value: 296010 }, + { name: "Validation", value: 44450 }, + { name: "Implementation labor", value: 0 }, + ], + }, + { + name: "Operating expenditure", + value: 36509571, + items: [ + { name: "Monitoring and Maintenance", value: 402322 }, + { name: "Community benefit sharing fund", value: 34523347 }, + { name: "Carbon standard fees", value: 227875 }, + { name: "Baseline reassessment", value: 75812 }, + { name: "MRV", value: 223062 }, + ], + }, + { name: "Total cost", value: 38023789 }, + ], +}; + +interface IProjectDetailsProps { + projectName: string; + open: boolean; + setIsOpen: (open: boolean) => void; +} + +export default function ProjectDetails({ + projectName, + open, + setIsOpen, +}: IProjectDetailsProps) { + return ( + + + +
+
+ + +
+
+ {projectName} +
+
+ +
+ +
+
+ +
+
+
+

Total project cost

+ +
+
+ Refers to the summary of Capital Expenditure and Operating + Expenditure +
+
+
+
+
+
+ + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+

Total project cost

+ +
+
+ Refers to the summary of Capital Expenditure and Operating + Expenditure +
+
+
+
+
+
+ + + +
+
+ + +
+
+
+ +
+
+
+
+ +
+ +
+
+

Abatement potential

+ +
+ + + +
+
+

Estimation of total CO2 expected during the project.

+
+
+ + +
+
+

Overall score

+ +
+
+
+ + {projectData.overallScore} + +
+
+
+ + +
+
+

Scorecard ratings

+ +
+
+ + +
+
+
+ {projectData.scorecard.map((item) => ( + <> +
+
{item.name}
+ +
+
+ + ))} +
+
+ + +
+
+

Cost estimates

+ +
+
+ + +
+
+
+ {projectData.costEstimates.map((estimate) => ( +
+
+
+ {estimate.name} +
+
+ + {formatCurrency(estimate.value)} + +
+
+ {estimate.items?.map((item) => ( +
+
+ {item.name} +
+
+ + {formatCurrency(item.value)} + +
+
+ ))} +
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/client/src/containers/overview/project-details/parameters/index.tsx b/client/src/containers/overview/project-details/parameters/index.tsx new file mode 100644 index 00000000..4a0049c2 --- /dev/null +++ b/client/src/containers/overview/project-details/parameters/index.tsx @@ -0,0 +1,109 @@ +import { + PROJECT_SIZE_FILTER, + COST_TYPE_SELECTOR, + PROJECT_PRICE_TYPE, +} from "@shared/entities/projects.entity"; +import { z } from "zod"; + +import { FILTER_KEYS } from "@/app/(overview)/constants"; +import { useGlobalFilters } from "@/app/(overview)/url-store"; +import { filtersSchema } from "@/app/(overview)/url-store"; + +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +export const PROJECT_PARAMETERS = [ + { + key: FILTER_KEYS[1], + label: "Project size", + className: "w-[125px]", + options: [ + { + label: PROJECT_SIZE_FILTER.SMALL, + value: PROJECT_SIZE_FILTER.SMALL, + }, + { + label: PROJECT_SIZE_FILTER.MEDIUM, + value: PROJECT_SIZE_FILTER.MEDIUM, + }, + { + label: PROJECT_SIZE_FILTER.LARGE, + value: PROJECT_SIZE_FILTER.LARGE, + }, + ], + }, + { + key: FILTER_KEYS[2], + label: "Carbon pricing type", + className: "w-[195px]", + options: [ + { + label: PROJECT_PRICE_TYPE.MARKET_PRICE, + value: PROJECT_PRICE_TYPE.MARKET_PRICE, + }, + { + label: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE, + value: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE, + }, + ], + }, + { + key: FILTER_KEYS[3], + label: "Cost", + className: "w-[85px]", + options: [ + { + label: COST_TYPE_SELECTOR.TOTAL, + value: COST_TYPE_SELECTOR.TOTAL, + }, + { + label: COST_TYPE_SELECTOR.NPV, + value: COST_TYPE_SELECTOR.NPV, + }, + ], + }, +] as const; + +export default function ParametersProjects() { + const [filters, setFilters] = useGlobalFilters(); + + const handleParameters = async ( + v: string, + parameter: keyof Omit, "keyword">, + ) => { + await setFilters((prev) => ({ ...prev, [parameter]: v })); + }; + + return ( +
+ {PROJECT_PARAMETERS.map((parameter) => ( +
+ + +
+ ))} +
+ ); +} diff --git a/client/src/containers/overview/table/view/overview/index.tsx b/client/src/containers/overview/table/view/overview/index.tsx index 2207b0c2..bc453f41 100644 --- a/client/src/containers/overview/table/view/overview/index.tsx +++ b/client/src/containers/overview/table/view/overview/index.tsx @@ -21,10 +21,7 @@ import { cn } from "@/lib/utils"; import { useGlobalFilters, useTableView } from "@/app/(overview)/url-store"; -import TablePagination, { - PAGINATION_SIZE_OPTIONS, -} from "@/components/ui/table-pagination"; - +import ProjectDetails from "@/containers/overview/project-details"; import { filtersToQueryParams, NO_DATA, @@ -42,10 +39,15 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import TablePagination, { + PAGINATION_SIZE_OPTIONS, +} from "@/components/ui/table-pagination"; export function OverviewTable() { const [tableView] = useTableView(); const [filters] = useGlobalFilters(); + const [projectName, setProjectName] = useState(""); + const [openDetails, setOpenDetails] = useState(false); const [sorting, setSorting] = useState([ { id: "projectName", @@ -107,6 +109,11 @@ export function OverviewTable() { return ( <> + {table.getHeaderGroups().map((headerGroup) => ( @@ -154,8 +161,13 @@ export function OverviewTable() { {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( { + setOpenDetails(true); + setProjectName(row.original.projectName ?? ""); + }} > {row.getVisibleCells().map((cell) => ( diff --git a/client/src/lib/format.tsx b/client/src/lib/format.tsx index 35bee11b..a1d0fef2 100644 --- a/client/src/lib/format.tsx +++ b/client/src/lib/format.tsx @@ -11,6 +11,24 @@ export const formatCurrency = ( }).format(value); }; +/** + * Parses a currency value into its symbol and amount components + * @param value - The numeric value to parse + * @param options - Optional Intl.NumberFormatOptions for currency formatting + * @returns A tuple containing [currencySymbol, amount] as strings + * @example + * parseCurrency(1234.56) // returns ['$', '1,234'] + */ +export const parseCurrency = ( + value: number, + options: Intl.NumberFormatOptions = {}, +) => { + let formatted = formatCurrency(value, options); + formatted = formatted.replace(/\.\d+/, ""); + const [symbol, amount] = formatted.match(/^(\D*)(.+)$/)!.slice(1); + return [symbol.trim(), amount]; +}; + export const formatNumber = ( value: number, options: Intl.NumberFormatOptions = {}, diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 198e19e5..b48f26ad 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -89,6 +89,16 @@ const config: Config = { "mint-green": { 200: "#70C69B" }, + "wheat": { + 200: "#EEE0BD" + }, + "yellow": { + 500: "#EACD3F" + }, + "high": "#B9CCA3", + "medium": "#F5EBB8", + "low": "#F7BA93", + "deep-ocean": "#132A47" }, borderRadius: { lg: "var(--radius)", From c94e98a1c3434939959c0defad46a83f8b3d2ed7 Mon Sep 17 00:00:00 2001 From: onehanddev Date: Thu, 21 Nov 2024 08:35:51 +0530 Subject: [PATCH 2/7] refactor: addressed review comments --- client/src/app/(overview)/store.ts | 5 + .../ui}/bar-chart/index.tsx | 14 +- .../overview/project-details/index.tsx | 145 +++++++++++------- .../project-details/parameters/index.tsx | 27 +++- .../overview/table/view/overview/index.tsx | 14 +- client/src/lib/format.tsx | 50 +++--- 6 files changed, 164 insertions(+), 91 deletions(-) rename client/src/{containers/overview/project-details => components/ui}/bar-chart/index.tsx (86%) diff --git a/client/src/app/(overview)/store.ts b/client/src/app/(overview)/store.ts index a87e2a5e..e817d2e1 100644 --- a/client/src/app/(overview)/store.ts +++ b/client/src/app/(overview)/store.ts @@ -12,3 +12,8 @@ export const popupAtom = atom<{ lngLat: MapMouseEvent["lngLat"]; features: MapMouseEvent["features"]; } | null>(null); + +export const projectDetailsAtom = atom<{ isOpen: boolean; projectName: string }>({ + isOpen: false, + projectName: "", +}); \ No newline at end of file diff --git a/client/src/containers/overview/project-details/bar-chart/index.tsx b/client/src/components/ui/bar-chart/index.tsx similarity index 86% rename from client/src/containers/overview/project-details/bar-chart/index.tsx rename to client/src/components/ui/bar-chart/index.tsx index b80de9d3..972983e6 100644 --- a/client/src/containers/overview/project-details/bar-chart/index.tsx +++ b/client/src/components/ui/bar-chart/index.tsx @@ -1,3 +1,5 @@ +import { toCompactAmount } from "@/lib/format"; + interface BarChartProps { total: number; segments: { @@ -18,10 +20,6 @@ const BarChart = ({ return `${Math.max(percentage, 0)}%`; }; - const formatAmount = (value: number) => { - return `${(value / 1_000_000).toFixed(1)}M`; - }; - if (orientation === "horizontal") { return (
@@ -36,8 +34,8 @@ const BarChart = ({ className={`relative h-full rounded transition-all duration-300 ease-in-out ${segment.colorClass}`} >
-
- ${formatAmount(segment.value)} +
+ ${toCompactAmount(segment.value)}
@@ -59,8 +57,8 @@ const BarChart = ({ className={`relative min-h-[30px] w-full transition-all duration-300 ease-in-out ${segment.colorClass}`} >
-
- ${formatAmount(segment.value)} +
+ ${toCompactAmount(segment.value)}
diff --git a/client/src/containers/overview/project-details/index.tsx b/client/src/containers/overview/project-details/index.tsx index 1b5869e2..f18ea63e 100644 --- a/client/src/containers/overview/project-details/index.tsx +++ b/client/src/containers/overview/project-details/index.tsx @@ -1,28 +1,22 @@ import { ChevronUp, ChevronDown, Plus } from "lucide-react"; +import { useAtom } from "jotai"; -import { parseCurrency, formatCurrency } from "@/lib/format"; +import { projectDetailsAtom } from "@/app/(overview)/store"; +import { renderCurrency, formatCurrency, renderAbatementCurrency } from "@/lib/format"; -import BarChart from "@/containers/overview/project-details/bar-chart"; +import BarChart from "@/components/ui/bar-chart"; import ParametersProjects from "@/containers/overview/project-details/parameters"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; -import InfoButton from "@/components/ui/info-button"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; +import { Label } from "@/components/ui/label"; -const Currency = ({ value }: { value: number }) => { - const [symbol, amount] = parseCurrency(value); - return ( - - {symbol} {amount} - - ); -}; //////// ScoreIndicator component //////// interface ScoreIndicatorProps { @@ -119,19 +113,14 @@ const projectData = { ], }; -interface IProjectDetailsProps { - projectName: string; - open: boolean; - setIsOpen: (open: boolean) => void; -} +export default function ProjectDetails() { + const [projectDetails, setProjectDetails] = useAtom(projectDetailsAtom); + + const handleOpenDetails = (open: boolean) => + setProjectDetails({ ...projectDetails, isOpen: open }); -export default function ProjectDetails({ - projectName, - open, - setIsOpen, -}: IProjectDetailsProps) { return ( - +
@@ -150,7 +139,7 @@ export default function ProjectDetails({
- {projectName} + {projectDetails.projectName}
@@ -162,20 +151,29 @@ export default function ProjectDetails({
-

Total project cost

- + +
-
+
Refers to the summary of Capital Expenditure and Operating Expenditure
-
+
- + {renderCurrency(projectData.totalCost)}
@@ -216,20 +214,28 @@ export default function ProjectDetails({
-

Total project cost

- +
-
+
Refers to the summary of Capital Expenditure and Operating Expenditure
-
+
- + {renderCurrency(projectData.totalCost)}
@@ -270,14 +276,22 @@ export default function ProjectDetails({
-

Abatement potential

- +
- + {renderAbatementCurrency(projectData.abatement)}
-
+

Estimation of total CO2 expected during the project.

@@ -285,8 +299,16 @@ export default function ProjectDetails({
-

Overall score

- +
@@ -297,16 +319,22 @@ export default function ProjectDetails({
- +
-

Scorecard ratings

- +
- +
-
- {projectData.scorecard.map((item) => ( +
+ {projectData.scorecard.map((item, index) => ( <> + {index === 0 &&
}
{item.name}
-
+ {projectData.scorecard.length !== index + 1 &&
} ))}
-
+
-

Cost estimates

- +
- +
-
+
{projectData.costEstimates.map((estimate) => (
diff --git a/client/src/containers/overview/project-details/parameters/index.tsx b/client/src/containers/overview/project-details/parameters/index.tsx index 4a0049c2..3ae2dc2e 100644 --- a/client/src/containers/overview/project-details/parameters/index.tsx +++ b/client/src/containers/overview/project-details/parameters/index.tsx @@ -4,9 +4,9 @@ import { PROJECT_PRICE_TYPE, } from "@shared/entities/projects.entity"; import { z } from "zod"; +import { parseAsJson, useQueryState } from "nuqs"; -import { FILTER_KEYS } from "@/app/(overview)/constants"; -import { useGlobalFilters } from "@/app/(overview)/url-store"; +import { FILTER_KEYS, INITIAL_ABATEMENT_POTENTIAL_RANGE, INITIAL_COST_RANGE } from "@/app/(overview)/constants"; import { filtersSchema } from "@/app/(overview)/url-store"; import { Label } from "@/components/ui/label"; @@ -17,6 +17,20 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; + +const INITIAL_FILTERS_STATE: z.infer = { + keyword: "", + projectSizeFilter: PROJECT_SIZE_FILTER.MEDIUM, + priceType: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE, + costRangeSelector: COST_TYPE_SELECTOR.NPV, + countryCode: "", + ecosystem: [], + activity: [], + activitySubtype: [], + costRange: INITIAL_COST_RANGE, + abatementPotentialRange: INITIAL_ABATEMENT_POTENTIAL_RANGE, +}; + export const PROJECT_PARAMETERS = [ { key: FILTER_KEYS[1], @@ -69,8 +83,15 @@ export const PROJECT_PARAMETERS = [ }, ] as const; +function useFilters() { + return useQueryState( + "filters", + parseAsJson(filtersSchema.parse).withDefault(INITIAL_FILTERS_STATE), + ); +} + export default function ParametersProjects() { - const [filters, setFilters] = useGlobalFilters(); + const [filters, setFilters] = useFilters(); const handleParameters = async ( v: string, diff --git a/client/src/containers/overview/table/view/overview/index.tsx b/client/src/containers/overview/table/view/overview/index.tsx index bc453f41..19900de2 100644 --- a/client/src/containers/overview/table/view/overview/index.tsx +++ b/client/src/containers/overview/table/view/overview/index.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"; import { projectsQuerySchema } from "@shared/contracts/projects.contract"; import { keepPreviousData } from "@tanstack/react-query"; +import { useAtom } from "jotai"; import { flexRender, getCoreRowModel, @@ -21,6 +22,7 @@ import { cn } from "@/lib/utils"; import { useGlobalFilters, useTableView } from "@/app/(overview)/url-store"; +import { projectDetailsAtom } from "@/app/(overview)/store"; import ProjectDetails from "@/containers/overview/project-details"; import { filtersToQueryParams, @@ -46,8 +48,7 @@ import TablePagination, { export function OverviewTable() { const [tableView] = useTableView(); const [filters] = useGlobalFilters(); - const [projectName, setProjectName] = useState(""); - const [openDetails, setOpenDetails] = useState(false); + const [, setProjectDetails] = useAtom(projectDetailsAtom); const [sorting, setSorting] = useState([ { id: "projectName", @@ -110,9 +111,6 @@ export function OverviewTable() { return ( <>
@@ -165,8 +163,10 @@ export function OverviewTable() { key={row.id} data-state={row.getIsSelected() && "selected"} onClick={() => { - setOpenDetails(true); - setProjectName(row.original.projectName ?? ""); + setProjectDetails({ + isOpen: true, + projectName: row.original.projectName ?? "", + }); }} > {row.getVisibleCells().map((cell) => ( diff --git a/client/src/lib/format.tsx b/client/src/lib/format.tsx index a1d0fef2..6c182a4e 100644 --- a/client/src/lib/format.tsx +++ b/client/src/lib/format.tsx @@ -11,24 +11,6 @@ export const formatCurrency = ( }).format(value); }; -/** - * Parses a currency value into its symbol and amount components - * @param value - The numeric value to parse - * @param options - Optional Intl.NumberFormatOptions for currency formatting - * @returns A tuple containing [currencySymbol, amount] as strings - * @example - * parseCurrency(1234.56) // returns ['$', '1,234'] - */ -export const parseCurrency = ( - value: number, - options: Intl.NumberFormatOptions = {}, -) => { - let formatted = formatCurrency(value, options); - formatted = formatted.replace(/\.\d+/, ""); - const [symbol, amount] = formatted.match(/^(\D*)(.+)$/)!.slice(1); - return [symbol.trim(), amount]; -}; - export const formatNumber = ( value: number, options: Intl.NumberFormatOptions = {}, @@ -39,6 +21,24 @@ export const formatNumber = ( }).format(value); }; +export const renderAbatementCurrency = ( + value: number, + options: Intl.NumberFormatOptions = {}, +) => { + let formatted = formatCurrency(value, options); + formatted = formatted.replace(/\.\d+/, ""); + const [, amount] = formatted.match(/^(\D*)(.+)$/)!.slice(1); + return ( + <> + + tCO2e/yr   + + {amount} + ); +}; + export function renderCurrency( value: number, options: Intl.NumberFormatOptions = {}, @@ -55,3 +55,17 @@ export function renderCurrency( ); } + +/** + * Converts a large numeric value into a compact format with an "M" suffix + * representing millions. + * + * @param {number} value - The numeric value to be converted. + * @returns {string} - The formatted string representing the value in millions with one decimal place. + * + * @example + * toCompactAmount(38023789); // Returns "38.0M" + */ +export const toCompactAmount = (value: number) => { + return `${(value / 1_000_000).toFixed(1)}M`; +}; From e6775ca82c67c5167fc54fce84014af847108f79 Mon Sep 17 00:00:00 2001 From: onehanddev Date: Thu, 21 Nov 2024 08:38:10 +0530 Subject: [PATCH 3/7] fix: lint --- client/src/app/(overview)/store.ts | 7 +- .../overview/project-details/index.tsx | 117 ++++++++++-------- .../project-details/parameters/index.tsx | 8 +- .../overview/table/view/overview/index.tsx | 7 +- client/src/lib/format.tsx | 15 ++- 5 files changed, 87 insertions(+), 67 deletions(-) diff --git a/client/src/app/(overview)/store.ts b/client/src/app/(overview)/store.ts index e817d2e1..0d7fe945 100644 --- a/client/src/app/(overview)/store.ts +++ b/client/src/app/(overview)/store.ts @@ -13,7 +13,10 @@ export const popupAtom = atom<{ features: MapMouseEvent["features"]; } | null>(null); -export const projectDetailsAtom = atom<{ isOpen: boolean; projectName: string }>({ +export const projectDetailsAtom = atom<{ + isOpen: boolean; + projectName: string; +}>({ isOpen: false, projectName: "", -}); \ No newline at end of file +}); diff --git a/client/src/containers/overview/project-details/index.tsx b/client/src/containers/overview/project-details/index.tsx index f18ea63e..524c1b14 100644 --- a/client/src/containers/overview/project-details/index.tsx +++ b/client/src/containers/overview/project-details/index.tsx @@ -1,22 +1,26 @@ -import { ChevronUp, ChevronDown, Plus } from "lucide-react"; import { useAtom } from "jotai"; +import { ChevronUp, ChevronDown, Plus } from "lucide-react"; + +import { + renderCurrency, + formatCurrency, + renderAbatementCurrency, +} from "@/lib/format"; import { projectDetailsAtom } from "@/app/(overview)/store"; -import { renderCurrency, formatCurrency, renderAbatementCurrency } from "@/lib/format"; -import BarChart from "@/components/ui/bar-chart"; import ParametersProjects from "@/containers/overview/project-details/parameters"; +import BarChart from "@/components/ui/bar-chart"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; -import { Label } from "@/components/ui/label"; - //////// ScoreIndicator component //////// interface ScoreIndicatorProps { @@ -151,13 +155,13 @@ export default function ProjectDetails() {
-
-
+
@@ -216,10 +220,11 @@ export default function ProjectDetails() {
-
+
@@ -277,15 +282,16 @@ export default function ProjectDetails() {
+ htmlFor="abatementPotential" + className="text-md font-medium" + tooltip={{ + title: "Abatement potential", + content: + "Refers to the summary of Capital Expenditure and Operating Expenditure", + }} + > +

Abatement potential

+
{renderAbatementCurrency(projectData.abatement)} @@ -300,15 +306,16 @@ export default function ProjectDetails() {
+ htmlFor="overallScore" + className="text-md font-medium" + tooltip={{ + title: "Overall score", + content: + "Refers to the summary of Capital Expenditure and Operating Expenditure", + }} + > +

Overall score

+
@@ -323,18 +330,21 @@ export default function ProjectDetails() {
+ htmlFor="scorecardRatings" + className="text-md font-medium" + tooltip={{ + title: "Scorecard ratings", + content: + "Refers to the summary of Capital Expenditure and Operating Expenditure", + }} + > +

Scorecard ratings

+
- +
- {projectData.scorecard.length !== index + 1 &&
} + {projectData.scorecard.length !== index + 1 && ( +
+ )} ))}
@@ -364,18 +376,21 @@ export default function ProjectDetails() {
+ htmlFor="costEstimates" + className="text-md font-medium" + tooltip={{ + title: "Cost estimates", + content: + "Refers to the summary of Capital Expenditure and Operating Expenditure", + }} + > +

Cost estimates

+
- +
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/client/src/lib/format.tsx b/client/src/lib/format.tsx index 6c182a4e..ed7046eb 100644 --- a/client/src/lib/format.tsx +++ b/client/src/lib/format.tsx @@ -30,13 +30,12 @@ export const renderAbatementCurrency = ( const [, amount] = formatted.match(/^(\D*)(.+)$/)!.slice(1); return ( <> - - tCO2e/yr   - - {amount} - ); + + tCO2e/yr   + + {amount} + + ); }; export function renderCurrency( @@ -57,7 +56,7 @@ export function renderCurrency( } /** - * Converts a large numeric value into a compact format with an "M" suffix + * Converts a large numeric value into a compact format with an "M" suffix * representing millions. * * @param {number} value - The numeric value to be converted. From 158f11dfc9c02179077eaf1428c3c05a1558aa4d Mon Sep 17 00:00:00 2001 From: onehanddev Date: Sun, 24 Nov 2024 12:51:51 +0530 Subject: [PATCH 4/7] fix: added create custom project modal for project details page --- .../overview/project-details/index.tsx | 57 ++++++++++++++++++- .../project-details/parameters/index.tsx | 14 ++--- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/client/src/containers/overview/project-details/index.tsx b/client/src/containers/overview/project-details/index.tsx index 524c1b14..65592e5d 100644 --- a/client/src/containers/overview/project-details/index.tsx +++ b/client/src/containers/overview/project-details/index.tsx @@ -1,5 +1,6 @@ import { useAtom } from "jotai"; -import { ChevronUp, ChevronDown, Plus } from "lucide-react"; +import Link from "next/link"; +import { ChevronUp, ChevronDown, Plus, NotebookPen } from "lucide-react"; import { renderCurrency, @@ -21,6 +22,45 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; +import { + Dialog, + DialogContent, + DialogClose, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +const CreateProjectDetails = () => ( + + + + + + + + Create a Custom Project + + + By creating a custom project you will generate a customizable version + where you can edit all parameters to fit your specific needs. + + + + + + + + + +); //////// ScoreIndicator component //////// interface ScoreIndicatorProps { @@ -149,7 +189,7 @@ export default function ProjectDetails() { -
+
@@ -430,6 +470,19 @@ export default function ProjectDetails() {
+ +
+
+
+ Values considered for a{" "} + small project (40 ha). +
+
For more detailed analysis, create a custom project.
+
+
+ +
+
); diff --git a/client/src/containers/overview/project-details/parameters/index.tsx b/client/src/containers/overview/project-details/parameters/index.tsx index c3025dfd..83363e1f 100644 --- a/client/src/containers/overview/project-details/parameters/index.tsx +++ b/client/src/containers/overview/project-details/parameters/index.tsx @@ -1,9 +1,9 @@ +import { atom, useAtom } from "jotai"; import { PROJECT_SIZE_FILTER, COST_TYPE_SELECTOR, PROJECT_PRICE_TYPE, } from "@shared/entities/projects.entity"; -import { parseAsJson, useQueryState } from "nuqs"; import { z } from "zod"; import { @@ -35,6 +35,9 @@ const INITIAL_FILTERS_STATE: z.infer = { abatementPotentialRange: INITIAL_ABATEMENT_POTENTIAL_RANGE, }; +// Create a Jotai atom with the initial state +const filtersAtom = atom(INITIAL_FILTERS_STATE); + export const PROJECT_PARAMETERS = [ { key: FILTER_KEYS[1], @@ -88,20 +91,17 @@ export const PROJECT_PARAMETERS = [ ] as const; function useFilters() { - return useQueryState( - "filters", - parseAsJson(filtersSchema.parse).withDefault(INITIAL_FILTERS_STATE), - ); + return useAtom(filtersAtom); } export default function ParametersProjects() { const [filters, setFilters] = useFilters(); - const handleParameters = async ( + const handleParameters = ( v: string, parameter: keyof Omit, "keyword">, ) => { - await setFilters((prev) => ({ ...prev, [parameter]: v })); + setFilters((prev) => ({ ...prev, [parameter]: v })); }; return ( From 5c07e7c65ebb3ae84c262de3136b24e913c58460 Mon Sep 17 00:00:00 2001 From: onehanddev Date: Mon, 25 Nov 2024 18:18:07 +0530 Subject: [PATCH 5/7] fix: addressed PR comments. --- .../src/containers/overview/project-details/index.tsx | 9 ++++----- .../overview/project-details/parameters/index.tsx | 10 +--------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/client/src/containers/overview/project-details/index.tsx b/client/src/containers/overview/project-details/index.tsx index 65592e5d..fdc0455d 100644 --- a/client/src/containers/overview/project-details/index.tsx +++ b/client/src/containers/overview/project-details/index.tsx @@ -165,7 +165,7 @@ export default function ProjectDetails() { return ( - +
@@ -189,7 +189,7 @@ export default function ProjectDetails() { -
+
@@ -442,7 +442,7 @@ export default function ProjectDetails() {
{projectData.costEstimates.map((estimate) => (
-
+
{estimate.name}
@@ -464,14 +464,13 @@ export default function ProjectDetails() {
))} -
))}
-
+
Values considered for a{" "} diff --git a/client/src/containers/overview/project-details/parameters/index.tsx b/client/src/containers/overview/project-details/parameters/index.tsx index 83363e1f..ba27da62 100644 --- a/client/src/containers/overview/project-details/parameters/index.tsx +++ b/client/src/containers/overview/project-details/parameters/index.tsx @@ -22,20 +22,12 @@ import { SelectValue, } from "@/components/ui/select"; -const INITIAL_FILTERS_STATE: z.infer = { - keyword: "", +const INITIAL_FILTERS_STATE: Partial> = { projectSizeFilter: PROJECT_SIZE_FILTER.MEDIUM, priceType: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE, - costRangeSelector: COST_TYPE_SELECTOR.NPV, - countryCode: "", - ecosystem: [], - activity: [], - activitySubtype: [], costRange: INITIAL_COST_RANGE, - abatementPotentialRange: INITIAL_ABATEMENT_POTENTIAL_RANGE, }; -// Create a Jotai atom with the initial state const filtersAtom = atom(INITIAL_FILTERS_STATE); export const PROJECT_PARAMETERS = [ From 9b91845e4c42acb774e00589e3667dcdccd9219b Mon Sep 17 00:00:00 2001 From: onehanddev Date: Mon, 25 Nov 2024 18:25:30 +0530 Subject: [PATCH 6/7] fix: lint --- .../overview/project-details/index.tsx | 21 ++++++++++--------- .../project-details/parameters/index.tsx | 8 ++----- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/client/src/containers/overview/project-details/index.tsx b/client/src/containers/overview/project-details/index.tsx index fdc0455d..7c0cf318 100644 --- a/client/src/containers/overview/project-details/index.tsx +++ b/client/src/containers/overview/project-details/index.tsx @@ -1,5 +1,6 @@ -import { useAtom } from "jotai"; import Link from "next/link"; + +import { useAtom } from "jotai"; import { ChevronUp, ChevronDown, Plus, NotebookPen } from "lucide-react"; import { @@ -15,13 +16,6 @@ import ParametersProjects from "@/containers/overview/project-details/parameters import BarChart from "@/components/ui/bar-chart"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; import { Dialog, DialogContent, @@ -32,6 +26,13 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; const CreateProjectDetails = () => ( @@ -165,7 +166,7 @@ export default function ProjectDetails() { return ( - +
@@ -470,7 +471,7 @@ export default function ProjectDetails() {
-
+
Values considered for a{" "} diff --git a/client/src/containers/overview/project-details/parameters/index.tsx b/client/src/containers/overview/project-details/parameters/index.tsx index ba27da62..aa47aa74 100644 --- a/client/src/containers/overview/project-details/parameters/index.tsx +++ b/client/src/containers/overview/project-details/parameters/index.tsx @@ -1,16 +1,12 @@ -import { atom, useAtom } from "jotai"; import { PROJECT_SIZE_FILTER, COST_TYPE_SELECTOR, PROJECT_PRICE_TYPE, } from "@shared/entities/projects.entity"; +import { atom, useAtom } from "jotai"; import { z } from "zod"; -import { - FILTER_KEYS, - INITIAL_ABATEMENT_POTENTIAL_RANGE, - INITIAL_COST_RANGE, -} from "@/app/(overview)/constants"; +import { FILTER_KEYS, INITIAL_COST_RANGE } from "@/app/(overview)/constants"; import { filtersSchema } from "@/app/(overview)/url-store"; import { Label } from "@/components/ui/label"; From e4d9890d2019a27de7a7812a53dfa6f432f4acad Mon Sep 17 00:00:00 2001 From: onehanddev Date: Tue, 26 Nov 2024 16:09:00 +0530 Subject: [PATCH 7/7] fix: Cost filter default value --- .../containers/overview/project-details/parameters/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/containers/overview/project-details/parameters/index.tsx b/client/src/containers/overview/project-details/parameters/index.tsx index aa47aa74..afd78739 100644 --- a/client/src/containers/overview/project-details/parameters/index.tsx +++ b/client/src/containers/overview/project-details/parameters/index.tsx @@ -6,7 +6,7 @@ import { import { atom, useAtom } from "jotai"; import { z } from "zod"; -import { FILTER_KEYS, INITIAL_COST_RANGE } from "@/app/(overview)/constants"; +import { FILTER_KEYS } from "@/app/(overview)/constants"; import { filtersSchema } from "@/app/(overview)/url-store"; import { Label } from "@/components/ui/label"; @@ -21,7 +21,7 @@ import { const INITIAL_FILTERS_STATE: Partial> = { projectSizeFilter: PROJECT_SIZE_FILTER.MEDIUM, priceType: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE, - costRange: INITIAL_COST_RANGE, + costRangeSelector: COST_TYPE_SELECTOR.NPV, }; const filtersAtom = atom(INITIAL_FILTERS_STATE);