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
275 changes: 275 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
"framer-motion": "^12.23.24",
"html2canvas": "^1.4.1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why install a dep if you're not going to use it

"html2canvas-pro": "^1.6.6",
"input-otp": "^1.4.2",
"jspdf": "^4.0.0",
"lucide": "^0.544.0",
"lucide-react": "^0.545.0",
"next": "^16.0.7",
Expand Down
8 changes: 7 additions & 1 deletion src/app/graphs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
Link,
Share,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import BarGraph, { type BarDataset } from "@/components/BarGraph";
import { Breadcrumbs } from "@/components/Breadcrumbs";
Expand All @@ -35,6 +35,7 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { downloadGraph } from "@/lib/export-to-pdf";

// define Project type
type Project = {
Expand Down Expand Up @@ -89,6 +90,7 @@ const groupByLabels: Record<string, string> = {
export default function GraphsPage() {
const [allProjects, setAllProjects] = useState<Project[]>([]);
const [filters, setFilters] = useState<Filters>(defaultFilters);
//const [isExporting, setIsExporting] = useState(false); potentially for loading stat
const [gatewayCities, setGatewayCities] = useState<string[]>([]);
const [chartType, setChartType] = useState<"line" | "bar">("line");
const [timePeriod, setTimePeriod] = useState<
Expand All @@ -103,6 +105,7 @@ export default function GraphsPage() {
start: 2020,
end: 2025,
});
const svgRef = useRef<SVGSVGElement | null>(null);

// Fetch all project data
useEffect(() => {
Expand Down Expand Up @@ -398,6 +401,7 @@ export default function GraphsPage() {
variant="outline"
size="sm"
className="flex items-center gap-2"
onClick={() => downloadGraph(svgRef)}
>
<Share className="w-4 h-4" />
Export
Expand Down Expand Up @@ -589,6 +593,7 @@ export default function GraphsPage() {
measuredAsLabels[filters.measuredAs]
}
xAxisLabel={groupByLabels[filters.groupBy]}
svgRefCopy={svgRef}
/>
) : (
<LineGraph
Expand All @@ -597,6 +602,7 @@ export default function GraphsPage() {
measuredAsLabels[filters.measuredAs]
}
xAxisLabel={groupByLabels[filters.groupBy]}
svgRefCopy={svgRef}
/>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/app/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AuthForm from "@/components/AuthForm";
import Dashboard from "@/components/Dashboard";
import WarpShader from "@/components/WarpShader";

export default function SignInPage() {
Expand Down
9 changes: 8 additions & 1 deletion src/components/BarGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ type BarGraphProps = {
dataset: BarDataset[];
yAxisLabel: string;
xAxisLabel: string;
svgRefCopy: React.RefObject<SVGSVGElement | null>;
};

export default function BarGraph({
dataset,
yAxisLabel,
xAxisLabel,
svgRefCopy,
}: BarGraphProps) {
const svgRef = useRef<SVGSVGElement | null>(null);

// Use same color scheme as LineGraph
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
//const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
const colorScale = d3.scaleOrdinal(
d3.schemeCategory10.map((c) => c.toString()),
);

useEffect(() => {
if (!svgRef.current || dataset.length === 0) return;
Expand Down Expand Up @@ -181,6 +186,8 @@ export default function BarGraph({
xOffset += itemWidth;
return transform;
});

svgRefCopy.current = svgRef.current;
}, [dataset]);

return (
Expand Down
11 changes: 6 additions & 5 deletions src/components/ConditionalLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ export default function ConditionalLayout({
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const isAuthPage = pathname === "/signin";
//const pathname = usePathname();

//const isAuthPage = pathname === "/signin";

// If on auth pages, just render children without sidebar
if (isAuthPage) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why

return <main className="w-full h-full">{children}</main>;
}
// if (isAuthPage) {
// return <main className="w-full h-full">{children}</main>;
// }

// Otherwise, render with sidebar and responsive layout
return (
Expand Down
10 changes: 9 additions & 1 deletion src/components/LineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ type MultiLineGraphProps = {
datasets: GraphDataset[];
yAxisLabel: string;
xAxisLabel: string;
svgRefCopy: React.RefObject<SVGSVGElement | null>;
};

export default function MultiLineGraph({
datasets,
yAxisLabel,
xAxisLabel,
svgRefCopy,
}: MultiLineGraphProps) {
const svgRef = useRef<SVGSVGElement | null>(null);
const wrapperRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -39,7 +41,11 @@ export default function MultiLineGraph({
}>({});

// Memoize color scale to prevent re-running useEffect unnecessarily
const colorScale = useMemo(() => d3.scaleOrdinal(d3.schemeCategory10), []);
//const colorScale = useMemo(() => d3.scaleOrdinal(d3.schemeCategory10), []);
const colorScale = useMemo(
() => d3.scaleOrdinal(d3.schemeCategory10.map((c) => c.toString())),
[],
);

useEffect(() => {
const svg = d3.select(svgRef.current);
Expand Down Expand Up @@ -390,6 +396,8 @@ export default function MultiLineGraph({
return transform;
});
});

svgRefCopy.current = svgRef.current;
}, [datasets, xAxisLabel, yAxisLabel, colorScale]);

return (
Expand Down
8 changes: 4 additions & 4 deletions src/components/Sidebar.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why

Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import {
ChevronRight,
User,
} from "lucide-react";
import { authClient } from "@/lib/auth-client";
//import { authClient } from "@/lib/auth-client";

export default function Sidebar() {
const pathname = usePathname();
const [isOverviewOpen, setIsOverviewOpen] = useState(false);
const { data: session } = authClient.useSession();
//const { data: session } = authClient.useSession();

// Automatically open Overview if any subitem is active
useEffect(() => {
Expand Down Expand Up @@ -203,7 +203,7 @@ export default function Sidebar() {
</div>
</div>

<div className="px-4 py-5 self-center flex items-center gap-3">
{/*<div className="px-4 py-5 self-center flex items-center gap-3">
<div className="w-8 h-8 rounded-full overflow-hidden bg-muted flex items-center justify-center">
{session?.user?.image ? (
<img
Expand All @@ -218,7 +218,7 @@ export default function Sidebar() {
<span className="text-sm font-medium text-foreground overflow-hidden whitespace-nowrap">
{session?.user?.email || "Loading..."}
</span>
</div>
</div>*/}
</aside>
);
}
76 changes: 76 additions & 0 deletions src/lib/export-to-pdf.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: call this file export-to-pdf.ts. it's cleaner

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/***************************************************************
*
* /src/lib/export-to-pdf.ts
*
* Author: Will and Justin
* Date: 2/1/2025
*
* Summary: Export an svg graph as a pdf
**************************************************************/

import React from "react";
import html2canvas from "html2canvas-pro";
import jsPDF from "jspdf";

export async function downloadGraph(
svgRef: React.RefObject<SVGSVGElement | null>,
) {
const newSVG = getClonedSvg(svgRef);
if (!newSVG) return;

// Get SVG dimensions from viewBox or attributes with fallbacks
const viewBox = newSVG.getAttribute("viewBox");
let svgWidth = 1000;
let svgHeight = 400;
let aspectRatio = svgHeight / svgWidth;

if (viewBox) {
const viewBoxValues = viewBox.split(" ");
svgWidth = parseFloat(viewBoxValues[2]) || 1000;
svgHeight = parseFloat(viewBoxValues[3]) || 400;
} else {
const width = newSVG.getAttribute("width");
const height = newSVG.getAttribute("height");
if (width) svgWidth = parseFloat(width) || 1000;
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";
wrapper.style.left = "-9999px";
wrapper.style.top = "-9999px";
wrapper.style.width = `${svgWidth}px`;
wrapper.style.height = `${svgHeight}px`;
wrapper.appendChild(newSVG);
document.body.append(wrapper);

const canvas = await html2canvas(wrapper, {
backgroundColor: "#fff",
scale: 2,
});

const pdf = new jsPDF();
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdfWidth * aspectRatio;

// Add image with proper dimensions that match PDF width while preserving aspect ratio
pdf.addImage(canvas, "JPEG", 0, 0, pdfWidth, pdfHeight);
pdf.save("graph.pdf");

document.body.removeChild(wrapper);
}

export function getClonedSvg(
svgRef: React.RefObject<SVGSVGElement | null>,
): SVGSVGElement | null {
const original = svgRef.current;
if (!original) return null;

//Creates and returns a clone of the svg element passed in
const clone = original.cloneNode(true) as SVGSVGElement;

return clone;
}