Skip to content

Commit

Permalink
Initial setup layout CustomProject page with Topbar
Browse files Browse the repository at this point in the history
  • Loading branch information
atrincas committed Nov 19, 2024
1 parent 764cbab commit 476ec9d
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 3 deletions.
2 changes: 2 additions & 0 deletions client/src/app/(overview)/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { atom } from "jotai";

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

export const popupAtom = atom<{
Expand Down
5 changes: 5 additions & 0 deletions client/src/app/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import CustomProject from "@/containers/projects/custom-project";

export default function CustomProjectPage() {
return <CustomProject />;
}
55 changes: 55 additions & 0 deletions client/src/containers/auth/dialog/index.tsx
Original file line number Diff line number Diff line change
@@ -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<AuthDialogProps> = ({ dialogTrigger, onSignIn }) => {
const [showSignin, setShowSignin] = useState<boolean>(true);

return (
<Dialog
onOpenChange={(open) => {
if (!open && !showSignin) setShowSignin(true);
}}
>
<DialogTrigger asChild>{dialogTrigger}</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Sign in</DialogTitle>
</DialogHeader>
{showSignin ? <SignInForm onSignIn={onSignIn} /> : <SignUpForm />}
<Separator />
<p className="text-center text-sm">
<span className="pr-2 text-muted-foreground">
{showSignin ? "Don't have an account?" : "Already have an account?"}
</span>
<Button
type="button"
variant="link"
className="p-0 text-primary"
onClick={() => setShowSignin(!showSignin)}
>
{showSignin ? "Sign up" : "Sign in"}
</Button>
</p>
</DialogContent>
</Dialog>
);
};

export default AuthDialog;
13 changes: 10 additions & 3 deletions client/src/containers/auth/signin/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignInFormProps> = ({ onSignIn }) => {
const router = useRouter();
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState<string | undefined>("");
Expand All @@ -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) {
Expand All @@ -64,7 +71,7 @@ const SignInForm: FC = () => {
}
})(evt);
},
[form, router, searchParams],
[form, router, searchParams, onSignIn],
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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<z.infer<typeof filtersSchema>, "keyword">,
) => {
await setFilters((prev) => ({
...prev,
[parameter]: v,
...(parameter === "costRangeSelector" && {
costRange: INITIAL_COST_RANGE[v as COST_TYPE_SELECTOR],
}),
}));
};

return (
<div className="flex flex-1 items-center justify-end space-x-4">
{PROJECT_PARAMETERS.map((parameter) => (
<div key={parameter.label} className="flex items-center space-x-2">
<Label htmlFor={parameter.label}>{parameter.label}</Label>
<Select
name={parameter.label}
defaultValue={filters[parameter.key]}
onValueChange={(v) => {
handleParameters(v, parameter.key);
}}
>
<SelectTrigger className={parameter.className}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{parameter.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
);
}
89 changes: 89 additions & 0 deletions client/src/containers/projects/custom-project/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<motion.div
layout
layoutDependency={navOpen}
className="flex flex-1"
transition={LAYOUT_TRANSITIONS}
>
<motion.aside
layout
initial={projectSummaryOpen ? "open" : "closed"}
animate={projectSummaryOpen ? "open" : "closed"}
variants={{
open: {
width: 460,
},
closed: {
width: 0,
},
}}
transition={LAYOUT_TRANSITIONS}
className="overflow-hidden"
>
<ProjectSummary />
</motion.aside>
<div className="mx-3 flex flex-1 flex-col">
<Topbar title="Custom project - v01" className="gap-4">
<div className="flex flex-1 justify-between gap-4">
<Button
type="button"
variant="outline"
onClick={() => {
setFiltersOpen((prev) => ({
...prev,
projectSummaryOpen: !prev.projectSummaryOpen,
}));
}}
>
<LayoutListIcon className="h-4 w-4" />
<span>Project summary</span>
</Button>
<CustomProjectParameters />
{session ? (
<Button type="button" onClick={handleSaveButtonClick}>
Save project
</Button>
) : (
<AuthDialog
dialogTrigger={<Button type="button">Save project</Button>}
onSignIn={handleSaveButtonClick}
/>
)}
</div>
</Topbar>
</div>
</motion.div>
);
};

export default CustomProject;
11 changes: 11 additions & 0 deletions client/src/containers/projects/project-summary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FC } from "react";

const ProjectSummary: FC = () => {
return (
<div className="bg-background p-6">
<h2>Summary</h2>
</div>
);
};

export default ProjectSummary;
29 changes: 29 additions & 0 deletions client/src/containers/projects/url-store.ts
Original file line number Diff line number Diff line change
@@ -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<typeof filtersSchema> = {
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),
);
}
26 changes: 26 additions & 0 deletions client/src/containers/topbar/index.tsx
Original file line number Diff line number Diff line change
@@ -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<TopbarProps> = ({ title, className, children }) => {
return (
<header
className={cn("flex w-full items-center justify-between py-3", className)}
>
<div className="flex items-center space-x-2">
<SidebarTrigger />
<h2 className="text-2xl font-medium">{title}</h2>
</div>
{children}
</header>
);
};

export default Topbar;

0 comments on commit 476ec9d

Please sign in to comment.