From 195744a9154d51078d62766512a99daef4ea5545 Mon Sep 17 00:00:00 2001 From: woleary2 Date: Wed, 11 Feb 2026 13:45:07 -0500 Subject: [PATCH 1/2] Initial commit, added feature of adding graphs to a cart --- src/app/graph/page.tsx | 22 +++++++++- src/components/Cart.tsx | 7 ++++ src/lib/export-to-pdf.ts | 90 +++++++++++++++++++++++++++++++++------- 3 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 src/components/Cart.tsx diff --git a/src/app/graph/page.tsx b/src/app/graph/page.tsx index 4c0491b..040b5ce 100644 --- a/src/app/graph/page.tsx +++ b/src/app/graph/page.tsx @@ -16,6 +16,7 @@ import { ChevronDown, LineChart, Link, + Plus, Share, } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -39,7 +40,11 @@ import { parseAsString, parseAsArrayOf, } from "nuqs"; -import { downloadGraph } from "@/lib/export-to-pdf"; +import { + addToCart, + downloadGraph, + downloadSingleGraph, +} from "@/lib/export-to-pdf"; type Project = { id: number; @@ -158,6 +163,8 @@ export default function GraphsPage() { parseAsString.withDefault(""), ); + const [cart, setCart] = useState([]); + const filters: Filters = useMemo( () => ({ individualProjects: true, @@ -530,11 +537,22 @@ export default function GraphsPage() { variant="outline" size="sm" className="flex items-center gap-2" - onClick={() => downloadGraph(svgRef)} + onClick={() => downloadSingleGraph(svgRef)} > Export + - + + + addToCart( + svgRef, + cart, + setCart, + filterNames, + setFilterNames, + filterName, + ) + } + > + + Add to + + } + /> + + + + + + + ))} +
+ + +
+ ); } diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx new file mode 100644 index 0000000..35114c1 --- /dev/null +++ b/src/components/ui/hover-card.tsx @@ -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 ; +} + +function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) { + return ( + + ); +} + +function HoverCardContent({ + className, + side = "bottom", + sideOffset = 4, + align = "start", + alignOffset = 4, + ...props +}: PreviewCardPrimitive.Popup.Props & + Pick< + PreviewCardPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ); +} + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/src/lib/export-to-pdf.ts b/src/lib/export-to-pdf.ts index 39f7a93..6f60ecb 100644 --- a/src/lib/export-to-pdf.ts +++ b/src/lib/export-to-pdf.ts @@ -11,20 +11,60 @@ import React, { Dispatch, SetStateAction } from "react"; import html2canvas from "html2canvas-pro"; import jsPDF from "jspdf"; +import logoImg from "../../public/images/logo.png"; +import { toast } from "sonner"; + +export function downloadGraphs(cart: string[], filterNames: string[]) { + // Displays toast when there are no images to export + if (cart.length == 0) { + toast.error("Cart is empty"); + } -export function downloadGraph(cart: HTMLCanvasElement[]) { const pdf = new jsPDF(); - const pdfWidth = pdf.internal.pageSize.getWidth(); - //const pdfHeight = pdfWidth * aspectRatio; - const pdfHeight = pdf.internal.pageSize.getHeight(); - // Add image with proper dimensions that match PDF width while preserving aspect ratio - cart.forEach((canvas: HTMLCanvasElement, i: number) => { - pdf.addImage(canvas, "JPEG", 0, i * pdfHeight, pdfWidth, pdfHeight); - i++; + // Does process for each graph in the cart + cart.forEach((canvas: string, idx: number) => { + const img = new Image(); + img.src = canvas; + + // Date data for image + const time = new Date(); + const year = String(time.getFullYear()); + const month = String(time.getMonth()); + const day = String(time.getDate()); + + img.onload = () => { + const imgWidth = pdf.internal.pageSize.getWidth(); + const imgHeight = (img.height / img.width) * imgWidth; + + pdf.setFont("Interstate", "bold"); + + pdf.text(`${month}/${day}/${year}`, 180, 15); + pdf.addImage( + logoImg.src, + "PNG", + 20, + 10, + logoImg.width * 0.03, + logoImg.height * 0.03, + ); + + pdf.text(filterNames[idx], 25, 50); + + pdf.addImage( + canvas, + "JPEG", + 10, + 55, + imgWidth * 0.9, + imgHeight * 0.9, + ); + + if (idx < cart.length - 1) pdf.addPage(); + + if (idx === cart.length - 1) pdf.save("graph.pdf"); + }; }); - - pdf.save("graph.pdf"); } export function getClonedSvg( @@ -41,8 +81,11 @@ export function getClonedSvg( export async function addToCart( svgRef: React.RefObject, - cart: HTMLCanvasElement[], - setCart: Dispatch>, + cart: string[], + setCart: Dispatch>, + filterNames: string[], + setFilterNames: Dispatch>, + filterName: string, ): Promise { const newSVG = getClonedSvg(svgRef); if (!newSVG) return; @@ -51,7 +94,6 @@ export async function addToCart( const viewBox = newSVG.getAttribute("viewBox"); let svgWidth = 1000; let svgHeight = 400; - let aspectRatio = svgHeight / svgWidth; if (viewBox) { const viewBoxValues = viewBox.split(" "); @@ -64,8 +106,6 @@ export async function addToCart( if (height) svgHeight = parseFloat(height) || 400; } - aspectRatio = svgHeight / svgWidth; - // Adds the svg element to the page temporarily (offscreen) const wrapper = document.createElement("div"); wrapper.style.position = "fixed"; @@ -81,7 +121,9 @@ export async function addToCart( scale: 2, }); - setCart([...cart, canvas]); + // Updates cart and filter names + setCart([...cart, canvas.toDataURL()]); + setFilterNames([...filterNames, filterName]); document.body.removeChild(wrapper); } @@ -136,3 +178,26 @@ export async function downloadSingleGraph( document.body.removeChild(wrapper); } + +export function clearCart( + setCart: Dispatch>, + setFilterNames: Dispatch>, +) { + // Resets cart and filter names + setCart([]); + setFilterNames([]); +} + +export function deleteFromCart( + cart: string[], + setCart: Dispatch>, + filterNames: string[], + setFilterNames: Dispatch>, + filterName: string, +) { + // Locates index where a filter occurs and removes it from cart + // NOTE: slightly buggy when there are multiple graphs with same name + var idx = filterNames.indexOf(filterName); + setCart(cart.filter((_, index) => index !== idx)); + setFilterNames(filterNames.filter((_, index) => index !== idx)); +}