diff --git a/client/src/app/(overview)/store.ts b/client/src/app/(overview)/store.ts
index a87e2a5e..0d7fe945 100644
--- a/client/src/app/(overview)/store.ts
+++ b/client/src/app/(overview)/store.ts
@@ -12,3 +12,11 @@ export const popupAtom = atom<{
lngLat: MapMouseEvent["lngLat"];
features: MapMouseEvent["features"];
} | null>(null);
+
+export const projectDetailsAtom = atom<{
+ isOpen: boolean;
+ projectName: string;
+}>({
+ isOpen: false,
+ projectName: "",
+});
diff --git a/client/src/components/ui/bar-chart/index.tsx b/client/src/components/ui/bar-chart/index.tsx
new file mode 100644
index 00000000..972983e6
--- /dev/null
+++ b/client/src/components/ui/bar-chart/index.tsx
@@ -0,0 +1,71 @@
+import { toCompactAmount } from "@/lib/format";
+
+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)}%`;
+ };
+
+ if (orientation === "horizontal") {
+ return (
+
+
+ {segments.map((segment, index) => (
+
+
+
+ ${toCompactAmount(segment.value)}
+
+
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+
+ {segments.map((segment, index) => (
+
+
+
+ ${toCompactAmount(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..7c0cf318
--- /dev/null
+++ b/client/src/containers/overview/project-details/index.tsx
@@ -0,0 +1,489 @@
+import Link from "next/link";
+
+import { useAtom } from "jotai";
+import { ChevronUp, ChevronDown, Plus, NotebookPen } from "lucide-react";
+
+import {
+ renderCurrency,
+ formatCurrency,
+ renderAbatementCurrency,
+} from "@/lib/format";
+
+import { projectDetailsAtom } from "@/app/(overview)/store";
+
+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 {
+ Dialog,
+ DialogContent,
+ DialogClose,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+
+const CreateProjectDetails = () => (
+
+);
+
+//////// 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 (
+
+ );
+};
+
+// 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 },
+ ],
+};
+
+export default function ProjectDetails() {
+ const [projectDetails, setProjectDetails] = useAtom(projectDetailsAtom);
+
+ const handleOpenDetails = (open: boolean) =>
+ setProjectDetails({ ...projectDetails, isOpen: open });
+
+ return (
+
+
+
+
+
+
+
+
+
+ {projectDetails.projectName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Refers to the summary of Capital Expenditure and Operating
+ Expenditure
+
+
+
+
+
+
+
+ {renderCurrency(projectData.totalCost)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Refers to the summary of Capital Expenditure and Operating
+ Expenditure
+
+
+
+
+
+
+
+ {renderCurrency(projectData.totalCost)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {renderAbatementCurrency(projectData.abatement)}
+
+
+
+
Estimation of total CO2 expected during the project.
+
+
+
+
+
+
+
+
+
+
+
+ {projectData.overallScore}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {projectData.scorecard.map((item, index) => (
+ <>
+ {index === 0 &&
}
+
+ {projectData.scorecard.length !== index + 1 && (
+
+ )}
+ >
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {projectData.costEstimates.map((estimate) => (
+
+
+
+ {estimate.name}
+
+
+
+ {formatCurrency(estimate.value)}
+
+
+
+ {estimate.items?.map((item) => (
+
+
+ {item.name}
+
+
+
+ {formatCurrency(item.value)}
+
+
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+ 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
new file mode 100644
index 00000000..afd78739
--- /dev/null
+++ b/client/src/containers/overview/project-details/parameters/index.tsx
@@ -0,0 +1,122 @@
+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 } from "@/app/(overview)/constants";
+import { filtersSchema } from "@/app/(overview)/url-store";
+
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+const INITIAL_FILTERS_STATE: Partial> = {
+ projectSizeFilter: PROJECT_SIZE_FILTER.MEDIUM,
+ priceType: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE,
+ costRangeSelector: COST_TYPE_SELECTOR.NPV,
+};
+
+const filtersAtom = atom(INITIAL_FILTERS_STATE);
+
+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;
+
+function useFilters() {
+ return useAtom(filtersAtom);
+}
+
+export default function ParametersProjects() {
+ const [filters, setFilters] = useFilters();
+
+ const handleParameters = (
+ v: string,
+ parameter: keyof Omit, "keyword">,
+ ) => {
+ 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..3d41b2d6 100644
--- a/client/src/containers/overview/table/view/overview/index.tsx
+++ b/client/src/containers/overview/table/view/overview/index.tsx
@@ -12,6 +12,7 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
+import { useAtom } from "jotai";
import { ChevronsUpDownIcon } from "lucide-react";
import { z } from "zod";
@@ -19,12 +20,10 @@ import { client } from "@/lib/query-client";
import { queryKeys } from "@/lib/query-keys";
import { cn } from "@/lib/utils";
+import { projectDetailsAtom } from "@/app/(overview)/store";
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 +41,14 @@ 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 [, setProjectDetails] = useAtom(projectDetailsAtom);
const [sorting, setSorting] = useState([
{
id: "projectName",
@@ -107,6 +110,7 @@ export function OverviewTable() {
return (
<>
+
{table.getHeaderGroups().map((headerGroup) => (
@@ -154,8 +158,15 @@ export function OverviewTable() {
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
{
+ 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 35bee11b..ed7046eb 100644
--- a/client/src/lib/format.tsx
+++ b/client/src/lib/format.tsx
@@ -21,6 +21,23 @@ 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 = {},
@@ -37,3 +54,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`;
+};
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)",