diff --git a/client/package.json b/client/package.json index 5ae872b..8aefdb8 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,7 @@ "mapbox-gl": "3.7.0", "next": "14.2.10", "next-auth": "4.24.8", + "nuqs": "2.0.4", "react": "^18", "react-dom": "^18", "react-map-gl": "7.1.7", diff --git a/client/src/app/(projects)/constants.ts b/client/src/app/(projects)/constants.ts index 68110aa..d1f8d72 100644 --- a/client/src/app/(projects)/constants.ts +++ b/client/src/app/(projects)/constants.ts @@ -2,3 +2,17 @@ export const LAYOUT_TRANSITIONS = { duration: 0.4, ease: "easeInOut", }; + +export const PROJECT_SIZE_VALUES = ["small", "medium", "large"] as const; +export const CARBON_PRICING_TYPE_VALUES = [ + "market_price", + "opex_breakeven_price", +] as const; +export const COST_VALUES = ["total", "npv"] as const; + +export const FILTER_KEYS = [ + "keyword", + "projectSize", + "carbonPricingType", + "cost", +] as const; diff --git a/client/src/app/(projects)/store.ts b/client/src/app/(projects)/store.ts index e03c9d0..79ed845 100644 --- a/client/src/app/(projects)/store.ts +++ b/client/src/app/(projects)/store.ts @@ -1,7 +1,5 @@ import { atom } from "jotai"; -import { PROJECT_PARAMETERS } from "@/containers/projects/header/parameters"; - export const projectsUIState = atom<{ filtersOpen: boolean; mapExpanded: "default" | "expanded" | "collapsed"; @@ -17,15 +15,3 @@ export const projectsMapState = atom<{ }>({ legendOpen: true, }); - -export const projectsFiltersState = atom<{ - keyword: string | undefined; - projectSize: (typeof PROJECT_PARAMETERS)[0]["options"][number]["value"]; - carbonPricingType: (typeof PROJECT_PARAMETERS)[1]["options"][number]["value"]; - cost: (typeof PROJECT_PARAMETERS)[2]["options"][number]["value"]; -}>({ - keyword: undefined, - projectSize: "medium", - carbonPricingType: "market_price", - cost: "npv", -}); diff --git a/client/src/app/(projects)/url-store.ts b/client/src/app/(projects)/url-store.ts new file mode 100644 index 0000000..0e40831 --- /dev/null +++ b/client/src/app/(projects)/url-store.ts @@ -0,0 +1,37 @@ +import { parseAsJson, parseAsStringLiteral, useQueryState } from "nuqs"; +import { z } from "zod"; + +import { + PROJECT_SIZE_VALUES, + CARBON_PRICING_TYPE_VALUES, + COST_VALUES, + FILTER_KEYS, +} from "@/app/(projects)/constants"; + +import { TABLE_MODES } from "@/containers/projects/table-visualization/toolbar/table-selector"; + +export const filtersSchema = z.object({ + [FILTER_KEYS[0]]: z.string().optional(), + [FILTER_KEYS[1]]: z.enum(PROJECT_SIZE_VALUES), + [FILTER_KEYS[2]]: z.enum(CARBON_PRICING_TYPE_VALUES), + [FILTER_KEYS[3]]: z.enum(COST_VALUES), +}); + +export function useGlobalFilters() { + return useQueryState( + "filters", + parseAsJson(filtersSchema.parse).withDefault({ + keyword: "", + projectSize: "medium", + carbonPricingType: "market_price", + cost: "npv", + }), + ); +} + +export function useTableMode() { + return useQueryState( + "table", + parseAsStringLiteral(TABLE_MODES).withDefault("overview"), + ); +} diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index 26d871d..435b50c 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -1,8 +1,11 @@ +import { PropsWithChildren } from "react"; + import { Inter } from "next/font/google"; import type { Metadata } from "next"; import "@/app/globals.css"; import { getServerSession } from "next-auth"; +import { NuqsAdapter } from "nuqs/adapters/next/app"; import { config } from "@/app/auth/api/[...nextauth]/config"; @@ -19,18 +22,18 @@ export const metadata: Metadata = { export default async function RootLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: Readonly) { const session = await getServerSession(config); return ( - -
{children}
- - + + +
{children}
+ + +
); diff --git a/client/src/containers/projects/header/parameters/index.tsx b/client/src/containers/projects/header/parameters/index.tsx index 59b0840..2ed5dc9 100644 --- a/client/src/containers/projects/header/parameters/index.tsx +++ b/client/src/containers/projects/header/parameters/index.tsx @@ -1,6 +1,13 @@ -import { useAtom, ExtractAtomValue } from "jotai"; +import { z } from "zod"; -import { projectsFiltersState } from "@/app/(projects)/store"; +import { + CARBON_PRICING_TYPE_VALUES, + COST_VALUES, + FILTER_KEYS, + PROJECT_SIZE_VALUES, +} from "@/app/(projects)/constants"; +import { useGlobalFilters } from "@/app/(projects)/url-store"; +import { filtersSchema } from "@/app/(projects)/url-store"; import { Label } from "@/components/ui/label"; import { @@ -10,70 +17,63 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; - export const PROJECT_PARAMETERS = [ { + key: FILTER_KEYS[1], label: "Project size", - key: "projectSize", - defaultValue: "medium", options: [ { label: "Small", - value: "small", + value: PROJECT_SIZE_VALUES[0], }, { label: "Medium", - value: "medium", + value: PROJECT_SIZE_VALUES[1], }, { label: "Large", - value: "large", + value: PROJECT_SIZE_VALUES[2], }, ], }, { + key: FILTER_KEYS[2], label: "Carbon pricing type", - key: "carbonPricingType", - defaultValue: "market_price", options: [ { label: "Market price", - value: "market_price", + value: CARBON_PRICING_TYPE_VALUES[0], }, { label: "OPEX Breakeven price", - value: "opex_breakeven_price", + value: CARBON_PRICING_TYPE_VALUES[1], }, ], }, { + key: FILTER_KEYS[3], label: "Cost", - key: "cost", - defaultValue: "npv", options: [ { label: "Total", - value: "total", + value: COST_VALUES[0], }, { label: "NPV", - value: "npv", + value: COST_VALUES[1], }, ], }, ] as const; export default function ParametersProjects() { - const [, setFilters] = useAtom(projectsFiltersState); + const [filters, setFilters] = useGlobalFilters(); - const handleParameters = ( + const handleParameters = async ( v: string, - parameter: keyof Omit< - ExtractAtomValue, - "keyword" - >, + parameter: keyof Omit, "keyword">, ) => { - setFilters((prev) => ({ ...prev, [parameter]: v })); + await setFilters((prev) => ({ ...prev, [parameter]: v })); }; return ( @@ -83,7 +83,7 @@ export default function ParametersProjects() {