From 8cbc4eda14ee5600154f6cec7b497de6e4d80a55 Mon Sep 17 00:00:00 2001 From: Dhara Pandya Date: Mon, 24 Nov 2025 15:34:11 +0530 Subject: [PATCH] feat:allow multiple markdown files in projects --- package-lock.json | 56 ++++++++---- package.json | 1 + .../blocks/BuilderPage/DashboardHome.jsx | 66 +++++++++++++- .../blocks/BuilderPage/MarkdownBlock.jsx | 6 +- .../BuilderPage/blocks/SkillIconsBlock.jsx | 11 ++- src/components/ui/filetabs.jsx | 39 +++++++++ src/context/FileContext.jsx | 54 ++++++++++++ src/main.jsx | 5 +- src/pages/Builder.jsx | 87 ++++++++++++++----- src/pages/Editor.jsx | 63 ++++++++++++++ 10 files changed, 343 insertions(+), 45 deletions(-) create mode 100644 src/components/ui/filetabs.jsx create mode 100644 src/context/FileContext.jsx create mode 100644 src/pages/Editor.jsx diff --git a/package-lock.json b/package-lock.json index 6f25635..03ac94f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -237,6 +237,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1979,8 +1980,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", @@ -1999,6 +1999,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -6141,8 +6142,7 @@ "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", @@ -6578,6 +6578,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6588,6 +6589,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6622,8 +6624,7 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/three": { "version": "0.181.0", @@ -6801,8 +6802,7 @@ "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", "integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xyflow/react": { "version": "12.9.3", @@ -6906,6 +6906,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7199,6 +7200,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7407,6 +7409,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -7734,6 +7737,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -8143,6 +8147,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8512,7 +8517,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -9747,6 +9753,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11299,8 +11306,7 @@ "version": "0.22.0", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromark": { "version": "4.0.2", @@ -11317,6 +11323,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -11953,7 +11960,8 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/mime-db": { "version": "1.52.0", @@ -12183,7 +12191,6 @@ "resolved": "https://registry.npmjs.org/on-change/-/on-change-4.0.2.tgz", "integrity": "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA==", "license": "MIT", - "peer": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -12480,6 +12487,7 @@ "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.0.tgz", "integrity": "sha512-tisx8XN/PWTL3uXz2mt8bjlMS1wiOUSCK3ixi4zjwUCFmP8XW8hNhXwrxwd2zf2VmCyCQ3GUaLm7GLnkkBbDsQ==", "license": "Zlib", + "peer": true, "peerDependencies": { "three": ">= 0.157.0 < 0.182.0" } @@ -12837,6 +12845,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12867,6 +12876,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12902,6 +12912,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.1.tgz", "integrity": "sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13031,6 +13042,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -13245,7 +13257,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -13671,6 +13684,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -13831,8 +13845,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/serialize-javascript": { "version": "6.0.2", @@ -14444,7 +14457,8 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -14494,6 +14508,7 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "devOptional": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -14520,7 +14535,8 @@ "version": "0.181.2", "resolved": "https://registry.npmjs.org/three/-/three-0.181.2.tgz", "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/thumbhash": { "version": "0.1.1", @@ -14829,6 +14845,7 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", + "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -15061,6 +15078,7 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -15176,6 +15194,7 @@ "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.5.tgz", "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", @@ -15739,6 +15758,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 260694b..f8f91fd 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.1.1", "tailwindcss": "^4.1.17", + "uuid": "^13.0.0", "three": "^0.181.2", "tokenlens": "^1.3.1", "use-stick-to-bottom": "^1.1.1", diff --git a/src/components/blocks/BuilderPage/DashboardHome.jsx b/src/components/blocks/BuilderPage/DashboardHome.jsx index 13701e0..67c4e4e 100644 --- a/src/components/blocks/BuilderPage/DashboardHome.jsx +++ b/src/components/blocks/BuilderPage/DashboardHome.jsx @@ -1,6 +1,7 @@ import Editor from "../BuilderPage/Editor"; import Preview from "../BuilderPage/Preview"; import Raw from "../BuilderPage/Raw"; +import { useRef, useState } from "react"; export default function DashboardHome({ activeTab, @@ -10,11 +11,74 @@ export default function DashboardHome({ onBlockDelete, onBlockAdd, }) { + const dropZoneRef= useRef(null); + const [isDraggingFile, setIsDraggingFile]= useState(false); + + const handleDragOver=(e)=>{ + e.preventDefault(); + e.stopPropagation(); + setIsDraggingFile(true); + }; + + const handleDragLeave=(e)=>{ + e.preventDefault(); + e.stopPropagation(); + setIsDraggingFile(false); + }; + + const handleDrop=(e)=>{ + e.preventDefault(); + e.stopPropagation(); + setIsDraggingFile(false); + + const files= e.dataTransfer.files; + if(files.length === 0) return; + + const file= files[0]; + const reader= new FileReader(); + reader.onload= (event)=>{ + const text= event.target.result; + + onBlocksChange([ + { + id: Date.now(), + type: "paragraph", + content: text, + }, + ]); +}; +reader.readAsText(file); +}; return (
{activeTab === "editor" && ( -
+
{ + // if dragging a file, allow file drop + if(e.dataTransfer.types.includes("Files")) return; + //block browser behavior + e.preventDefault(); + + }} + onDrop={(e)=>{ + //if a file then let DashboardHome handle it + if(e.dataTransfer.types.includes("Files")){ + handleDrop(e); + return; + } + //block open in new tab + e.preventDefault() + }} + onDragStart={(e)=>{ + //allow only dnd draggable handles + if(e.target.closest("[data-dnd-draggable]")){ + e.preventDefault(); + } + + }} + > + - +
diff --git a/src/components/blocks/BuilderPage/blocks/SkillIconsBlock.jsx b/src/components/blocks/BuilderPage/blocks/SkillIconsBlock.jsx index 3efc265..56cbcb9 100644 --- a/src/components/blocks/BuilderPage/blocks/SkillIconsBlock.jsx +++ b/src/components/blocks/BuilderPage/blocks/SkillIconsBlock.jsx @@ -54,6 +54,8 @@ function SortableIcon({ icon, onRemove }) { "group flex items-center gap-1.5 px-2 py-1 bg-muted/40 hover:bg-muted rounded border border-border/50 text-xs font-mono cursor-move select-none transition-colors", isDragging && "ring-2 ring-ring ring-offset-2 bg-background" )} + draggable={false} + onDragStart={(e)=>e.preventDefault()} >
@@ -98,12 +100,12 @@ export default function SkillIconsBlock({ block, onUpdate }) { const handleDragEnd = (event) => { const { active, over } = event; - if (over && active.id !== over.id) { + if (!over && active.id !== over.id) return; const oldIndex = selectedIcons.indexOf(active.id); const newIndex = selectedIcons.indexOf(over.id); const newIcons = arrayMove(selectedIcons, oldIndex, newIndex); handleUpdate({ icons: newIcons.join(",") }); - } + }; const generateIconsUrl = () => { @@ -232,7 +234,10 @@ export default function SkillIconsBlock({ block, onUpdate }) { onDragEnd={handleDragEnd} > -
+
e.preventDefault()} + onDrop={(e)=>e.preventDefault()} + > {selectedIcons.map((icon) => ( ))} diff --git a/src/components/ui/filetabs.jsx b/src/components/ui/filetabs.jsx new file mode 100644 index 0000000..b42038b --- /dev/null +++ b/src/components/ui/filetabs.jsx @@ -0,0 +1,39 @@ +import {useFiles} from "@/context/FileContext"; +import {X} from "lucide-react"; + + +export default function FileTabs() { + const {files, addFile, switchFile, closeFile}= useFiles(); + + return ( +
+ {files.map((file)=>( +
e.preventDefault()} + className={`px-3 py-1 rounded-t-md cursor-pointer flex items-center gap-2 ${ + file.isActive?"bg-background shadow-sm": "opacity-70" + } `} + onClick={()=>switchFile(file.id)} + > + {file.name} + { + e.stopPropagation(); + closeFile(file.id); + }} + /> +
+ ))} + +
+ ); +} \ No newline at end of file diff --git a/src/context/FileContext.jsx b/src/context/FileContext.jsx new file mode 100644 index 0000000..d86261b --- /dev/null +++ b/src/context/FileContext.jsx @@ -0,0 +1,54 @@ +import {createContext, useContext, useState} from "react"; +import {v4 as uuidv4} from "uuid"; + +const FileContext = createContext(); + +export const FileProvider=({ children})=>{ + const [files, setFiles] = useState([ + {id: uuidv4(), name: "Untitled.md", content:"", blocks:[], isActive: true }, + ]); + +const addFile=(fileData= null)=> { + const newFile ={ + id: uuidv4(), + name: fileData?.name || `Untitled-${files.length+1}.md`, + content: fileData?.content || "", + blocks: fileData?.blocks || [], + isActive: true}; + setFiles((prev)=> + prev.map((f)=>({...f,isActive:false})).concat(newFile) +); +}; +const switchFile=(id)=> + setFiles((prev)=> + prev.map((f)=> ({...f, isActive: f.id === id})) +); +const updateFileContent=(id,newContent)=> + setFiles((prev)=> + prev.map((f)=>(f.id === id? {...f, content: newContent}:f)) +); +const updateFileBlocks=(id,newBlocks)=> + setFiles((prev) => + prev.map((f)=>(f.id===id? {...f,blocks: newBlocks}: f)) +); +const closeFile=(id)=> + setFiles((prev)=>{ + const remaining = prev.filter((f) => f.id!==id); + if(!remaining.length) return [{id: uuidv4(), name: "Untitled.md", content:"", blocks: [], isActive:true},]; + if(!remaining.some((f)=>f.isActive)) + remaining[remaining.length-1].isActive= true; + return remaining; + }); +return( + + {children} + +); +}; +export const useFiles=()=> useContext(FileContext); \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 3c9485e..e930782 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,11 +3,14 @@ import { ThemeProvider } from "@/components/ThemeProvider"; import App from "./App.jsx"; import { AuthProvider } from "./context/AuthContext"; import "./index.css"; +import { FileProvider } from "./context/FileContext.jsx"; createRoot(document.getElementById("root")).render( - + + + ); diff --git a/src/pages/Builder.jsx b/src/pages/Builder.jsx index ec56453..f2334c4 100644 --- a/src/pages/Builder.jsx +++ b/src/pages/Builder.jsx @@ -79,6 +79,8 @@ import { useAuth } from "@/context/AuthContext"; import { useBuilderTour } from "@/hooks/useBuilderTour"; import { blocksToMarkdown, exportToHTML, exportToMarkdown, exportToPDF } from "@/lib/exportUtils"; import { createMarkdown, getMarkdownById, updateMarkdown } from "@/lib/storage"; +import FileTabs from "@/components/ui/filetabs"; +import {useFiles} from "@/context/FileContext"; export default function Builder() { const { theme, setTheme } = useTheme(); @@ -87,10 +89,13 @@ export default function Builder() { const { id } = useParams(); const { startTour } = useBuilderTour(); const [activeTab, setActiveTab] = useState("editor"); - const [blocks, setBlocks] = useState(() => { - const saved = localStorage.getItem("markdown-blocks"); - return saved ? JSON.parse(saved) : []; - }); + // const [blocks, setBlocks] = useState(() => { + // const saved = localStorage.getItem("markdown-blocks"); + // return saved ? JSON.parse(saved) : []; + // }); + const {files, addFile, switchFile, updateFileBlocks} = useFiles(); + const activeFile= files.find((f)=> f.isActive); + const blocks = activeFile?.blocks || []; const [isImporting, setIsImporting] = useState(false); const [history, setHistory] = useState([[]]); const [historyIndex, setHistoryIndex] = useState(0); @@ -117,7 +122,7 @@ export default function Builder() { const document = await getMarkdownById(id); if (document && document.user_id === user.id) { const parsedBlocks = JSON.parse(document.content); - setBlocks(parsedBlocks); + updateFileBlocks(activeFile.id,parsedBlocks); setCurrentDocumentId(document.id); setSaveTitle(document.title); setLastSavedContent(document.content); @@ -217,7 +222,7 @@ export default function Builder() { ); const handleReset = () => { - setBlocks([]); + updateFileBlocks([]); saveToHistory([]); toast.success("Editor reset"); }; @@ -225,14 +230,14 @@ export default function Builder() { const handleUndo = useCallback(() => { if (historyIndex > 0) { setHistoryIndex(historyIndex - 1); - setBlocks(history[historyIndex - 1]); + updateFileBlocks(history[historyIndex - 1]); } }, [historyIndex, history]); const handleRedo = useCallback(() => { if (historyIndex < history.length - 1) { setHistoryIndex(historyIndex + 1); - setBlocks(history[historyIndex + 1]); + updateFileBlocks(history[historyIndex + 1]); } }, [historyIndex, history]); @@ -299,6 +304,22 @@ export default function Builder() { }; }, [handleUndo, handleRedo, handleSave]); + //prevent browser from opening dropped file in a new tab + useEffect(()=>{ + const preventDefaultFileOpen=(e)=>{ + if(e.dataTransfer?.types?.includes("Files")){ + e.preventDefault(); + } + }; + window.addEventListener("dragover", preventDefaultFileOpen); + window.addEventListener("drop",preventDefaultFileOpen); + + return()=>{ + window.removeEventListener("dragover", preventDefaultFileOpen); + window.removeEventListener("drop",preventDefaultFileOpen); + }; + },[]); + const toggleTheme = () => { setTheme(theme === "dark" ? "light" : "dark"); }; @@ -344,18 +365,30 @@ export default function Builder() { const input = document.createElement("input"); input.type = "file"; input.accept = ".md,.markdown,.txt"; + input.multiple= true; input.onchange = async (e) => { - const file = e.target.files?.[0]; - if (!file) return; + const files = Array.from(e.target.files || []); + if (!files.length) return; setIsImporting(true); try { + for(let i=0;i[...prev, ...newBlocks]); + } + toast.success(`${files.length} Markdown file(s) imported!`); + } catch(error) { + console.error(error); toast.error("Failed to import file"); } finally { setIsImporting(false); @@ -400,7 +433,7 @@ export default function Builder() { (document) => { try { const parsedBlocks = JSON.parse(document.content); - setBlocks(parsedBlocks); + updateFileBlocks(activeFile.id,parsedBlocks); setCurrentDocumentId(document.id); setSaveTitle(document.title); setLastSavedContent(document.content); @@ -531,6 +564,10 @@ export default function Builder() { return blocks; }; + useEffect(()=>{ + window.markdownToBlocks= markdownToBlocks; + },[]); + const handleDragStart = (event) => { setActiveId(event.active.id); }; @@ -552,7 +589,7 @@ export default function Builder() { if (oldIndex !== -1 && newIndex !== -1) { const newBlocks = arrayMove(blocks, oldIndex, newIndex); - setBlocks(newBlocks); + updateFileBlocks(activeFile.id,newBlocks); saveToHistory(newBlocks); } return; @@ -633,7 +670,7 @@ export default function Builder() { newBlocks = [...blocks.slice(0, overIndex + 1), newBlock, ...blocks.slice(overIndex + 1)]; } - setBlocks(newBlocks); + updateFileBlocks(activeFile.id,newBlocks); saveToHistory(newBlocks); } } @@ -645,18 +682,18 @@ export default function Builder() { const handleBlockUpdate = (blockId, updatedBlock) => { const newBlocks = blocks.map((b) => (b.id === blockId ? updatedBlock : b)); - setBlocks(newBlocks); + updateFileBlocks(activeFile.id,newBlocks); }; const handleBlockDelete = (blockId) => { const newBlocks = blocks.filter((b) => b.id !== blockId); - setBlocks(newBlocks); + updateFileBlocks(activeFile.id,newBlocks); saveToHistory(newBlocks); toast.success("Block deleted"); }; const handleBlocksChange = (newBlocks) => { - setBlocks(newBlocks); + updateFileBlocks(activeFile.id, newBlocks); saveToHistory(newBlocks); }; @@ -683,7 +720,7 @@ export default function Builder() { newBlocks = [...blocks, newBlock]; } - setBlocks(newBlocks); + updateFileBlocks(activeFile.id,newBlocks); saveToHistory(newBlocks); }; @@ -707,8 +744,15 @@ export default function Builder() { { + if (event?.active?.data?.current?.type === "native") return; + handleDragStart(event); + }} + + onDragOver={(event)=>{ + if (event?.over == null && event?.active?.data?.current === null) return; + handleDragOver(event); + }} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} > @@ -945,6 +989,7 @@ export default function Builder() { transition={{ duration: 0.5, delay: 0.2 }} >
+ f.isActive); + + const handleChange=(e)=>{ + if(activeFile) { + updateFileContent(activeFile.id, e.target.value); + } + }; + const handleFileDrop=async(e)=>{ + //const newBlocks= markdownToBlocks(text.replace(/\r\n/g,"\n")); + e.preventDefault(); + const droppedFiles=Array.from(e.dataTransfer.files||[]); + if(droppedFiles.length===0) return; + + for(const file of droppedFiles){ + const text= (await file.text()).replace(/\r\n/g,"\n"); + + //use global markdownToBlocks created in Builder + const newBlocks= window.markdownToBlocks(text); + addFile({ + name: file.name, + content: text, + blocks: newBlocks, + }); + } + }; + + return ( +
+ +
+ {activeFile ? ( +