From 056c006928ee281a2ca260806ae433c2fb3686a2 Mon Sep 17 00:00:00 2001 From: Sid-80 Date: Tue, 25 Jun 2024 20:37:54 +0530 Subject: [PATCH 01/10] feat: api for private and public --- .env.example | 1 + convex/files.tsx | 26 ++++------------------ package.json | 1 + src/app/api/files/private/route.ts | 22 ++++++++++++++++++ src/app/api/files/public/route.ts | 24 ++++++++++++++++++++ src/app/api/teams/get/route.ts | 16 +++++++++++++ src/app/dashboard/_components/FileList.tsx | 1 - src/config/AxiosInstance.ts | 11 +++++++++ 8 files changed, 79 insertions(+), 23 deletions(-) create mode 100644 src/app/api/files/private/route.ts create mode 100644 src/app/api/files/public/route.ts create mode 100644 src/app/api/teams/get/route.ts create mode 100644 src/config/AxiosInstance.ts diff --git a/.env.example b/.env.example index ca5d3cd..eb32c15 100644 --- a/.env.example +++ b/.env.example @@ -12,4 +12,5 @@ NEXT_PUBLIC_EMAILJS_TEMPLATE_ID=your_id NEXT_PUBLIC_EMAILJS_API_KEY=your_id NEXT_PUBLIC_EMAILJS_SERVICE_ID=your_id RESEND_API_KEY=your_api_key +NEXT_PUBLIC_CLIENT_URL=http://localhost:3000 #use these variables only ;) \ No newline at end of file diff --git a/convex/files.tsx b/convex/files.tsx index 0d9ffad..556fdb8 100644 --- a/convex/files.tsx +++ b/convex/files.tsx @@ -154,19 +154,10 @@ export const getPrivateFiles = query({ export const changeToPrivate = mutation({ args: { - _id: v.id("files"), - teamId: v.id("teams"), - email: v.string(), + _id: v.id("files") }, handler: async (ctx, args) => { - const { _id, email, teamId } = args; - - const teamInfo = await await ctx.db.get(teamId); - - if (teamInfo.createdBy === email) { - return { status: 401 }; - } - + const { _id } = args; const res = await ctx.db.patch(_id, { private: true }); return res; }, @@ -174,19 +165,10 @@ export const changeToPrivate = mutation({ export const changeToPublic = mutation({ args: { - _id: v.id("files"), - teamId: v.id("teams"), - email: v.string(), + _id: v.id("files") }, handler: async (ctx, args) => { - const { _id, email, teamId } = args; - - const teamInfo = await await ctx.db.get(teamId); - - if (teamInfo.createdBy === email) { - return { status: 401 }; - } - + const { _id } = args; const res = await ctx.db.patch(_id, { private: false }); return res; }, diff --git a/package.json b/package.json index 67321fc..9d2b5aa 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@react-three/fiber": "^8.16.6", "@reduxjs/toolkit": "^2.2.4", "@studio-freight/lenis": "^1.0.42", + "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "convex": "^1.11.3", diff --git a/src/app/api/files/private/route.ts b/src/app/api/files/private/route.ts new file mode 100644 index 0000000..7f883d2 --- /dev/null +++ b/src/app/api/files/private/route.ts @@ -0,0 +1,22 @@ +import { api } from "../../../../../convex/_generated/api"; +import { ConvexHttpClient } from "convex/browser"; + +export const PUT = async(req: Request) => { + try { + const { teamId, email, fileId } = await req.json(); + + if (!teamId || !email || !fileId) return new Response('Parameters missing!!',{status: 401}); + + const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + const teamInfo = await client.query(api.teams.getTeamById,{_id:teamId}) + + if (teamInfo.createdBy === email) return new Response('Only owner can make changes!!',{status: 400}); + + await client.mutation(api.files.changeToPrivate,{_id:fileId}); + + return new Response('Changed to Private!!',{status: 200}); + } catch (err) { + console.log(err) + } +} diff --git a/src/app/api/files/public/route.ts b/src/app/api/files/public/route.ts new file mode 100644 index 0000000..c437af1 --- /dev/null +++ b/src/app/api/files/public/route.ts @@ -0,0 +1,24 @@ +import { api } from "../../../../../convex/_generated/api"; +import { ConvexHttpClient } from "convex/browser"; + +export const PUT = async(req: Request) => { + try { + const { teamId, email, fileId } = await req.json(); + + if (!teamId || !email || !fileId) return new Response('Parameters missing!!',{status: 401}); + + const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + const teamInfo = await client.query(api.teams.getTeamById,{_id:teamId}) + + if (teamInfo.createdBy === email) { + return new Response('Only owner can make changes!!',{status: 400}); + } + + await client.mutation(api.files.changeToPublic,{_id:fileId}); + + return new Response('Changed to Public!!',{status: 200}); + } catch (err) { + console.log(err) + } +} \ No newline at end of file diff --git a/src/app/api/teams/get/route.ts b/src/app/api/teams/get/route.ts new file mode 100644 index 0000000..6140806 --- /dev/null +++ b/src/app/api/teams/get/route.ts @@ -0,0 +1,16 @@ +import { api } from "../../../../../convex/_generated/api"; +import { ConvexHttpClient } from "convex/browser"; + +export const GET = async () => { + try { + console.log(process.env.NEXT_PUBLIC_CONVEX_URL!) + + const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + const teamInfo = await client.query(api.teams.getAllTeam); + + return Response.json(teamInfo); + } catch (err) { + console.log(err) + } +}; diff --git a/src/app/dashboard/_components/FileList.tsx b/src/app/dashboard/_components/FileList.tsx index d841f44..80ee72b 100644 --- a/src/app/dashboard/_components/FileList.tsx +++ b/src/app/dashboard/_components/FileList.tsx @@ -7,7 +7,6 @@ import { ArchiveRestore, Clock, Edit, - Users, CheckCircle2, } from "lucide-react"; import moment from "moment"; diff --git a/src/config/AxiosInstance.ts b/src/config/AxiosInstance.ts new file mode 100644 index 0000000..7a202a4 --- /dev/null +++ b/src/config/AxiosInstance.ts @@ -0,0 +1,11 @@ +import axios from 'axios'; + +const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_CLIENT_URL, // Replace with your API base URL + timeout: 10000, // Request timeout + headers: { + 'Content-Type': 'application/json', + }, +}); + +export default axiosInstance; \ No newline at end of file From 7fd52ccf220598e3fdfc6072d131444149558aa3 Mon Sep 17 00:00:00 2001 From: chitraa-jain <164042739+chitraa-cj@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:59:03 +0530 Subject: [PATCH 02/10] save as pdf option added --- next.config.mjs | 21 +- package.json | 4 + src/app/workspace/[fileId]/page.tsx | 198 ++++++++++++++++-- src/app/workspace/_components/Canvas.tsx | 15 +- src/app/workspace/_components/Editor.tsx | 80 +++---- .../workspace/_components/WorkspaceHeader.tsx | 12 +- 6 files changed, 263 insertions(+), 67 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index b4acabe..6e8602e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -17,5 +17,22 @@ export default withPWAConfig({ hostname: '*', } ] - } -}); \ No newline at end of file + }, + 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; + }, +}); diff --git a/package.json b/package.json index 67321fc..6270eef 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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" diff --git a/src/app/workspace/[fileId]/page.tsx b/src/app/workspace/[fileId]/page.tsx index 2654666..c2725ee 100644 --- a/src/app/workspace/[fileId]/page.tsx +++ b/src/app/workspace/[fileId]/page.tsx @@ -1,21 +1,31 @@ "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(); const [fullScreen, setFullScreen] = useState(false); + const editorRef = useRef(null); + const canvasRef = useRef(null); useEffect(() => { - params.fileId && getFileData(); - }, []); + if (params.fileId) { + getFileData(); + } + }, [params.fileId]); const getFileData = async () => { const result = await convex.query(api.files.getFileById, { @@ -23,7 +33,160 @@ function Workspace({ params }: any) { }); 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 (
@@ -32,24 +195,21 @@ function Workspace({ params }: any) { name={fileData?.fileName || "New Document"} setFullScreen={setFullScreen} setFileData={setFileData} + onSaveAsPdf={saveAsPdf} /> -
-
- -
-
- {/*Render the - Canvas component here. - */} +
+
+ } + onSaveTrigger={triggerSave} + fileId={params.fileId} + fileData={fileData} + /> +
+
} onSaveTrigger={triggerSave} fileId={params.fileId} fileData={fileData} diff --git a/src/app/workspace/_components/Canvas.tsx b/src/app/workspace/_components/Canvas.tsx index 6d4c8d1..0ed03c5 100644 --- a/src/app/workspace/_components/Canvas.tsx +++ b/src/app/workspace/_components/Canvas.tsx @@ -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"; @@ -7,7 +7,7 @@ import { useTheme } from "next-themes"; import { api } from "../../../../convex/_generated/api"; import { toast } from "sonner"; -function Canvas({ +const Canvas = forwardRef(({ onSaveTrigger, fileId, fileData, @@ -15,11 +15,12 @@ function Canvas({ onSaveTrigger: any; fileId: any; fileData: FILE; -}) { +}, ref) => { const [whiteBoardData, setWhiteBoardData] = useState(); const { theme } = useTheme(); const updateWhiteboard = useMutation(api.files.updateWhiteboard); + useEffect(() => { onSaveTrigger && saveWhiteboard(); }, [onSaveTrigger]); @@ -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 @@ -107,6 +112,6 @@ function Canvas({
); -} +}); export default Canvas; diff --git a/src/app/workspace/_components/Editor.tsx b/src/app/workspace/_components/Editor.tsx index 3002459..57f0a62 100644 --- a/src/app/workspace/_components/Editor.tsx +++ b/src/app/workspace/_components/Editor.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; -import EditorJS from "@editorjs/editorjs"; +import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from "react"; +import EditorJS, { OutputData } from "@editorjs/editorjs"; // @ts-ignore import Header from "@editorjs/header"; // @ts-ignore @@ -14,13 +14,12 @@ import Warning from "@editorjs/warning"; // @ts-ignore import InlineImage from 'editorjs-inline-image'; // @ts-ignore -import Table from '@editorjs/table' +import Table from '@editorjs/table'; import { useMutation } from "convex/react"; import { api } from "../../../../convex/_generated/api"; import { toast } from "sonner"; import { FILE } from "../../dashboard/_components/FileList"; import { useTheme } from "next-themes"; -import { table } from "console"; const rawDocument = { time: 1550476186479, @@ -44,43 +43,52 @@ const rawDocument = { version: "2.8.1", }; -function Editor({ - onSaveTrigger, - fileId, - fileData, -}: { - onSaveTrigger: any; +type EditorProps = { + onSaveTrigger: boolean; fileId: any; fileData: FILE; -}) { - const ref = useRef(null); +}; + +const Editor = forwardRef((props: EditorProps, ref) => { + const editorInstanceRef = useRef(null); const updateDocument = useMutation(api.files.updateDocument); const [document, setDocument] = useState(rawDocument); const { theme } = useTheme(); + useImperativeHandle(ref, () => ({ + get instance() { + return editorInstanceRef.current; + }, + save: () => { + return editorInstanceRef.current + ? editorInstanceRef.current.save() + : Promise.resolve({ blocks: [] } as OutputData); + }, + }), [editorInstanceRef]); + useEffect(() => { - if (fileData) { + if (props.fileData) { initEditor(); } return () => { - if (ref.current) { - ref.current.destroy(); - ref.current = null; + if (editorInstanceRef.current) { + editorInstanceRef.current.destroy(); + editorInstanceRef.current = null; } }; - }, [fileData]); + }, [props.fileData]); useEffect(() => { - onSaveTrigger && onSaveDocument(); - }, [onSaveTrigger]); + props.onSaveTrigger && onSaveDocument(); + }, [props.onSaveTrigger]); const initEditor = () => { - if (ref.current) { - ref.current.destroy(); + if (editorInstanceRef.current) { + editorInstanceRef.current.destroy(); } - ref.current = new EditorJS({ + editorInstanceRef.current = new EditorJS({ tools: { header: { class: Header, @@ -102,9 +110,9 @@ function Editor({ class: Checklist, inlineToolbar: true, }, - paragraph:{ - class:Paragraph, - inlineToolbar:true + paragraph: { + class: Paragraph, + inlineToolbar: true, }, warning: Warning, image: { @@ -118,8 +126,8 @@ function Editor({ appName: 'india', apiUrl: 'https://unsplash.com/s/photos/', maxResults: 30, - } - } + }, + }, }, table: { class: Table, @@ -127,23 +135,23 @@ function Editor({ config: { rows: 2, cols: 3, - withHeadings:true + withHeadings: true, }, }, }, holder: "editorjs", - data: fileData?.document ? JSON.parse(fileData.document) : rawDocument, + data: props.fileData?.document ? JSON.parse(props.fileData.document) : rawDocument, }); }; const onSaveDocument = () => { - if (ref.current) { - ref.current + if (editorInstanceRef.current) { + editorInstanceRef.current .save() .then((outputData) => { updateDocument({ - _id: fileId, + _id: props.fileId, document: JSON.stringify(outputData), }).then( (resp) => { @@ -160,17 +168,17 @@ function Editor({ } }; - - return (
); -} +}); + +Editor.displayName = 'Editor'; export default Editor; diff --git a/src/app/workspace/_components/WorkspaceHeader.tsx b/src/app/workspace/_components/WorkspaceHeader.tsx index f0c8a8a..dc58352 100644 --- a/src/app/workspace/_components/WorkspaceHeader.tsx +++ b/src/app/workspace/_components/WorkspaceHeader.tsx @@ -7,8 +7,7 @@ import { toast } from "sonner"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { GenAIModal } from "@/components/shared/AiModal"; - -function WorkspaceHeader({ onSave, name, setFullScreen, setFileData }: any) { +function WorkspaceHeader({ onSave, onSaveAsPdf, name, setFullScreen, setFileData }: any) { return ( <>
@@ -27,7 +26,10 @@ function WorkspaceHeader({ onSave, name, setFullScreen, setFileData }: any) {
+
@@ -52,9 +54,9 @@ function WorkspaceHeader({ onSave, name, setFullScreen, setFileData }: any) { }} >

- Share + Share

- {" "} +
From 293b8d09c56bcc8039dd02d42a8eb0bbdff6aa64 Mon Sep 17 00:00:00 2001 From: chitraa-jain <164042739+chitraa-cj@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:32:31 +0530 Subject: [PATCH 03/10] added displayname --- src/app/workspace/_components/Canvas.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/workspace/_components/Canvas.tsx b/src/app/workspace/_components/Canvas.tsx index 0ed03c5..1f05fa6 100644 --- a/src/app/workspace/_components/Canvas.tsx +++ b/src/app/workspace/_components/Canvas.tsx @@ -114,4 +114,6 @@ const Canvas = forwardRef(({ ); }); +Canvas.displayName = "Canvas"; + export default Canvas; From 1622c0cd84fc0aa07b7a2dd52088f500dd159a1e Mon Sep 17 00:00:00 2001 From: Sid-80 Date: Wed, 26 Jun 2024 00:04:03 +0530 Subject: [PATCH 04/10] feat: UI for private and public file --- src/app/api/files/private/route.ts | 9 ++-- src/app/api/files/public/route.ts | 2 +- src/app/dashboard/_components/FileList.tsx | 47 +++++++++++++---- src/app/dashboard/archive/page.tsx | 1 + src/app/dashboard/page.tsx | 1 + src/components/shared/FileStatusModal.tsx | 60 ++++++++++++++++++++++ src/components/ui/badge.tsx | 36 +++++++++++++ src/lib/API-URLs.ts | 2 + 8 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 src/components/shared/FileStatusModal.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/lib/API-URLs.ts diff --git a/src/app/api/files/private/route.ts b/src/app/api/files/private/route.ts index 7f883d2..d114b20 100644 --- a/src/app/api/files/private/route.ts +++ b/src/app/api/files/private/route.ts @@ -5,13 +5,16 @@ export const PUT = async(req: Request) => { try { const { teamId, email, fileId } = await req.json(); + console.log(teamId,email,fileId) + if (!teamId || !email || !fileId) return new Response('Parameters missing!!',{status: 401}); const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); - const teamInfo = await client.query(api.teams.getTeamById,{_id:teamId}) - - if (teamInfo.createdBy === email) return new Response('Only owner can make changes!!',{status: 400}); + const teamInfo = await client.query(api.teams.getTeamById,{_id:teamId}); + + if (teamInfo.createdBy !== email) return new Response('Only owner can make changes!!',{status: 400}); + await client.mutation(api.files.changeToPrivate,{_id:fileId}); diff --git a/src/app/api/files/public/route.ts b/src/app/api/files/public/route.ts index c437af1..851805b 100644 --- a/src/app/api/files/public/route.ts +++ b/src/app/api/files/public/route.ts @@ -11,7 +11,7 @@ export const PUT = async(req: Request) => { const teamInfo = await client.query(api.teams.getTeamById,{_id:teamId}) - if (teamInfo.createdBy === email) { + if (teamInfo.createdBy !== email) { return new Response('Only owner can make changes!!',{status: 400}); } diff --git a/src/app/dashboard/_components/FileList.tsx b/src/app/dashboard/_components/FileList.tsx index 80ee72b..a66b40f 100644 --- a/src/app/dashboard/_components/FileList.tsx +++ b/src/app/dashboard/_components/FileList.tsx @@ -29,6 +29,8 @@ import { } from "@/components/ui/alert-dialog"; import RenameFileModal from "@/components/shared/RenameFileModal"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import FileStatusModal from "@/components/shared/FileStatusModal"; export interface FILE { archive: boolean; @@ -39,6 +41,7 @@ export interface FILE { whiteboard: string; _id: string; _creationTime: number; + private: boolean; } const ActionDialog = ({ @@ -117,6 +120,7 @@ const FileRow = ({ index, isSubmitted, authorData, + user }: { file: FILE; picture: string; @@ -128,6 +132,7 @@ const FileRow = ({ index: number; isSubmitted: boolean; authorData: any[]; + user:any; }) => ( router.push("/workspace/" + file._id)} > - {authorData.map((author,index) => - (author.email === file.createdBy && ( - - - - {author.name.charAt(0)} - - - ) ) + {authorData.map( + (author, index) => + author.email === file.createdBy && ( + + + + {author.name.charAt(0)} + + + ) )} + + + {pathname === "/dashboard" && ( @@ -202,9 +224,11 @@ const FileRow = ({ function FileList({ fileList, picture, + user }: { fileList?: FILE[]; picture: string; + user:any; }) { const router = useRouter(); const convex = useConvex(); @@ -221,9 +245,9 @@ function FileList({ useEffect(() => { const getData = async () => { let listOfCreators: string[] = []; - authorData.forEach((user:any)=>{ + authorData.forEach((user: any) => { listOfCreators.push(user.email); - }) + }); fileList?.forEach(async (file) => { if (!listOfCreators.includes(file.createdBy)) { @@ -344,6 +368,7 @@ function FileList({ {(sortedFiles.length > 0 ? sortedFiles : safeFileList).map( (file, index) => ( diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 4fc2e7d..ce0b790 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -174,6 +174,7 @@ function Dashboard() {
diff --git a/src/components/shared/FileStatusModal.tsx b/src/components/shared/FileStatusModal.tsx new file mode 100644 index 0000000..4891c80 --- /dev/null +++ b/src/components/shared/FileStatusModal.tsx @@ -0,0 +1,60 @@ +import { useState } from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "../ui/alert-dialog"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; + +type Props = { + dialogTitle: string; + dialogDescription: string; + successTitle: string; + privateFIle: boolean; + fileId: string; + email: string; +}; + +export default function FileStatusModal({ + dialogTitle, + dialogDescription, + successTitle, + privateFIle, + fileId, + email, +}: Props) { + const [isSubmitted, setIsSubmitted] = useState(false); + + return ( + + + + {!privateFIle ? "Public" : "Private"} + + + + {!isSubmitted && ( + <> + + {dialogTitle} + + {dialogDescription} + + + + Cancel + + + + )} + + + ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/lib/API-URLs.ts b/src/lib/API-URLs.ts new file mode 100644 index 0000000..1abae95 --- /dev/null +++ b/src/lib/API-URLs.ts @@ -0,0 +1,2 @@ +export const changeToPrivateUrl = "/api/files/private" +export const changeToPublicUrl = "/api/files/public" \ No newline at end of file From 15fd9b1516d897847d0cd6d80fb27e022b3f753d Mon Sep 17 00:00:00 2001 From: Sid-80 Date: Wed, 26 Jun 2024 00:13:44 +0530 Subject: [PATCH 05/10] feat: responsive --- src/app/dashboard/_components/FileList.tsx | 57 ++++++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/app/dashboard/_components/FileList.tsx b/src/app/dashboard/_components/FileList.tsx index a66b40f..c443db0 100644 --- a/src/app/dashboard/_components/FileList.tsx +++ b/src/app/dashboard/_components/FileList.tsx @@ -120,7 +120,7 @@ const FileRow = ({ index, isSubmitted, authorData, - user + user, }: { file: FILE; picture: string; @@ -132,7 +132,7 @@ const FileRow = ({ index: number; isSubmitted: boolean; authorData: any[]; - user:any; + user: any; }) => ( @@ -224,11 +226,11 @@ const FileRow = ({ function FileList({ fileList, picture, - user + user, }: { fileList?: FILE[]; picture: string; - user:any; + user: any; }) { const router = useRouter(); const convex = useConvex(); @@ -443,14 +445,37 @@ function FileList({ />
-
- - {moment(file._creationTime).format("YYYY-MM-DD")} -
-
- - {moment(file._creationTime).format("YYYY-MM-DD")} +
+
+
+ + {moment(file._creationTime).format("YYYY-MM-DD")} +
+
+ + {moment(file._creationTime).format("YYYY-MM-DD")} +
+
+
+
Date: Wed, 26 Jun 2024 16:03:27 +0530 Subject: [PATCH 06/10] feat/356 --- src/app/workspace/[fileId]/page.tsx | 121 ++++++++++++++-------- src/components/shared/FileStatusModal.tsx | 83 ++++++++++++++- 2 files changed, 160 insertions(+), 44 deletions(-) diff --git a/src/app/workspace/[fileId]/page.tsx b/src/app/workspace/[fileId]/page.tsx index c2725ee..41b3211 100644 --- a/src/app/workspace/[fileId]/page.tsx +++ b/src/app/workspace/[fileId]/page.tsx @@ -6,12 +6,13 @@ 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"; +import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs"; +import { useRouter } from "next/navigation"; // Dynamic imports for server-side libraries -const jsPDFPromise = import('jspdf'); -const excalidrawPromise = import('@excalidraw/excalidraw'); +const jsPDFPromise = import("jspdf"); +const excalidrawPromise = import("@excalidraw/excalidraw"); function Workspace({ params }: any) { const [triggerSave, setTriggerSave] = useState(false); @@ -21,29 +22,49 @@ function Workspace({ params }: any) { const editorRef = useRef(null); const canvasRef = useRef(null); + const { user, isAuthenticated, isLoading } = useKindeBrowserClient(); + const router = useRouter(); + + console.log(user, isAuthenticated, isLoading); + useEffect(() => { - if (params.fileId) { + if (params.fileId && !isLoading) { getFileData(); } - }, [params.fileId]); + }, [params.fileId, isLoading]); const getFileData = async () => { const result = await convex.query(api.files.getFileById, { _id: params.fileId, }); + + const teamInfo = await convex.query(api.teams.getTeamById, { + _id: result.teamId, + }); + + if (result.private) { + if (user) { + if (!teamInfo.teamMembers.includes(user.email)) { + router.push("/"); + } + console.log(user); + } else { + router.push("/"); + } + } 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(); @@ -52,51 +73,56 @@ function Workspace({ params }: any) { 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' }]; + 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' })); + 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 = [{ text: block.data.text, style: "normal" }]; } - + lines.forEach((line: any) => { if (y + 10 > textHeight) { pdf.addPage(); y = margin; } - + switch (line.style) { - case 'bold': + case "bold": pdf.setFont("helvetica", "bold"); break; - case 'italic': + case "italic": pdf.setFont("helvetica", "italic"); break; - case 'header': + case "header": pdf.setFont("helvetica", "bold"); - const headerWidth = pdf.getStringUnitWidth(line.text) * 16 / pdf.internal.scaleFactor; + 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') { + + 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) => { @@ -109,17 +135,17 @@ function Workspace({ params }: any) { }); } }); - + // 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 @@ -129,40 +155,48 @@ function Workspace({ params }: any) { 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 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); + 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)}`; }); }); @@ -170,23 +204,22 @@ function Workspace({ params }: any) { 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'); + 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' }); + 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 (
@@ -198,7 +231,9 @@ function Workspace({ params }: any) { onSaveAsPdf={saveAsPdf} /> -
+ {!isLoading &&
} @@ -215,7 +250,7 @@ function Workspace({ params }: any) { fileData={fileData} />
-
+
}
); } diff --git a/src/components/shared/FileStatusModal.tsx b/src/components/shared/FileStatusModal.tsx index 4891c80..f554ba4 100644 --- a/src/components/shared/FileStatusModal.tsx +++ b/src/components/shared/FileStatusModal.tsx @@ -12,6 +12,11 @@ import { } from "../ui/alert-dialog"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; +import { useSelector } from "react-redux"; +import { RootState } from "@/app/store"; +import { changeToPrivateUrl, changeToPublicUrl } from "@/lib/API-URLs"; +import axiosInstance from "@/config/AxiosInstance"; +import { CheckCircle2 } from "lucide-react"; type Props = { dialogTitle: string; @@ -31,7 +36,45 @@ export default function FileStatusModal({ email, }: Props) { const [isSubmitted, setIsSubmitted] = useState(false); + const [isError, setError] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); + const teamId = useSelector((state: RootState) => state.team.teamId); + const FileHandler = async () => { + if (!privateFIle) { + try { + const res = await axiosInstance.put(changeToPrivateUrl, { + teamId, + email, + fileId, + }); + if (res.status === 200) { + setIsSubmitted(true); + } else { + setError(true); + setErrorMsg("Error occured!! Please try again later!!"); + } + } catch (err) { + console.log(err); + } + } else { + try { + const res = await axiosInstance.put(changeToPublicUrl, { + teamId, + email, + fileId, + }); + if (res.status === 200) { + setIsSubmitted(true); + } else { + setError(true); + setErrorMsg("Error occured!! Please try again later!!"); + } + } catch (err) { + console.log(err); + } + } + }; return ( @@ -50,7 +93,45 @@ export default function FileStatusModal({ Cancel - + + + + )} + + {isSubmitted && ( + <> + + +

{successTitle}

+
+
+ + { + window.location.reload(); + }} + > + Continue + + + + )} + + {isError && ( + <> + + +

{errorMsg}

+
+
+ + { + window.location.reload(); + }} + > + Continue + )} From fa4559a0f6dc55bad080f0951d2a21f80b8ee1bf Mon Sep 17 00:00:00 2001 From: Sid-80 Date: Thu, 27 Jun 2024 00:18:36 +0530 Subject: [PATCH 07/10] feat: read and write api --- convex/files.tsx | 24 +++++++++++++++++++++++ convex/schemas.ts | 5 +++-- src/app/api/files/read/route.ts | 33 ++++++++++++++++++++++++++++++++ src/app/api/files/write/route.ts | 29 ++++++++++++++++++++++++++++ src/app/api/teams/get/route.ts | 4 ++-- 5 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 src/app/api/files/read/route.ts create mode 100644 src/app/api/files/write/route.ts diff --git a/convex/files.tsx b/convex/files.tsx index 556fdb8..51a5d37 100644 --- a/convex/files.tsx +++ b/convex/files.tsx @@ -173,3 +173,27 @@ export const changeToPublic = mutation({ return res; }, }); + +export const updateWrite = mutation({ + args: { + _id: v.id("files"), + writtenBy: v.array(v.string()) + }, + handler: async (ctx, args) => { + const { _id,writtenBy } = args; + const res = await ctx.db.patch(_id, { writtenBy, write:true, read:true }); + return res; + }, +}); + +export const updateRead = mutation({ + args: { + _id: v.id("files"), + writtenBy: v.array(v.string()) + }, + handler: async (ctx, args) => { + const { _id,writtenBy } = args; + const res = await ctx.db.patch(_id, { writtenBy, write:false, read:true }); + return res; + }, +}); \ No newline at end of file diff --git a/convex/schemas.ts b/convex/schemas.ts index d4d7cf8..32969ac 100644 --- a/convex/schemas.ts +++ b/convex/schemas.ts @@ -3,7 +3,6 @@ import { v } from "convex/values"; export default defineSchema({ teams: defineTable({ - teamName: v.string(), createdBy: v.string(), teamMembers: v.array(v.string()), @@ -21,6 +20,8 @@ export default defineSchema({ document: v.string(), whiteboard: v.string(), private: v.string(), - accessedBy: v.array(v.string()) + writtenBy: v.array(v.string()), + write:v.boolean(), + read:v.boolean() }), }); diff --git a/src/app/api/files/read/route.ts b/src/app/api/files/read/route.ts new file mode 100644 index 0000000..e6266d5 --- /dev/null +++ b/src/app/api/files/read/route.ts @@ -0,0 +1,33 @@ +import { api } from "../../../../../convex/_generated/api"; +import { ConvexHttpClient } from "convex/browser"; + +export const PUT = async (req: Request) => { + try { + const { teamId, email, memberEmail, writtenBy, fileId } = await req.json(); + + if (!teamId || !memberEmail || !email || !fileId) + return new Response("Parameters missing!!", { status: 401 }); + + const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + const teamInfo = await client.query(api.teams.getTeamById, { _id: teamId }); + + if (!teamInfo.teamMembers.includes(memberEmail)) { + return new Response("User is not member of the team", { status: 400 }); + } + + if (teamInfo.createdBy !== email) { + return new Response("Only owner can make changes!!", { status: 400 }); + } + + const updatedWrittenBy = Array.isArray(writtenBy) + ? writtenBy.filter(writer => writer !== memberEmail) + : []; + + await client.mutation(api.files.updateRead, { _id: fileId, writtenBy:updatedWrittenBy }); + + return new Response("Changed to Public!!", { status: 200 }); + } catch (err) { + console.log(err); + } +}; \ No newline at end of file diff --git a/src/app/api/files/write/route.ts b/src/app/api/files/write/route.ts new file mode 100644 index 0000000..8b057b2 --- /dev/null +++ b/src/app/api/files/write/route.ts @@ -0,0 +1,29 @@ +import { api } from "../../../../../convex/_generated/api"; +import { ConvexHttpClient } from "convex/browser"; + +export const PUT = async (req: Request) => { + try { + const { teamId, email, memberEmail, writtenBy, fileId } = await req.json(); + + if (!teamId || !memberEmail || !email || !fileId) + return new Response("Parameters missing!!", { status: 401 }); + + const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + const teamInfo = await client.query(api.teams.getTeamById, { _id: teamId }); + + if (!teamInfo.teamMembers.includes(memberEmail)) { + return new Response("User is not member of the team", { status: 400 }); + } + + if (teamInfo.createdBy !== email) { + return new Response("Only owner can make changes!!", { status: 400 }); + } + + await client.mutation(api.files.updateWrite, { _id: fileId, writtenBy: [...writtenBy,memberEmail] }); + + return new Response("Changed to Public!!", { status: 200 }); + } catch (err) { + console.log(err); + } +}; \ No newline at end of file diff --git a/src/app/api/teams/get/route.ts b/src/app/api/teams/get/route.ts index 6140806..e5d5b23 100644 --- a/src/app/api/teams/get/route.ts +++ b/src/app/api/teams/get/route.ts @@ -1,15 +1,15 @@ +import { NextResponse } from "next/server"; import { api } from "../../../../../convex/_generated/api"; import { ConvexHttpClient } from "convex/browser"; export const GET = async () => { try { - console.log(process.env.NEXT_PUBLIC_CONVEX_URL!) const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); const teamInfo = await client.query(api.teams.getAllTeam); - return Response.json(teamInfo); + return NextResponse.json(teamInfo); } catch (err) { console.log(err) } From 16424c8d771255c146d38b0cc4847b15704f880e Mon Sep 17 00:00:00 2001 From: chitraa-jain <164042739+chitraa-cj@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:20:29 +0530 Subject: [PATCH 08/10] fixed the pdf flowchart issue --- src/app/workspace/[fileId]/page.tsx | 45 +++++++++++++---------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/app/workspace/[fileId]/page.tsx b/src/app/workspace/[fileId]/page.tsx index c2725ee..62b6536 100644 --- a/src/app/workspace/[fileId]/page.tsx +++ b/src/app/workspace/[fileId]/page.tsx @@ -33,7 +33,7 @@ function Workspace({ params }: any) { }); setFileData(result); }; - + const saveAsPdf = async () => { const { default: jsPDF } = await jsPDFPromise; const { exportToSvg } = await excalidrawPromise; @@ -68,7 +68,6 @@ function Workspace({ params }: any) { 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' }]; } @@ -97,7 +96,6 @@ function Workspace({ params }: any) { } 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) { @@ -110,7 +108,6 @@ function Workspace({ params }: any) { } }); - // Reset font style and size after each block pdf.setFont("helvetica", "normal"); pdf.setFontSize(12); }); @@ -122,21 +119,9 @@ function Workspace({ params }: any) { exportToSvg({ elements: elements, - appState: { ...appState, exportBackground: false }, // No background + appState: { ...appState, exportBackground: false }, 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"); @@ -150,13 +135,24 @@ function Workspace({ params }: any) { const imgData = canvas.toDataURL("image/png"); const imgProps = pdf.getImageProperties(imgData); - const imgHeight = (imgProps.height * pageWidth) / imgProps.width; + let imgWidth = pageWidth - margin * 2; + let imgHeight = (imgProps.height * imgWidth) / imgProps.width; + + // Check if the image height exceeds the remaining page height + if (y + imgHeight > pageHeight - margin) { + pdf.addPage(); + y = margin; + } - // Add canvas image just below the heading - pdf.addImage(imgData, "PNG", margin, y, pageWidth - margin * 2, imgHeight); - y += imgHeight; + // Check if the image height exceeds the page height and scale it down if necessary + if (imgHeight > pageHeight - margin * 2) { + const scaleFactor = (pageHeight - margin * 2) / imgHeight; + imgHeight *= scaleFactor; + imgWidth *= scaleFactor; + } + + pdf.addImage(imgData, "PNG", margin, y, imgWidth, imgHeight, undefined, "FAST"); - // Save the PDF pdf.save("document.pdf"); } else { console.error("Failed to get canvas context"); @@ -169,8 +165,8 @@ function Workspace({ params }: any) { } else { console.error("Unable to find the content to save as PDF"); } - }; - + }; + const parseText = (text: string) => { const lines: any[] = []; const parser = new DOMParser(); @@ -186,7 +182,6 @@ function Workspace({ params }: any) { }); return lines; }; - return (
From 9de07e62afaf4cb7996908c3e0d99011b781e282 Mon Sep 17 00:00:00 2001 From: chitraa-jain <164042739+chitraa-cj@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:31:59 +0530 Subject: [PATCH 09/10] added --- src/app/workspace/[fileId]/page.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/app/workspace/[fileId]/page.tsx b/src/app/workspace/[fileId]/page.tsx index 62b6536..97898e8 100644 --- a/src/app/workspace/[fileId]/page.tsx +++ b/src/app/workspace/[fileId]/page.tsx @@ -139,11 +139,21 @@ function Workspace({ params }: any) { let imgHeight = (imgProps.height * imgWidth) / imgProps.width; // Check if the image height exceeds the remaining page height - if (y + imgHeight > pageHeight - margin) { + if (y + imgHeight + 20 > pageHeight - margin) { // 20 for the heading space pdf.addPage(); y = margin; } + // 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) * 16 / pdf.internal.scaleFactor; + pdf.text(headingText, (pageWidth - headingWidth) / 2, y); + pdf.setFontSize(12); // Reset font size + pdf.setFont("helvetica", "normal"); + y += 20; // Adjust y position to avoid overlap with the heading + // Check if the image height exceeds the page height and scale it down if necessary if (imgHeight > pageHeight - margin * 2) { const scaleFactor = (pageHeight - margin * 2) / imgHeight; From bfa07869591a30e169a560a1df146c2abf349869 Mon Sep 17 00:00:00 2001 From: Sid-80 Date: Fri, 28 Jun 2024 00:08:48 +0530 Subject: [PATCH 10/10] fix: create file api --- convex/files.tsx | 10 +++++++--- convex/schemas.ts | 1 + src/app/api/files/read/route.ts | 8 ++++---- src/components/shared/MemberModal.tsx | 6 +++++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/convex/files.tsx b/convex/files.tsx index 51a5d37..aad9428 100644 --- a/convex/files.tsx +++ b/convex/files.tsx @@ -20,6 +20,10 @@ export const createFile = mutation({ document, whiteboard, private: false, + read:true, + write:false, + writtenBy:[createdBy], + readBy:[createdBy] }); return result; }, @@ -189,11 +193,11 @@ export const updateWrite = mutation({ export const updateRead = mutation({ args: { _id: v.id("files"), - writtenBy: v.array(v.string()) + readBy: v.array(v.string()) }, handler: async (ctx, args) => { - const { _id,writtenBy } = args; - const res = await ctx.db.patch(_id, { writtenBy, write:false, read:true }); + const { _id,readBy } = args; + const res = await ctx.db.patch(_id, { readBy, write:false, read:true }); return res; }, }); \ No newline at end of file diff --git a/convex/schemas.ts b/convex/schemas.ts index 32969ac..1bfe194 100644 --- a/convex/schemas.ts +++ b/convex/schemas.ts @@ -21,6 +21,7 @@ export default defineSchema({ whiteboard: v.string(), private: v.string(), writtenBy: v.array(v.string()), + readBy: v.array(v.string()), write:v.boolean(), read:v.boolean() }), diff --git a/src/app/api/files/read/route.ts b/src/app/api/files/read/route.ts index e6266d5..95da066 100644 --- a/src/app/api/files/read/route.ts +++ b/src/app/api/files/read/route.ts @@ -3,7 +3,7 @@ import { ConvexHttpClient } from "convex/browser"; export const PUT = async (req: Request) => { try { - const { teamId, email, memberEmail, writtenBy, fileId } = await req.json(); + const { teamId, email, memberEmail, readBy, fileId } = await req.json(); if (!teamId || !memberEmail || !email || !fileId) return new Response("Parameters missing!!", { status: 401 }); @@ -20,11 +20,11 @@ export const PUT = async (req: Request) => { return new Response("Only owner can make changes!!", { status: 400 }); } - const updatedWrittenBy = Array.isArray(writtenBy) - ? writtenBy.filter(writer => writer !== memberEmail) + const updatedReadBy = Array.isArray(readBy) + ? readBy.filter(writer => writer !== memberEmail) : []; - await client.mutation(api.files.updateRead, { _id: fileId, writtenBy:updatedWrittenBy }); + await client.mutation(api.files.updateRead, { _id: fileId, readBy:updatedReadBy }); return new Response("Changed to Public!!", { status: 200 }); } catch (err) { diff --git a/src/components/shared/MemberModal.tsx b/src/components/shared/MemberModal.tsx index 9799f63..c8ac6f3 100644 --- a/src/components/shared/MemberModal.tsx +++ b/src/components/shared/MemberModal.tsx @@ -11,6 +11,8 @@ import { import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { useSelector } from "react-redux"; import { RootState } from "@/app/store"; +import { FileListContext } from "@/app/_context/FilesListContext"; +import { useState,useContext,useEffect } from "react"; type Props = { image: string; @@ -54,7 +56,9 @@ export default function MemberModal({ {teamName} -

File Access :

+ + + Email : {email}