Skip to content

Commit

Permalink
save as pdf option added
Browse files Browse the repository at this point in the history
  • Loading branch information
chitraa-cj committed Jun 25, 2024
1 parent 460b292 commit 7fd52cc
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 67 deletions.
21 changes: 19 additions & 2 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,22 @@ export default withPWAConfig({
hostname: '*',
}
]
}
});
},
webpack: (config, { isServer }) => {
// If it's a server build, exclude the `.node` file from bundling
if (isServer) {
config.externals = config.externals || [];
config.externals.push({
'@resvg/resvg-js-darwin-arm64/resvgjs.darwin-arm64.node': 'commonjs2 @resvg/resvg-js-darwin-arm64/resvgjs.darwin-arm64.node',
});
}

// Use node-loader for .node files
config.module.rules.push({
test: /\.node$/,
use: 'node-loader',
});

return config;
},
});
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"embla-carousel-react": "^8.0.4",
"express": "^4.19.2",
"framer-motion": "^11.1.9",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"langchain": "^0.2.4",
"lucide-react": "^0.378.0",
"moment": "^2.30.1",
Expand All @@ -70,6 +72,7 @@
"resend": "^3.3.0",
"sharp": "^0.33.3",
"sonner": "^1.5.0",
"svg2img": "^1.0.0-beta.2",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"three": "^0.164.1",
Expand All @@ -84,6 +87,7 @@
"editorjs-inline-image": "^2.1.1",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.3",
"node-loader": "^2.0.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5"
Expand Down
198 changes: 179 additions & 19 deletions src/app/workspace/[fileId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,192 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef, MutableRefObject } from "react";
import WorkspaceHeader from "../_components/WorkspaceHeader";
import Editor from "../_components/Editor";
import { useConvex } from "convex/react";
import { api } from "../../../../convex/_generated/api";
import { FILE } from "../../dashboard/_components/FileList";
import Canvas from "../_components/Canvas";
import dynamic from 'next/dynamic';
import EditorJS, { OutputData } from "@editorjs/editorjs";

// Dynamic imports for server-side libraries
const jsPDFPromise = import('jspdf');
const excalidrawPromise = import('@excalidraw/excalidraw');

function Workspace({ params }: any) {
const [triggerSave, setTriggerSave] = useState(false);
const convex = useConvex();
const [fileData, setFileData] = useState<FILE | any>();
const [fullScreen, setFullScreen] = useState(false);
const editorRef = useRef<EditorJS | null>(null);
const canvasRef = useRef<any>(null);

useEffect(() => {
params.fileId && getFileData();
}, []);
if (params.fileId) {
getFileData();
}
}, [params.fileId]);

const getFileData = async () => {
const result = await convex.query(api.files.getFileById, {
_id: params.fileId,
});
setFileData(result);
};


const saveAsPdf = async () => {
const { default: jsPDF } = await jsPDFPromise;
const { exportToSvg } = await excalidrawPromise;

const editorInstance = editorRef.current;
const canvasInstance = canvasRef.current;

if (editorInstance && canvasInstance) {
const pdf = new jsPDF("p", "mm", "a4");

// Extract text content from the editor
editorInstance.save().then((editorContent: OutputData) => {
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const margin = 10;
const textWidth = pageWidth - margin * 2;
const textHeight = pageHeight - margin * 2;
let y = margin;

editorContent.blocks.forEach((block: any) => {
let lines: any[] = [];

switch (block.type) {
case "paragraph":
lines = parseText(block.data.text);
break;
case "header":
pdf.setFontSize(16); // Set font size for header
lines = [{ text: block.data.text, style: 'header' }];
pdf.setFontSize(12); // Reset font size
break;
case "list":
lines = block.data.items.map((item: string) => ({ text: `• ${item}`, style: 'normal' }));
break;
// Add more cases if needed for different block types
default:
lines = [{ text: block.data.text, style: 'normal' }];
}

lines.forEach((line: any) => {
if (y + 10 > textHeight) {
pdf.addPage();
y = margin;
}

switch (line.style) {
case 'bold':
pdf.setFont("helvetica", "bold");
break;
case 'italic':
pdf.setFont("helvetica", "italic");
break;
case 'header':
pdf.setFont("helvetica", "bold");
const headerWidth = pdf.getStringUnitWidth(line.text) * 16 / pdf.internal.scaleFactor;
pdf.text(line.text, (pageWidth - headerWidth) / 2, y);
y += 10;
break;
default:
pdf.setFont("helvetica", "normal");
}

if (line.style !== 'header') {
// Split text if it's too wide and handle separately
const wrappedLines = pdf.splitTextToSize(line.text, textWidth);
wrappedLines.forEach((wrappedLine: string) => {
if (y + 10 > textHeight) {
pdf.addPage();
y = margin;
}
pdf.text(wrappedLine, margin, y);
y += 10;
});
}
});

// Reset font style and size after each block
pdf.setFont("helvetica", "normal");
pdf.setFontSize(12);
});

// Export flowchart as SVG from Excalidraw
const elements = canvasInstance.getSceneElements();
const appState = canvasInstance.getAppState();
const files = canvasInstance.getFiles();

exportToSvg({
elements: elements,
appState: { ...appState, exportBackground: false }, // No background
files: files,
}).then((svg: SVGSVGElement) => {
// Add heading for the flowchart
pdf.setFont("helvetica", "bold");
pdf.setFontSize(16); // Set font size for the heading
const headingText = "Flowchart";
const headingWidth = pdf.getStringUnitWidth(headingText) * pdf.internal.scaleFactor;
const headingX = (pageWidth - headingWidth) / 2;
pdf.text(headingText, headingX, y + 10);
pdf.setFontSize(12); // Reset font size
pdf.setFont("helvetica", "normal");
y += 20; // Adjust y position to avoid overlap with the heading

// Convert SVG to PNG using the Canvas API
const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const img = new Image();

img.onload = () => {
if (context) {
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);

const imgData = canvas.toDataURL("image/png");
const imgProps = pdf.getImageProperties(imgData);
const imgHeight = (imgProps.height * pageWidth) / imgProps.width;

// Add canvas image just below the heading
pdf.addImage(imgData, "PNG", margin, y, pageWidth - margin * 2, imgHeight);
y += imgHeight;

// Save the PDF
pdf.save("document.pdf");
} else {
console.error("Failed to get canvas context");
}
};

img.src = `data:image/svg+xml;base64,${btoa(svgData)}`;
});
});
} else {
console.error("Unable to find the content to save as PDF");
}
};

const parseText = (text: string) => {
const lines: any[] = [];
const parser = new DOMParser();
const parsedHtml = parser.parseFromString(text, 'text/html');
parsedHtml.body.childNodes.forEach((node: ChildNode) => {
if (node.nodeType === Node.TEXT_NODE) {
lines.push({ text: node.textContent, style: 'normal' });
} else if (node.nodeName === 'B') {
lines.push({ text: node.textContent, style: 'bold' });
} else if (node.nodeName === 'I') {
lines.push({ text: node.textContent, style: 'italic' });
}
});
return lines;
};


return (
<div className="overflow-x-hidden">
Expand All @@ -32,24 +195,21 @@ function Workspace({ params }: any) {
name={fileData?.fileName || "New Document"}
setFullScreen={setFullScreen}
setFileData={setFileData}
onSaveAsPdf={saveAsPdf}
/>

<div className={`grid grid-cols-1 ${fullScreen ? "": "md:grid-cols-2"} overflow-x-none`}>
<div className={`${fullScreen ? "hidden" : "block"}
`}>
<Editor
onSaveTrigger={triggerSave}
fileId={params.fileId}
fileData={fileData}
/>
</div>
<div
className={`h-screen border-l`}
>
{/*Render the
Canvas component here.
*/}
<div className={`grid grid-cols-1 ${fullScreen ? "" : "md:grid-cols-2"} overflow-x-none`}>
<div className={`${fullScreen ? "hidden" : "block"}`}>
<Editor
ref={editorRef as MutableRefObject<EditorJS | null>}
onSaveTrigger={triggerSave}
fileId={params.fileId}
fileData={fileData}
/>
</div>
<div className={`h-screen border-l`}>
<Canvas
ref={canvasRef as MutableRefObject<any>}
onSaveTrigger={triggerSave}
fileId={params.fileId}
fileData={fileData}
Expand Down
15 changes: 10 additions & 5 deletions src/app/workspace/_components/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, forwardRef, useImperativeHandle } from "react";
import { Excalidraw, MainMenu, WelcomeScreen } from "@excalidraw/excalidraw";
import { FILE } from "../../dashboard/_components/FileList";
import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
Expand All @@ -7,19 +7,20 @@ import { useTheme } from "next-themes";
import { api } from "../../../../convex/_generated/api";
import { toast } from "sonner";

function Canvas({
const Canvas = forwardRef(({
onSaveTrigger,
fileId,
fileData,
}: {
onSaveTrigger: any;
fileId: any;
fileData: FILE;
}) {
}, ref) => {
const [whiteBoardData, setWhiteBoardData] = useState<any>();
const { theme } = useTheme();

const updateWhiteboard = useMutation(api.files.updateWhiteboard);

useEffect(() => {
onSaveTrigger && saveWhiteboard();
}, [onSaveTrigger]);
Expand All @@ -33,7 +34,11 @@ function Canvas({
});
};


useImperativeHandle(ref, () => ({
getSceneElements: () => whiteBoardData,
getAppState: () => ({ viewBackgroundColor: "#e6e6e6" }),
getFiles: () => [], // Implement getFiles based on your app's requirements
}));

const handleMermaidToExcalidraw = async () => {
const mermaidCode = `flowchart TD
Expand Down Expand Up @@ -107,6 +112,6 @@ function Canvas({
</div>
</>
);
}
});

export default Canvas;
Loading

0 comments on commit 7fd52cc

Please sign in to comment.