Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

save as pdf option added #359

Merged
merged 2 commits into from
Jun 25, 2024
Merged
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
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
17 changes: 12 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,8 @@ function Canvas({
</div>
</>
);
}
});

Canvas.displayName = "Canvas";

export default Canvas;
Loading
Loading