Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"prepare": "husky"
},
"dependencies": {
"@base-ui/react": "^1.2.0",
"@floating-ui/dom": "^1.7.4",
"@hookform/resolvers": "^5.2.2",
"@mantine/core": "^8.3.4",
Expand Down
73 changes: 71 additions & 2 deletions src/app/graph/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ChevronDown,
LineChart,
Link,
PlusCircle,
Share,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
Expand All @@ -39,7 +40,13 @@ import {
parseAsString,
parseAsArrayOf,
} from "nuqs";
import { downloadGraph } from "@/lib/export-to-pdf";
import { addToCart, downloadSingleGraph } from "@/lib/export-to-pdf";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Cart } from "@/components/Cart";

type Project = {
id: number;
Expand Down Expand Up @@ -158,6 +165,10 @@ export default function GraphsPage() {
parseAsString.withDefault(""),
);

const [cart, setCart] = useState<string[]>([]);

const [filterNames, setFilterNames] = useState<string[]>([]);

const filters: Filters = useMemo(
() => ({
individualProjects: true,
Expand Down Expand Up @@ -486,6 +497,29 @@ export default function GraphsPage() {
new Set(allProjects.map((p) => p.category)),
).sort();

const filterName = `Projects by ${groupByLabels[filters.groupBy]}`;

useEffect(() => {
const cartStorage = sessionStorage.getItem("cartStorage");
const cartNameStorage = sessionStorage.getItem("cartNameStorage");

if (cartStorage) {
setCart(JSON.parse(cartStorage));
}

if (cartNameStorage) {
setFilterNames(JSON.parse(cartNameStorage));
}
}, []);

useEffect(() => {
sessionStorage.setItem("cartStorage", JSON.stringify(cart));
}, [cart]);

useEffect(() => {
sessionStorage.setItem("cartNameStorage", JSON.stringify(filterNames));
}, [filterNames]);

return (
<div className="w-full min-h-screen flex bg-background">
{/* Left Sidebar - Filter Panel */}
Expand Down Expand Up @@ -530,11 +564,46 @@ export default function GraphsPage() {
variant="outline"
size="sm"
className="flex items-center gap-2"
onClick={() => downloadGraph(svgRef)}
onClick={() => downloadSingleGraph(svgRef)}
>
<Share className="w-4 h-4" />
Export
</Button>
<HoverCard>
<HoverCardTrigger
delay={10}
closeDelay={100}
render={
<Button
variant="outline"
size="sm"
className="flex items-center gap-2"
onClick={() =>
addToCart(
svgRef,
cart,
setCart,
filterNames,
setFilterNames,
filterName,
)
}
>
<PlusCircle className="w-4 h-4" />
Add to
</Button>
}
/>
<HoverCardContent className="flex w-64 flex-col gap-0.5">
<Cart
filterNames={filterNames}
cart={cart}
setCart={setCart}
setFilterNames={setFilterNames}
/>
</HoverCardContent>
</HoverCard>

<Button
variant="outline"
size="sm"
Expand Down
64 changes: 64 additions & 0 deletions src/components/Cart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/***************************************************************
*
* /src/components/cart.tsx
*
* Author: Will and Justin
* Date: 2/1/2025
*
* Summary: Displays cart of images to export when
* hovering over cart button
**************************************************************/

import { Trash2 } from "lucide-react";
import { Button } from "./ui/button";
import { clearCart, deleteFromCart, downloadGraphs } from "@/lib/export-to-pdf";
import { Dispatch, SetStateAction } from "react";

type CartProps = {
filterNames: string[];
cart: string[];
setCart: Dispatch<SetStateAction<string[]>>;
setFilterNames: Dispatch<SetStateAction<string[]>>;
};

export function Cart({
filterNames,
cart,
setCart,
setFilterNames,
}: CartProps) {
return (
<div className="flex flex-col gap-2 w-full max-w-5xl px-3 py-1 mr-5">
{filterNames.map((filterName, index) => (
<div
key={index}
className="flex flex-row gap-4 justify-between"
>
<p>{filterName}</p>
<button
onClick={() =>
deleteFromCart(
cart,
setCart,
filterNames,
setFilterNames,
filterName,
)
}
className="text-gray-400 hover:text-red-500 p-1 transition-colors duration-150 ease-in-out"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
))}
<div className="flex flex-row gap-7 border-t pt-2">
<button onClick={() => clearCart(setCart, setFilterNames)}>
Clear All
</button>
<Button onClick={() => downloadGraphs(cart, filterNames)}>
Export To PDF
</Button>
</div>
</div>
);
}
65 changes: 65 additions & 0 deletions src/components/ui/hover-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/***************************************************************
*
* /src/components/hover-card.tsx
*
* Author: Will and Justin
* Date: 2/13/2025
*
* Summary: Empty hover card component when hovering
* over Add to button. Filled with Cart.tsx
**************************************************************/

"use client";

import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";

import { cn } from "@/lib/utils";

function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />;
}

function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
return (
<PreviewCardPrimitive.Trigger
data-slot="hover-card-trigger"
{...props}
/>
);
}

function HoverCardContent({
className,
side = "bottom",
sideOffset = 4,
align = "start",
alignOffset = 4,
...props
}: PreviewCardPrimitive.Popup.Props &
Pick<
PreviewCardPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
<PreviewCardPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<PreviewCardPrimitive.Popup
data-slot="hover-card-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-64 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 origin-(--transform-origin) outline-hidden",
className,
)}
{...props}
/>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
);
}

export { HoverCard, HoverCardTrigger, HoverCardContent };
Loading