From 476ec9d54ed429b5482ff372edf0a129120d3b6f Mon Sep 17 00:00:00 2001 From: atrincas Date: Tue, 19 Nov 2024 12:13:34 +0100 Subject: [PATCH] Initial setup layout CustomProject page with Topbar --- client/src/app/(overview)/store.ts | 2 + client/src/app/projects/[id]/page.tsx | 5 + client/src/containers/auth/dialog/index.tsx | 55 +++++++++++ .../src/containers/auth/signin/form/index.tsx | 13 ++- .../header/parameters/index.tsx | 98 +++++++++++++++++++ .../projects/custom-project/index.tsx | 89 +++++++++++++++++ .../projects/project-summary/index.tsx | 11 +++ client/src/containers/projects/url-store.ts | 29 ++++++ client/src/containers/topbar/index.tsx | 26 +++++ 9 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 client/src/app/projects/[id]/page.tsx create mode 100644 client/src/containers/auth/dialog/index.tsx create mode 100644 client/src/containers/projects/custom-project/header/parameters/index.tsx create mode 100644 client/src/containers/projects/custom-project/index.tsx create mode 100644 client/src/containers/projects/project-summary/index.tsx create mode 100644 client/src/containers/projects/url-store.ts create mode 100644 client/src/containers/topbar/index.tsx diff --git a/client/src/app/(overview)/store.ts b/client/src/app/(overview)/store.ts index a87e2a5e..45f367e3 100644 --- a/client/src/app/(overview)/store.ts +++ b/client/src/app/(overview)/store.ts @@ -4,8 +4,10 @@ import { atom } from "jotai"; export const projectsUIState = atom<{ filtersOpen: boolean; + projectSummaryOpen: boolean; }>({ filtersOpen: false, + projectSummaryOpen: false, }); export const popupAtom = atom<{ diff --git a/client/src/app/projects/[id]/page.tsx b/client/src/app/projects/[id]/page.tsx new file mode 100644 index 00000000..c85b17fe --- /dev/null +++ b/client/src/app/projects/[id]/page.tsx @@ -0,0 +1,5 @@ +import CustomProject from "@/containers/projects/custom-project"; + +export default function CustomProjectPage() { + return ; +} diff --git a/client/src/containers/auth/dialog/index.tsx b/client/src/containers/auth/dialog/index.tsx new file mode 100644 index 00000000..de7aa93d --- /dev/null +++ b/client/src/containers/auth/dialog/index.tsx @@ -0,0 +1,55 @@ +import { FC, useState } from "react"; + +import SignInForm from "@/containers/auth/signin/form"; +import SignUpForm from "@/containers/auth/signup/form"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; + +interface AuthDialogProps { + dialogTrigger: React.ReactNode; + onSignIn: () => void; +} + +const AuthDialog: FC = ({ dialogTrigger, onSignIn }) => { + const [showSignin, setShowSignin] = useState(true); + + return ( + { + if (!open && !showSignin) setShowSignin(true); + }} + > + {dialogTrigger} + + + Sign in + + {showSignin ? : } + +

+ + {showSignin ? "Don't have an account?" : "Already have an account?"} + + +

+
+
+ ); +}; + +export default AuthDialog; diff --git a/client/src/containers/auth/signin/form/index.tsx b/client/src/containers/auth/signin/form/index.tsx index 856f6996..f68f695d 100644 --- a/client/src/containers/auth/signin/form/index.tsx +++ b/client/src/containers/auth/signin/form/index.tsx @@ -25,7 +25,10 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -const SignInForm: FC = () => { +interface SignInFormProps { + onSignIn?: () => void; +} +const SignInForm: FC = ({ onSignIn }) => { const router = useRouter(); const searchParams = useSearchParams(); const [errorMessage, setErrorMessage] = useState(""); @@ -51,7 +54,11 @@ const SignInForm: FC = () => { }); if (response?.ok) { - router.push(searchParams.get("callbackUrl") ?? "/profile"); + if (onSignIn) { + onSignIn(); + } else { + router.push(searchParams.get("callbackUrl") ?? "/profile"); + } } if (!response?.ok) { @@ -64,7 +71,7 @@ const SignInForm: FC = () => { } })(evt); }, - [form, router, searchParams], + [form, router, searchParams, onSignIn], ); return ( diff --git a/client/src/containers/projects/custom-project/header/parameters/index.tsx b/client/src/containers/projects/custom-project/header/parameters/index.tsx new file mode 100644 index 00000000..19b84c1e --- /dev/null +++ b/client/src/containers/projects/custom-project/header/parameters/index.tsx @@ -0,0 +1,98 @@ +import { + COST_TYPE_SELECTOR, + PROJECT_PRICE_TYPE, +} from "@shared/entities/projects.entity"; +import { z } from "zod"; + +import { FILTER_KEYS } from "@/app/(overview)/constants"; +import { filtersSchema } from "@/app/(overview)/url-store"; + +import { INITIAL_COST_RANGE } from "@/containers/overview/filters/constants"; +import { useGlobalFilters } from "@/containers/projects/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[3], + label: "Project size", + className: "w-[125px]", + options: [ + { + label: COST_TYPE_SELECTOR.NPV, + value: COST_TYPE_SELECTOR.NPV, + }, + { + label: COST_TYPE_SELECTOR.TOTAL, + value: COST_TYPE_SELECTOR.TOTAL, + }, + ], + }, + { + 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, + }, + ], + }, +] as const; + +export default function CustomProjectParameters() { + const [filters, setFilters] = useGlobalFilters(); + + const handleParameters = async ( + v: string, + parameter: keyof Omit, "keyword">, + ) => { + await setFilters((prev) => ({ + ...prev, + [parameter]: v, + ...(parameter === "costRangeSelector" && { + costRange: INITIAL_COST_RANGE[v as COST_TYPE_SELECTOR], + }), + })); + }; + + return ( +
+ {PROJECT_PARAMETERS.map((parameter) => ( +
+ + +
+ ))} +
+ ); +} diff --git a/client/src/containers/projects/custom-project/index.tsx b/client/src/containers/projects/custom-project/index.tsx new file mode 100644 index 00000000..e4530139 --- /dev/null +++ b/client/src/containers/projects/custom-project/index.tsx @@ -0,0 +1,89 @@ +"use client"; +import { FC } from "react"; + +import { motion } from "framer-motion"; +import { useAtom } from "jotai"; +import { LayoutListIcon } from "lucide-react"; +import { useSession } from "next-auth/react"; + +import { LAYOUT_TRANSITIONS } from "@/app/(overview)/constants"; +import { projectsUIState } from "@/app/(overview)/store"; + +import AuthDialog from "@/containers/auth/dialog"; +import CustomProjectParameters from "@/containers/projects/custom-project/header/parameters"; +import ProjectSummary from "@/containers/projects/project-summary"; +import Topbar from "@/containers/topbar"; + +import { Button } from "@/components/ui/button"; +import { useSidebar } from "@/components/ui/sidebar"; +import { useToast } from "@/components/ui/toast/use-toast"; + +const CustomProject: FC = () => { + const [{ projectSummaryOpen }, setFiltersOpen] = useAtom(projectsUIState); + const { open: navOpen } = useSidebar(); + const { data: session } = useSession(); + const { toast } = useToast(); + const handleSaveButtonClick = () => { + // TODO: Add API call when available + toast({ description: "Project updated successfully." }); + }; + + return ( + + + + +
+ +
+ + + {session ? ( + + ) : ( + Save project} + onSignIn={handleSaveButtonClick} + /> + )} +
+
+
+
+ ); +}; + +export default CustomProject; diff --git a/client/src/containers/projects/project-summary/index.tsx b/client/src/containers/projects/project-summary/index.tsx new file mode 100644 index 00000000..0c28e675 --- /dev/null +++ b/client/src/containers/projects/project-summary/index.tsx @@ -0,0 +1,11 @@ +import { FC } from "react"; + +const ProjectSummary: FC = () => { + return ( +
+

Summary

+
+ ); +}; + +export default ProjectSummary; diff --git a/client/src/containers/projects/url-store.ts b/client/src/containers/projects/url-store.ts new file mode 100644 index 00000000..3fa44121 --- /dev/null +++ b/client/src/containers/projects/url-store.ts @@ -0,0 +1,29 @@ +import { + COST_TYPE_SELECTOR, + PROJECT_PRICE_TYPE, +} from "@shared/entities/projects.entity"; +import { parseAsJson, useQueryState } from "nuqs"; +import { z } from "zod"; + +import { FILTER_KEYS } from "@/app/(overview)/constants"; + +import { INITIAL_COST_RANGE } from "@/containers/overview/filters/constants"; + +export const filtersSchema = z.object({ + [FILTER_KEYS[2]]: z.nativeEnum(PROJECT_PRICE_TYPE), + [FILTER_KEYS[3]]: z.nativeEnum(COST_TYPE_SELECTOR), + [FILTER_KEYS[8]]: z.array(z.number()).length(2), +}); + +export const INITIAL_FILTERS_STATE: z.infer = { + priceType: PROJECT_PRICE_TYPE.OPEN_BREAK_EVEN_PRICE, + costRangeSelector: COST_TYPE_SELECTOR.NPV, + costRange: INITIAL_COST_RANGE[COST_TYPE_SELECTOR.NPV], +}; + +export function useGlobalFilters() { + return useQueryState( + "filters", + parseAsJson(filtersSchema.parse).withDefault(INITIAL_FILTERS_STATE), + ); +} diff --git a/client/src/containers/topbar/index.tsx b/client/src/containers/topbar/index.tsx new file mode 100644 index 00000000..b19f5f22 --- /dev/null +++ b/client/src/containers/topbar/index.tsx @@ -0,0 +1,26 @@ +import { FC, PropsWithChildren } from "react"; + +import { cn } from "@/lib/utils"; + +import { SidebarTrigger } from "@/components/ui/sidebar"; + +interface TopbarProps extends PropsWithChildren { + title: string; + className?: HTMLDivElement["className"]; +} + +const Topbar: FC = ({ title, className, children }) => { + return ( +
+
+ +

{title}

+
+ {children} +
+ ); +}; + +export default Topbar;