Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…ost-tool into feature/TBCCT-123-implement-custom-projects-page
  • Loading branch information
onehanddev committed Nov 14, 2024
2 parents d4b6e17 + 5d26ba5 commit 3e67f0a
Show file tree
Hide file tree
Showing 45 changed files with 714 additions and 247 deletions.
13 changes: 12 additions & 1 deletion api/src/modules/custom-projects/dto/create-custom-project-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { ACTIVITY } from '@shared/entities/activity.enum';
import { ECOSYSTEM } from '@shared/entities/ecosystem.enum';
import { ConservationProjectParamDto } from '@api/modules/custom-projects/dto/conservation-project-params.dto';
import { RestorationProjectParamsDto } from '@api/modules/custom-projects/dto/restoration-project-params.dto';
import { CustomProjectAssumptionsDto } from '@api/modules/custom-projects/dto/project-assumptions.dto';
import { CustomProjectCostInputsDto } from '@api/modules/custom-projects/dto/project-cost-inputs.dto';
import { ProjectParamsValidator } from '@api/modules/custom-projects/validation/project-params.validator';
import { Transform } from 'class-transformer';

Expand Down Expand Up @@ -41,7 +43,16 @@ export class CreateCustomProjectDto {
@IsEnum(CARBON_REVENUES_TO_COVER)
carbonRevenuesToCover: CARBON_REVENUES_TO_COVER;

@IsNotEmpty()
@IsNotEmpty({
message: 'Assumptions are required to create a custom project',
})
assumptions: CustomProjectAssumptionsDto;

@IsNotEmpty({
message: 'Cost inputs are required to create a custom project',
})
costInputs: CustomProjectCostInputsDto;

@IsNotEmpty()
@Transform(injectEcosystemToParams)
@Validate(ProjectParamsValidator)
Expand Down
24 changes: 24 additions & 0 deletions api/src/modules/custom-projects/dto/project-assumptions.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IsNumber } from 'class-validator';

export class CustomProjectAssumptionsDto {
@IsNumber()
verificationFrequency: number;

@IsNumber()
discountRate: number;

@IsNumber()
carbonPriceIncrease: number;

@IsNumber()
buffer: number;

@IsNumber()
baselineReassessmentFrequency: number;

@IsNumber()
restorationRate: number;

@IsNumber()
restorationProjectLength: number;
}
60 changes: 60 additions & 0 deletions api/src/modules/custom-projects/dto/project-cost-inputs.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { CustomProject } from '@shared/entities/custom-project.entity';
import { IsNumber } from 'class-validator';

export class CustomProjectCapexCostInputsDto {
@IsNumber()
feasibilityAnalisys: number;

@IsNumber()
conservationPlanningAndAdmin: number;

@IsNumber()
dataCollectionAndFieldCost: number;

@IsNumber()
communityRepresentation: number;

@IsNumber()
blueCarbonProjectPlanning: number;

@IsNumber()
establishingCarbonRights: number;

@IsNumber()
validation: number;

@IsNumber()
implementationLabor: number;
}

export class CustomProjectOpexCostInputsDto {
@IsNumber()
monitoring: number;

@IsNumber()
maintenance: number;

@IsNumber()
communityBenefitShsharingFund: number;

@IsNumber()
carbonStandardFees: number;

@IsNumber()
baselineReassessment: number;

@IsNumber()
mrv: number;

@IsNumber()
longTermProjectOperating: number;
}

export class CustomProjectCostInputsDto {
@IsNumber()
financingCost: number;

capexCostInputs: CustomProjectCapexCostInputsDto;

opexCostInputs: CustomProjectOpexCostInputsDto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('Create Custom Projects - Request Validations', () => {
.post(customProjectContract.createCustomProject.path)
.send({});

expect(response.body.errors).toHaveLength(10);
expect(response.body.errors).toHaveLength(12);
});
});
describe('Conservation Project Validations', () => {
Expand Down
6 changes: 4 additions & 2 deletions client/src/app/(overview)/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ export const FILTER_KEYS = [

// ? cost and abatement potential ranges are hardcoded for now.
// ? Ask data to figure out the correct values and hardcore them here.
export const INITIAL_COST_RANGE = [1200, 2300];
export const INITIAL_ABATEMENT_POTENTIAL_RANGE = [0, 100];
// export const INITIAL_COST_RANGE = [1200, 2300];
// export const INITIAL_ABATEMENT_POTENTIAL_RANGE = [0, 100];
export const INITIAL_COST_RANGE = [0, 120494811];
export const INITIAL_ABATEMENT_POTENTIAL_RANGE = [0, 10199224];
90 changes: 3 additions & 87 deletions client/src/app/(overview)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,5 @@
"use client";
import Overview from "@/containers/overview";

import { useMap } from "react-map-gl";

import { motion } from "framer-motion";
import { useAtomValue } from "jotai";

import { LAYOUT_TRANSITIONS } from "@/app/(overview)/constants";
import { projectsUIState } from "@/app/(overview)/store";

import ProjectsFilters, {
FILTERS_SIDEBAR_WIDTH,
} from "@/containers/projects/filters";
import ProjectsHeader from "@/containers/projects/header";
import ProjectsMap from "@/containers/projects/map";

import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { useSidebar } from "@/components/ui/sidebar";
import ProjectsTable from "src/containers/projects/table";

const PANEL_MIN_SIZE = 25;
const PANEL_DEFAULT_SIZE = 50;

export default function Projects() {
const { filtersOpen } = useAtomValue(projectsUIState);
const { open: navOpen } = useSidebar();
const { default: map } = useMap();

const onResizeMapPanel = () => {
if (!map) return;

map.resize();
};

return (
<motion.div
layout
layoutDependency={navOpen}
className="flex flex-1"
transition={LAYOUT_TRANSITIONS}
>
<motion.aside
layout
initial={filtersOpen ? "open" : "closed"}
animate={filtersOpen ? "open" : "closed"}
variants={{
open: {
width: FILTERS_SIDEBAR_WIDTH,
},
closed: {
width: 0,
},
}}
transition={LAYOUT_TRANSITIONS}
className="overflow-hidden"
>
<ProjectsFilters />
</motion.aside>
<div className="mx-3 flex flex-1 flex-col">
<ProjectsHeader />
<ResizablePanelGroup
direction="vertical"
className="grid flex-grow grid-rows-2"
>
<ResizablePanel
className="flex flex-1 flex-col"
minSize={PANEL_MIN_SIZE}
onResize={onResizeMapPanel}
defaultSize={PANEL_DEFAULT_SIZE}
>
<ProjectsMap />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
className="mb-4 flex flex-1 flex-col"
minSize={PANEL_MIN_SIZE}
defaultSize={PANEL_DEFAULT_SIZE}
>
<ProjectsTable />
</ResizablePanel>
</ResizablePanelGroup>
</div>
</motion.div>
);
export default function OverviewPage() {
return <Overview />;
}
7 changes: 7 additions & 0 deletions client/src/app/(overview)/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { MapMouseEvent } from "react-map-gl";

import { atom } from "jotai";

export const projectsUIState = atom<{
filtersOpen: boolean;
}>({
filtersOpen: false,
});

export const popupAtom = atom<{
lngLat: MapMouseEvent["lngLat"];
features: MapMouseEvent["features"];
} | null>(null);
4 changes: 2 additions & 2 deletions client/src/app/(overview)/url-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
INITIAL_ABATEMENT_POTENTIAL_RANGE,
} from "@/app/(overview)/constants";

import { TABLE_VIEWS } from "@/containers/projects/table/toolbar/table-selector";
import { TABLE_VIEWS } from "@/containers/overview/table/toolbar/table-selector";

const SUB_ACTIVITIES = RESTORATION_ACTIVITY_SUBTYPE;

Expand All @@ -37,7 +37,7 @@ export const filtersSchema = z.object({
export const INITIAL_FILTERS_STATE: z.infer<typeof filtersSchema> = {
keyword: "",
projectSizeFilter: PROJECT_SIZE_FILTER.MEDIUM,
priceType: PROJECT_PRICE_TYPE.MARKET_PRICE,
priceType: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE,
costRangeSelector: COST_TYPE_SELECTOR.NPV,
countryCode: "",
ecosystem: [],
Expand Down
32 changes: 32 additions & 0 deletions client/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,35 @@ body {
@apply bg-background text-foreground antialiased;
}
}

/* mapbox styles */
.mapboxgl-popup-anchor-top {
.mapboxgl-popup-tip {
@apply !border-b-popover !z-10 relative;
}
}

.mapboxgl-popup-anchor-bottom {
.mapboxgl-popup-tip {
@apply !border-t-popover !z-10 relative;
}
}

.mapboxgl-popup-anchor-left {
.mapboxgl-popup-tip {
@apply !border-r-popover !z-10 relative;
}
}

.mapboxgl-popup-anchor-right {
.mapboxgl-popup-tip {
@apply !border-l-popover !z-10 relative;
}
}

.mapboxgl-popup-content {
@apply !bg-popover !rounded-md border border-border text-big-stone-50 shadow-md;
}
.mapboxgl-popup-close-button {
@apply hidden;
}
28 changes: 26 additions & 2 deletions client/src/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ export default function Map({
};
}, [bounds, isFlying]);

useEffect(() => {
if (!mapRef || !mapboxProps.interactiveLayerIds?.length) return;

const setPointerStyle = () => {
mapRef.getCanvas().style.cursor = "pointer";
};

const removePointerStyle = () => {
mapRef.getCanvas().style.cursor = "";
};

mapboxProps.interactiveLayerIds.forEach((layerId) => {
mapRef.on("mouseenter", layerId, setPointerStyle);
mapRef.on("mouseleave", layerId, removePointerStyle);
});

return () => {
if (!mapRef || !mapboxProps.interactiveLayerIds?.length) return;

mapboxProps.interactiveLayerIds.forEach((layerId) => {
mapRef.off("mouseenter", layerId, setPointerStyle);
mapRef.off("mouseleave", layerId, removePointerStyle);
});
};
}, [mapRef, mapboxProps]);

return (
<div className={cn("relative z-0 h-full w-full", className)}>
<ReactMapGL
Expand All @@ -152,8 +178,6 @@ export default function Map({
onMove={handleMapMove}
onLoad={handleMapLoad}
mapStyle={MAPBOX_STYLE}
maxZoom={10}
minZoom={0}
{...mapboxProps}
{...localViewState}
>
Expand Down
39 changes: 39 additions & 0 deletions client/src/components/map/popup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { PropsWithChildren } from "react";
import * as React from "react";

import { Popup } from "react-map-gl";

import { useAtom } from "jotai";
import { XIcon } from "lucide-react";

import { popupAtom } from "@/app/(overview)/store";

export default function MapPopup({ children }: PropsWithChildren) {
const [popup, setPopup] = useAtom(popupAtom);

if (!popup || !popup.features?.length) {
return null;
}

const closePopup = () => {
setPopup(null);
};

return (
<Popup
longitude={popup.lngLat.lng}
latitude={popup.lngLat.lat}
closeOnClick={false}
onClose={closePopup}
className="bg-transparent text-sm"
maxWidth="320"
>
<div className="flex flex-col">
<button type="button" onClick={closePopup} className="self-end">
<XIcon className="h-4 w-4 text-foreground hover:text-muted-foreground" />
</button>
{children}
</div>
</Popup>
);
}
Loading

0 comments on commit 3e67f0a

Please sign in to comment.