From 6b306abae11a32403433fdc99ea0133ffad676e5 Mon Sep 17 00:00:00 2001 From: Swapnil Date: Tue, 10 Dec 2024 14:34:20 +0530 Subject: [PATCH] Refined changes for the editor folder --- packages/editor/package-lock.json | 102 +++ packages/editor/package.json | 5 +- .../src/app/editMode/[...slug]/page.tsx | 327 +-------- packages/editor/src/app/editMode/layout.tsx | 7 + packages/editor/src/app/editMode/page.tsx | 625 +----------------- packages/editor/src/app/globals.css | 8 + packages/editor/src/components/editor.tsx | 276 ++++++++ .../editor/src/components/file-explorer.tsx | 481 ++++++++++++++ packages/editor/src/components/layout.tsx | 7 + .../editor/src/components/ui/resizable.tsx | 45 ++ 10 files changed, 968 insertions(+), 915 deletions(-) create mode 100644 packages/editor/src/app/editMode/layout.tsx create mode 100644 packages/editor/src/components/editor.tsx create mode 100644 packages/editor/src/components/file-explorer.tsx create mode 100644 packages/editor/src/components/layout.tsx create mode 100644 packages/editor/src/components/ui/resizable.tsx diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index 4a12056..dd91d21 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -8,9 +8,11 @@ "name": "@akiradocs/editor", "version": "0.1.0", "dependencies": { + "@anthropic-ai/sdk": "^0.17.1", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", + "@google/generative-ai": "^0.2.1", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.1", @@ -36,6 +38,7 @@ "react": "^18.3.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.1.2", + "react-resizable-panels": "^2.1.7", "react-syntax-highlighter": "^15.6.1", "sonner": "^1.7.0", "tailwind-merge": "^2.5.4", @@ -64,6 +67,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.17.2.tgz", + "integrity": "sha512-xDfL/OblarYcwTSN2xBhynXJTkaTaxr8/v1fRKdT3grOZ4TrzIdrFfaTM771proR4g3uLe76PFSF3+gPjI6Gpw==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/@anthropic-ai/sdk/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/@babel/runtime": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", @@ -226,6 +266,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "node_modules/@google/generative-ai": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.2.1.tgz", + "integrity": "sha512-gNmMFadfwi7qf/6M9gImgyGJXY1jKQ/de8vGOqgJ0PPYgQ7WwzZDavbKrIuXS2zdqZZaYtxW3EFN6aG9x5wtFw==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2508,6 +2556,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2649,6 +2702,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2801,6 +2862,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2959,6 +3028,15 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4336,6 +4414,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-bun-module": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz", @@ -4879,6 +4962,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5752,6 +5845,15 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", + "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", diff --git a/packages/editor/package.json b/packages/editor/package.json index cee9f97..9ebb927 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -9,9 +9,11 @@ "lint": "next lint" }, "dependencies": { + "@anthropic-ai/sdk": "^0.17.1", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", + "@google/generative-ai": "^0.2.1", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.1", @@ -34,11 +36,10 @@ "next": "15.0.3", "next-themes": "^0.4.3", "openai": "^4.73.1", - "@anthropic-ai/sdk": "^0.17.1", - "@google/generative-ai": "^0.2.1", "react": "^18.3.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.1.2", + "react-resizable-panels": "^2.1.7", "react-syntax-highlighter": "^15.6.1", "sonner": "^1.7.0", "tailwind-merge": "^2.5.4", diff --git a/packages/editor/src/app/editMode/[...slug]/page.tsx b/packages/editor/src/app/editMode/[...slug]/page.tsx index 116dbff..90dbf39 100644 --- a/packages/editor/src/app/editMode/[...slug]/page.tsx +++ b/packages/editor/src/app/editMode/[...slug]/page.tsx @@ -1,326 +1,5 @@ -'use client' +import { redirect } from "next/navigation"; -import React, { useState, useEffect } from 'react' -import { Button } from "@/components/ui/button" -import { toast } from "sonner" -import { BlockType } from '@/types/Block' -import { Plus } from 'lucide-react' -import { ArticleHeaders } from '@/components/blocks/ArticleHeaders' -import { TitleBar } from '@/components/layout/TitleBar' -import { DndContext, DragEndEvent, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core' -import { SortableContext, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable' -import { SortableBlock } from '@/components/blocks/SortableBlock' -import { deleteImageFromPublic, moveImageToRoot } from '@/lib/fileUtils' - -type Block = { - id: string - type: BlockType - content: string - metadata?: Record +export default function SlugPage({ params }: { params: { slug: string[] } }) { + redirect("/editmode"); } - -export default function ArticleEditorContent({ params }: { params: Promise<{ slug: string[] }> }) { - const resolvedParams = React.use(params) - const filePath = resolvedParams.slug?.length ? resolvedParams.slug.join('/') : '' - const [blocks, setBlocks] = useState([]) - const [title, setTitle] = useState('') - const [subtitle, setSubtitle] = useState('') - const [showPreview, setShowPreview] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [isSaving, setIsSaving] = useState(false) - const [activeChangeTypeId, setActiveChangeTypeId] = useState(null) - const [deletedImages, setDeletedImages] = useState([]) - - useEffect(() => { - const loadFileContent = async () => { - if (!filePath) { - setBlocks([{ id: '1', type: 'paragraph', content: '', metadata: {} }]) - setIsLoading(false) - return - } - - try { - const response = await fetch(`/api/files?path=${encodeURIComponent(filePath)}`) - if (!response.ok) throw new Error('Failed to load file') - const data = await response.json() - setTitle(data.title || '') - setSubtitle(data.description || '') - setBlocks(data.blocks || [{ id: '1', type: 'paragraph', content: '', metadata: {} }]) - } catch (error) { - console.error('Error loading file:', error) - setBlocks([{ id: '1', type: 'paragraph', content: '', metadata: {} }]) - } finally { - setIsLoading(false) - } - } - - loadFileContent() - }, [filePath]) - - const handleSave = async () => { - if (!filePath) { - console.error('No file path specified') - return - } - - setIsSaving(true) - try { - // Delete files that were explicitly marked for deletion - await Promise.all( - deletedImages.map(filename => - fetch(`/api/upload/delete-from-root?filename=${encodeURIComponent(filename)}`, { - method: 'DELETE' - }) - ) - ) - - // Get all current media files from blocks - const currentFiles = blocks - .filter(block => block.type === 'image' || block.type === 'video' || block.type === 'audio' || block.type === 'file') - .map(block => { - try { - const content = JSON.parse(block.content) - return content.url.split('/').pop() - } catch { - return block.content.split('/').pop() - } - }) - .filter(Boolean) - - // Fetch the current article to compare files - const response = await fetch(`/api/files?path=${encodeURIComponent(filePath)}`) - if (response.ok) { - const data = await response.json() - const oldFiles = data.blocks - ?.filter((block: Block) => block.type === 'image' || block.type === 'video' || block.type === 'audio' || block.type === 'file') - .map((block: Block) => { - try { - const content = JSON.parse(block.content) - return content.url.split('/').pop() - } catch { - return block.content.split('/').pop() - } - }) - .filter(Boolean) || [] - - // Delete files that are no longer used - const filesToDelete = oldFiles.filter((file: string) => !currentFiles.includes(file)) - await Promise.all( - filesToDelete.map((filename: string) => - fetch(`/api/upload/delete-from-root?filename=${encodeURIComponent(filename)}`, { - method: 'DELETE' - }) - ) - ) - } - - // Move current files to root - await Promise.all( - currentFiles.map(filename => - fetch(`/api/upload/move-to-root?filename=${encodeURIComponent(filename)}`, { - method: 'POST' - }) - ) - ) - - const content = { - title, - description: subtitle, - author: "Anonymous", - date: new Date().toISOString().split('T')[0], - blocks - } - - const saveResponse = await fetch('/api/files', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: filePath, content }) - }) - - if (!saveResponse.ok) throw new Error('Failed to save file') - - setDeletedImages([]) - toast.success('Changes saved successfully') - } catch (error) { - console.error('Error saving file:', error) - toast.error('Failed to save changes') - } finally { - setIsSaving(false) - } - } - - const addBlock = (afterId: string) => { - const newBlock: Block = { - id: Date.now().toString(), - type: 'paragraph', - content: '', - metadata: {} - } - - if (newBlock.type === 'table') { - newBlock.metadata = { - headers: ['Column 1', 'Column 2'], - rows: [['', '']] - } - } - - if (newBlock.type === 'checkList') { - newBlock.content = JSON.stringify([{ text: '', checked: false }]); - } - - if (afterId === 'new') { - setBlocks([newBlock]) - } else { - const index = blocks.findIndex(block => block.id === afterId) - setBlocks([...blocks.slice(0, index + 1), newBlock, ...blocks.slice(index + 1)]) - } - setActiveChangeTypeId(newBlock.id) - } - - const updateBlock = (id: string, content: string) => { - setBlocks(blocks.map(block => { - if (block.id === id) { - if (block.type === 'table') { - try { - const { headers, rows } = JSON.parse(content); - return { ...block, metadata: { ...block.metadata, headers, rows } }; - } catch { - return block; - } - } - return { ...block, content } - } - return block - })) - } - - const changeBlockType = (id: string, newType: BlockType) => { - setBlocks(blocks.map(block => block.id === id ? { ...block, type: newType } : block)) - setActiveChangeTypeId(null) - } - - const deleteBlock = async (id: string) => { - const block = blocks.find(b => b.id === id) - if (block?.type === 'image' || block?.type === 'video' || block?.type === 'audio' || block?.type === 'file') { - try { - const content = JSON.parse(block.content) - const filename = content.url.split('/').pop() - if (filename) { - await deleteImageFromPublic(filename) - setDeletedImages(prev => [...prev, filename]) - } - } catch (error) { - console.error('Error deleting file:', error) - } - } - setBlocks(blocks.filter(block => block.id !== id)) - } - - const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - distance: 8, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 200, - tolerance: 8, - }, - }) - ) - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event - - if (over && active.id !== over.id) { - setBlocks((blocks) => { - const oldIndex = blocks.findIndex((block) => block.id === active.id) - const newIndex = blocks.findIndex((block) => block.id === over.id) - - return arrayMove(blocks, oldIndex, newIndex) - }) - } - } - - const updateBlockMetadata = (id: string, metadata: any) => { - setBlocks(blocks.map(block => { - if (block.id === id) { - return { ...block, metadata: metadata } - } - return block - })) - } - - if (isLoading) { - return ( -
-
Loading...
-
- ) - } - - return ( - -
- -
- -
- -
- - - {blocks.map((block) => ( - - ))} - - - {blocks.length === 0 && !showPreview && ( -
- -
- )} -
-
-
-
- ) -} - -// export default function ArticleEditor() { -// return ( -// -//
Loading...
-// }> -// -//
-// ) -// } \ No newline at end of file diff --git a/packages/editor/src/app/editMode/layout.tsx b/packages/editor/src/app/editMode/layout.tsx new file mode 100644 index 0000000..f3ca610 --- /dev/null +++ b/packages/editor/src/app/editMode/layout.tsx @@ -0,0 +1,7 @@ +export default function EditorLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/packages/editor/src/app/editMode/page.tsx b/packages/editor/src/app/editMode/page.tsx index 439ffaa..99f4b7d 100644 --- a/packages/editor/src/app/editMode/page.tsx +++ b/packages/editor/src/app/editMode/page.tsx @@ -1,592 +1,39 @@ -'use client' - -import { useState, KeyboardEvent, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Folder, File, Plus, X, ChevronRight, ChevronDown, Trash2 } from "lucide-react" -import { motion, AnimatePresence } from "framer-motion" -import { fetchAllContent } from '@/src/lib/getContents' -import { ThemeToggle } from "@/components/ui/ThemeToggle" - -const API_URL = process.env.NEXT_PUBLIC_BACKEND_API_URL || 'http://localhost:3001' - -type FileNode = { - id: string - name: string - type: 'file' | 'folder' - children?: FileNode[] -} - -// Add this function to track the full path of each node -const getNodeFullPath = (tree: FileNode[], nodeId: string, parentPath: string = ''): string | null => { - for (const node of tree) { - const currentPath = parentPath ? `${parentPath}/${node.name}` : node.name - if (node.id === nodeId) { - return currentPath - } - if (node.children) { - const foundPath = getNodeFullPath(node.children, nodeId, currentPath) - if (foundPath) return foundPath - } - } - return null -} - -interface MetaItem { - title: string - path?: string - items?: Record -} - -interface FolderMeta { - [key: string]: MetaItem -} - -interface RootMeta { - defaultRoute?: string - [key: string]: MetaItem | string | undefined -} - -export default function ImprovedFileTreeUI() { - const [fileTree, setFileTree] = useState([]) - const router = useRouter() - const isDevPage = process.env.NEXT_PUBLIC_AKIRADOCS_EDIT_MODE === 'true' - - // Get this from the backend - useEffect(() => { - const content = fetchAllContent() - const transformedTree = transformContentToFileTree(content) - setFileTree(transformedTree) - }, []) - - const [expandedFolders, setExpandedFolders] = useState>(new Set(['1', '4'])) - const [newItemParent, setNewItemParent] = useState(null) - const [newItemType, setNewItemType] = useState<'file' | 'folder' | null>(null) - const [newItemName, setNewItemName] = useState('') - - const toggleFolder = (folderId: string) => { - setExpandedFolders(prev => { - const newSet = new Set(prev) - if (newSet.has(folderId)) { - newSet.delete(folderId) - } else { - newSet.add(folderId) - } - return newSet - }) - } - - const handleFileClick = (node: FileNode) => { - // Get the full path for the file - const fullPath = getNodeFullPath(fileTree, node.id) - if (!fullPath) { - console.error('Could not find full path for node') - return - } - - // Encode the file path to handle special characters in URLs - const encodedPath = encodeURIComponent(fullPath) - router.push(`/editMode/${fullPath}`) - } - - const startNewItem = (parentId: string, type: 'file' | 'folder') => { - setNewItemParent(parentId) - setNewItemType(type) - setNewItemName('') - } - - const cancelNewItem = () => { - setNewItemParent(null) - setNewItemType(null) - setNewItemName('') - } - - const addNewItem = async () => { - if (!newItemParent || !newItemType || !newItemName) return - - const newItem: FileNode = { - id: Date.now().toString(), - name: newItemName, - type: newItemType, - children: newItemType === 'folder' ? [] : undefined - } - - const parentPath = getNodeFullPath(fileTree, newItemParent) - if (!parentPath) { - console.error('Could not find parent path') - return - } - - const fullPath = `${parentPath}/${newItemName}` - - if (newItemType === 'file') { - try { - // Get the language and section from the path - const pathParts = parentPath.split('/') - const language = pathParts[0] // e.g. 'en' - const section = pathParts[1] // e.g. 'docs' - - // Create default content for the new file - const fileId = newItemName.replace('.json', '') - const defaultContent = { - title: fileId.split('_') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '), - description: "", - author: "Anonymous", - publishDate: new Date().toISOString(), - modifiedDate: new Date().toISOString(), - blocks: [] - } - - // Create the new file - const fileResponse = await fetch(`${API_URL}/api/files`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: fullPath, - content: defaultContent - }) - }) - - if (!fileResponse.ok) throw new Error('Failed to create file') - - // Update folder level _meta.json - const folderMetaPath = `${parentPath}/_meta.json` - let folderMeta: FolderMeta = {} - - try { - const folderMetaResponse = await fetch(`${API_URL}/api/files?path=${encodeURIComponent(folderMetaPath)}`) - if (folderMetaResponse.ok) { - folderMeta = await folderMetaResponse.json() - } - } catch (error) { - console.log('No existing folder meta found') - } - - // Add the new file to folder meta - folderMeta[fileId] = { - title: defaultContent.title, - path: `/${section}/${pathParts.slice(2).join('/')}/${fileId}` - } - - // Save folder meta - await fetch(`${API_URL}/api/files`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: folderMetaPath, - content: folderMeta - }) - }) - - // Update root level _meta.json - const rootMetaPath = `${language}/${section}/_meta.json` - const rootMetaResponse = await fetch(`${API_URL}/api/files?path=${encodeURIComponent(rootMetaPath)}`) - - if (rootMetaResponse.ok) { - const rootMeta = await rootMetaResponse.json() as RootMeta - - // Navigate through the path to find the right section - let currentSection = rootMeta - for (let i = 2; i < pathParts.length; i++) { - const part = pathParts[i] - - // Convert path to camelCase for section key - const sectionKey = part.replace(/-/g, ' ') - .split(' ') - .map((word, index) => { - const capitalized = word.charAt(0).toUpperCase() + word.slice(1) - return index === 0 ? capitalized.toLowerCase() : capitalized - }) - .join('') - - // Create section if it doesn't exist - if (!currentSection[sectionKey]) { - const newSection: MetaItem = { - title: part.split('-') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '), - items: {} - } - currentSection[sectionKey] = newSection - } - - // Move to items object for next iteration - const section = currentSection[sectionKey] as MetaItem - const items = section?.items - if (!items) { - currentSection[sectionKey] = { - ...section, - items: {} - } - currentSection = currentSection[sectionKey].items! - } else { - currentSection = items - } - } - - // Add the new file entry - currentSection[fileId] = { - title: defaultContent.title, - path: `/${section}/${pathParts.slice(2).join('/')}/${fileId}` - } - - // Save updated root meta - await fetch(`${API_URL}/api/files`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: rootMetaPath, - content: rootMeta - }) - }) - } - - } catch (error) { - console.error('Error creating file or updating metadata:', error) - return - } - } - - // Update the file tree UI - const updatedTree = addItemToTree(fileTree, newItemParent, newItem) - setFileTree(updatedTree) - - if (newItemType === 'folder') { - setExpandedFolders(prev => new Set(prev).add(newItem.id)) - } - - cancelNewItem() - } - - const addItemToTree = (tree: FileNode[], parentId: string, newItem: FileNode): FileNode[] => { - return tree.map(node => { - if (node.id === parentId) { - return { ...node, children: [...(node.children || []), newItem] } - } - if (node.children) { - return { ...node, children: addItemToTree(node.children, parentId, newItem) } - } - return node - }) - } - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - addNewItem() - } - } - - const renderFileTree = (nodes: FileNode[], level: number = 0) => { - return ( -
    0 ? 'border-l border-border ml-4 pl-4' : ''}`}> - {nodes.map((node) => ( -
  • -
    -
    - {node.type === 'folder' && ( - - )} -
    - {node.type === 'folder' ? ( - - ) : ( - - )} -
    - node.type === 'file' ? handleFileClick(node) : toggleFolder(node.id)} - > - {node.name} - -
    -
    - {node.type === 'folder' && ( - <> - - - - )} - -
    -
    - {node.type === 'folder' && node.children && ( - - {expandedFolders.has(node.id) && ( - -
    - {renderFileTree(node.children, level + 1)} -
    -
    - )} -
    - )} - {newItemParent === node.id && ( -
    - setNewItemName(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={`New ${newItemType}`} - className="h-8 text-sm bg-background text-foreground flex-grow" - /> - - -
    - )} -
  • - ))} -
- ) - } - - const transformContentToFileTree = (content: { [key: string]: any }): FileNode[] => { - const tree: { [key: string]: FileNode } = {} - let rootNodes: FileNode[] = [] - - // Create nodes for each path - Object.keys(content).forEach(path => { - const parts = path.split('/') - let currentPath = '' - - parts.forEach((part, index) => { - const isFile = index === parts.length - 1 - const fullPath = currentPath ? `${currentPath}/${part}` : part - const nodeId = fullPath.replace(/[/.]/g, '_') - - if (!tree[fullPath]) { - tree[fullPath] = { - id: nodeId, - name: part, - type: isFile ? 'file' : 'folder', - children: isFile ? undefined : [] - } - } - - if (index === 0) { - if (!rootNodes.find(node => node.id === nodeId)) { - rootNodes.push(tree[fullPath]) - } - } else { - const parentPath = currentPath - const parent = tree[parentPath] - if (parent && parent.children && !parent.children.find(child => child.id === nodeId)) { - parent.children.push(tree[fullPath]) - } - } - - currentPath = fullPath - }) - }) - - return rootNodes - } - - const deleteItem = async (nodeId: string, nodeName: string, nodeType: 'file' | 'folder') => { - try { - // Get the full path of the node - const fullPath = getNodeFullPath(fileTree, nodeId) - if (!fullPath) { - console.error('Could not find full path for node') - return - } - - // Confirmation dialog - const confirmMessage = `Are you sure you want to delete this ${nodeType}${nodeType === 'folder' ? ' and all its contents' : ''}?` - if (!confirm(confirmMessage)) { - return - } - - const pathParts = fullPath.split('/') - const language = pathParts[0] // e.g. 'en' - const section = pathParts[1] // e.g. 'docs' - const fileId = nodeName.replace('.json', '') - - // Delete the file/folder first - const response = await fetch(`${API_URL}/api/files`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: fullPath, - type: nodeType - }) - }) - - if (!response.ok) { - throw new Error('Failed to delete item') - } - - if (nodeType === 'file') { - // Update folder level _meta.json - const folderMetaPath = `${pathParts.slice(0, -1).join('/')}/_meta.json` - try { - const folderMetaResponse = await fetch(`${API_URL}/api/files?path=${encodeURIComponent(folderMetaPath)}`) - if (folderMetaResponse.ok) { - const folderMeta: FolderMeta = await folderMetaResponse.json() - - // Remove the file from folder meta - delete folderMeta[fileId] - - // Save updated folder meta - await fetch(`${API_URL}/api/files`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: folderMetaPath, - content: folderMeta - }) - }) - } - } catch (error) { - console.log('No folder meta found or error updating it:', error) - } - - // Update root level _meta.json - const rootMetaPath = `${language}/${section}/_meta.json` - const rootMetaResponse = await fetch(`${API_URL}/api/files?path=${encodeURIComponent(rootMetaPath)}`) - - if (rootMetaResponse.ok) { - const rootMeta = await rootMetaResponse.json() as RootMeta - - let currentSection = rootMeta - let parent = null - - for (let i = 2; i < pathParts.length - 1; i++) { - const part = pathParts[i] - - const sectionKey = part.replace(/-/g, ' ') - .split(' ') - .map((word, index) => { - const capitalized = word.charAt(0).toUpperCase() + word.slice(1) - return index === 0 ? capitalized.toLowerCase() : capitalized - }) - .join('') - - const section = currentSection[sectionKey] as MetaItem - if (!section || !section.items) { - console.error('Section not found in root meta:', sectionKey) - return - } - - parent = currentSection - currentSection = section.items - } - - // Remove the file entry - delete currentSection[fileId] - - // If section is empty and not root level, remove the section - if (parent && Object.keys(currentSection).length === 0) { - const lastPart = pathParts[pathParts.length - 2] - const lastSectionKey = lastPart.replace(/-/g, ' ') - .split(' ') - .map((word, index) => { - const capitalized = word.charAt(0).toUpperCase() + word.slice(1) - return index === 0 ? capitalized.toLowerCase() : capitalized - }) - .join('') - delete parent[lastSectionKey] - } - - // Save updated root meta - await fetch(`${API_URL}/api/files`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: rootMetaPath, - content: rootMeta - }) - }) - } - } - - // Update the file tree UI - const updatedTree = deleteItemFromTree(fileTree, nodeId) - setFileTree(updatedTree) - - } catch (error) { - console.error('Error deleting item:', error) - } - } - - const deleteItemFromTree = (tree: FileNode[], nodeId: string): FileNode[] => { - return tree.filter(node => { - if (node.id === nodeId) { - return false; - } - if (node.children) { - node.children = deleteItemFromTree(node.children, nodeId); - } - return true; - }); - } - - // if (!isDevPage) { - // router.push('/docs') - // return null - // } +"use client"; + +import { useState } from "react"; +import dynamic from "next/dynamic"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +const FileExplorer = dynamic(() => import("@/components/file-explorer"), { + ssr: false, +}); +const Editor = dynamic(() => import("@/components/editor"), { ssr: false }); + +export default function EditModePage() { + const [selectedFile, setSelectedFile] = useState(null); return ( -
-
-

- Project Explorer -

- -
-
- - {renderFileTree(fileTree)} - -
-
- ) + + +
+ +
+
+ + +
+ {selectedFile ? ( + + ) : ( +
+ Select a file to edit +
+ )} +
+
+
+ ); } diff --git a/packages/editor/src/app/globals.css b/packages/editor/src/app/globals.css index a23ac26..44cacb6 100644 --- a/packages/editor/src/app/globals.css +++ b/packages/editor/src/app/globals.css @@ -70,3 +70,11 @@ body { @apply bg-background text-foreground; } } +.custom-scrollbar::-webkit-scrollbar { + width: 10px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 5px; +} diff --git a/packages/editor/src/components/editor.tsx b/packages/editor/src/components/editor.tsx new file mode 100644 index 0000000..8f47978 --- /dev/null +++ b/packages/editor/src/components/editor.tsx @@ -0,0 +1,276 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; +import { BlockType } from '@/types/Block'; +import { Plus } from 'lucide-react'; +import { ArticleHeaders } from '@/components/blocks/ArticleHeaders'; +import { TitleBar } from '@/components/layout/TitleBar'; +import { + DndContext, + DragEndEvent, + MouseSensor, + TouchSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + SortableContext, + arrayMove, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { SortableBlock } from '@/components/blocks/SortableBlock'; +import { SEO } from '@/components/layout/SEO'; + +type Block = { + id: string; + type: BlockType; + content: string; + metadata?: Record; +}; + +interface EditorProps { + filePath: string; +} + +export function Editor({ filePath }: EditorProps) { + const [blocks, setBlocks] = useState([]); + const [title, setTitle] = useState(''); + const [subtitle, setSubtitle] = useState(''); + const [showPreview, setShowPreview] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [activeChangeTypeId, setActiveChangeTypeId] = useState( + null + ); + + useEffect(() => { + const loadFileContent = async () => { + if (!filePath) { + setBlocks([{ id: '1', type: 'paragraph', content: '', metadata: {} }]); + setIsLoading(false); + return; + } + + try { + const response = await fetch( + `/api/files?path=${encodeURIComponent(filePath)}` + ); + if (!response.ok) throw new Error('Failed to load file'); + const data = await response.json(); + setTitle(data.title || ''); + setSubtitle(data.description || ''); + setBlocks( + data.blocks || [ + { id: '1', type: 'paragraph', content: '', metadata: {} }, + ] + ); + } catch (error) { + console.error('Error loading file:', error); + setBlocks([{ id: '1', type: 'paragraph', content: '', metadata: {} }]); + } finally { + setIsLoading(false); + } + }; + + loadFileContent(); + }, [filePath]); + + const handleSave = async () => { + if (!filePath) { + console.error('No file path specified'); + return; + } + + setIsSaving(true); + try { + const content = { + title, + description: subtitle, + author: 'Anonymous', // You might want to make this dynamic + date: new Date().toISOString().split('T')[0], + blocks, + }; + + const response = await fetch('/api/files', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: filePath, content }), + }); + + if (!response.ok) throw new Error('Failed to save file'); + toast.success('Changes saved successfully'); + } catch (error) { + console.error('Error saving file:', error); + toast.error('Failed to save changes'); + } finally { + setIsSaving(false); + } + }; + + const addBlock = (afterId: string) => { + const newBlock: Block = { + id: Date.now().toString(), + type: 'paragraph', + content: '', + metadata: {}, + }; + + if (newBlock.type === 'list') { + newBlock.content = '[]'; + } + + if (afterId === 'new') { + setBlocks([newBlock]); + } else { + const index = blocks.findIndex((block) => block.id === afterId); + setBlocks([ + ...blocks.slice(0, index + 1), + newBlock, + ...blocks.slice(index + 1), + ]); + } + setActiveChangeTypeId(newBlock.id); + }; + + const updateBlock = (id: string, content: string) => { + setBlocks( + blocks.map((block) => { + if (block.id === id) { + if (block.type === 'list') { + try { + const parsed = JSON.parse(content); + return { + ...block, + content: JSON.stringify( + Array.isArray(parsed) ? parsed : [parsed] + ), + }; + } catch { + return { ...block, content: JSON.stringify([content]) }; + } + } + return { ...block, content }; + } + return block; + }) + ); + }; + + const changeBlockType = (id: string, newType: BlockType) => { + setBlocks( + blocks.map((block) => + block.id === id ? { ...block, type: newType } : block + ) + ); + setActiveChangeTypeId(null); + }; + + const deleteBlock = (id: string) => { + setBlocks(blocks.filter((block) => block.id !== id)); + }; + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 200, + tolerance: 8, + }, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setBlocks((blocks) => { + const oldIndex = blocks.findIndex((block) => block.id === active.id); + const newIndex = blocks.findIndex((block) => block.id === over.id); + + return arrayMove(blocks, oldIndex, newIndex); + }); + } + }; + + const updateBlockMetadata = (id: string, metadata: any) => { + setBlocks( + blocks.map((block) => { + if (block.id === id) { + return { ...block, metadata: metadata }; + } + return block; + }) + ); + }; + + if (isLoading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+ + +
+ + + + {blocks.map((block) => ( + + ))} + + + {blocks.length === 0 && !showPreview && ( +
+ +
+ )} +
+
+
+ ); +} + +export default Editor; diff --git a/packages/editor/src/components/file-explorer.tsx b/packages/editor/src/components/file-explorer.tsx new file mode 100644 index 0000000..d778c9d --- /dev/null +++ b/packages/editor/src/components/file-explorer.tsx @@ -0,0 +1,481 @@ +'use client'; + +import { useState, KeyboardEvent, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Folder, + File, + Plus, + X, + ChevronRight, + ChevronDown, + Trash2, +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { fetchAllContent } from '@/lib/getContents'; + +type FileNode = { + id: string; + name: string; + type: 'file' | 'folder'; + children?: FileNode[]; +}; + +interface FileExplorerProps { + onFileSelect: (path: string) => void; +} + +const API_URL = + process.env.NEXT_PUBLIC_BACKEND_API_URL || 'http://localhost:8000'; + +// Add this function to track the full path of each node +const getNodeFullPath = ( + tree: FileNode[], + nodeId: string, + parentPath: string = '' +): string | null => { + for (const node of tree) { + const currentPath = parentPath ? `${parentPath}/${node.name}` : node.name; + if (node.id === nodeId) { + return currentPath; + } + if (node.children) { + const foundPath = getNodeFullPath(node.children, nodeId, currentPath); + if (foundPath) return foundPath; + } + } + return null; +}; + +export default function FileExplorer({ onFileSelect }: FileExplorerProps) { + const [fileTree, setFileTree] = useState([]); + const [expandedFolders, setExpandedFolders] = useState>( + new Set(['1', '4']) + ); + const [newItemParent, setNewItemParent] = useState(null); + const [newItemType, setNewItemType] = useState<'file' | 'folder' | null>( + null + ); + const [newItemName, setNewItemName] = useState(''); + + useEffect(() => { + const content = fetchAllContent(); + const transformedTree = transformContentToFileTree(content); + setFileTree(transformedTree); + }, []); + + const toggleFolder = (folderId: string) => { + setExpandedFolders((prev) => { + const newSet = new Set(prev); + if (newSet.has(folderId)) { + newSet.delete(folderId); + } else { + newSet.add(folderId); + } + return newSet; + }); + }; + + const handleFileClick = (node: FileNode) => { + const fullPath = getNodeFullPath(fileTree, node.id); + if (!fullPath) { + console.error('Could not find full path for node'); + return; + } + onFileSelect(fullPath); + }; + + const startNewItem = (parentId: string, type: 'file' | 'folder') => { + setNewItemParent(parentId); + setNewItemType(type); + setNewItemName(''); + }; + + const cancelNewItem = () => { + setNewItemParent(null); + setNewItemType(null); + setNewItemName(''); + }; + + const addNewItem = async () => { + if (!newItemParent || !newItemType || !newItemName) return; + + const newItem: FileNode = { + id: Date.now().toString(), + name: newItemName, + type: newItemType, + children: newItemType === 'folder' ? [] : undefined, + }; + + const parentPath = getNodeFullPath(fileTree, newItemParent); + if (!parentPath) { + console.error('Could not find parent path'); + return; + } + + const fullPath = `${parentPath}/${newItemName}`; + + if (newItemType === 'file') { + try { + // First, read the existing metadata + const metaPath = `${parentPath}/_meta.json`; + const metaResponse = await fetch( + `${API_URL}/api/files?path=${encodeURIComponent(metaPath)}`, + { + method: 'GET', + } + ); + + if (!metaResponse.ok) { + throw new Error('Failed to read metadata'); + } + + const existingMeta = await metaResponse.json(); + const newFileId = newItemName.replace('.json', ''); + + // Update the metadata with the new file entry + const updatedMeta = { + ...existingMeta, + [newFileId]: { + title: newFileId, + path: `/articles/${newFileId}`, + }, + }; + + // Save the updated metadata + const updateMetaResponse = await fetch(`${API_URL}/api/files`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + path: metaPath, + content: updatedMeta, + }), + }); + + if (!updateMetaResponse.ok) { + throw new Error('Failed to update metadata'); + } + + // Create the new file with default content + const defaultContent = { + id: newFileId, + title: 'New Article', + description: 'Add your description here', + author: 'Anonymous', + date: new Date().toISOString().split('T')[0], + blocks: [], + }; + + // Create the new file + const fileResponse = await fetch(`${API_URL}/api/files`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + path: fullPath, + content: defaultContent, + }), + }); + + if (!fileResponse.ok) { + throw new Error('Failed to create file'); + } + } catch (error) { + console.error('Error creating file or updating metadata:', error); + return; + } + } + + const updatedTree = addItemToTree(fileTree, newItemParent, newItem); + setFileTree(updatedTree); + + if (newItemType === 'folder') { + setExpandedFolders((prev) => new Set(prev).add(newItem.id)); + } + + cancelNewItem(); + }; + + const addItemToTree = ( + tree: FileNode[], + parentId: string, + newItem: FileNode + ): FileNode[] => { + return tree.map((node) => { + if (node.id === parentId) { + return { ...node, children: [...(node.children || []), newItem] }; + } + if (node.children) { + return { + ...node, + children: addItemToTree(node.children, parentId, newItem), + }; + } + return node; + }); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + addNewItem(); + } + }; + + const renderFileTree = (nodes: FileNode[], level: number = 0) => { + return ( +
    0 ? 'border-l border-border ml-4 pl-4' : '' + }`} + > + {nodes.map((node) => ( +
  • +
    +
    + {node.type === 'folder' && ( + + )} +
    + {node.type === 'folder' ? ( + + ) : ( + + )} +
    + + node.type === 'file' + ? handleFileClick(node) + : toggleFolder(node.id) + } + > + {node.name} + +
    +
    + {node.type === 'folder' && ( + <> + + + + )} + +
    +
    + {node.type === 'folder' && node.children && ( + + {expandedFolders.has(node.id) && ( + +
    + {renderFileTree(node.children, level + 1)} +
    +
    + )} +
    + )} + {newItemParent === node.id && ( +
    + setNewItemName(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={`New ${newItemType}`} + className="h-8 text-sm bg-background text-foreground flex-grow" + /> + + +
    + )} +
  • + ))} +
+ ); + }; + + const transformContentToFileTree = (content: { + [key: string]: any; + }): FileNode[] => { + const tree: { [key: string]: FileNode } = {}; + let rootNodes: FileNode[] = []; + + // Create nodes for each path + Object.keys(content).forEach((path) => { + const parts = path.split('/'); + let currentPath = ''; + + parts.forEach((part, index) => { + const isFile = index === parts.length - 1; + const fullPath = currentPath ? `${currentPath}/${part}` : part; + const nodeId = fullPath.replace(/[/.]/g, '_'); + + if (!tree[fullPath]) { + tree[fullPath] = { + id: nodeId, + name: part, + type: isFile ? 'file' : 'folder', + children: isFile ? undefined : [], + }; + } + + if (index === 0) { + if (!rootNodes.find((node) => node.id === nodeId)) { + rootNodes.push(tree[fullPath]); + } + } else { + const parentPath = currentPath; + const parent = tree[parentPath]; + if ( + parent && + parent.children && + !parent.children.find((child) => child.id === nodeId) + ) { + parent.children.push(tree[fullPath]); + } + } + + currentPath = fullPath; + }); + }); + + return rootNodes; + }; + + const deleteItem = async ( + nodeId: string, + nodeName: string, + nodeType: 'file' | 'folder' + ) => { + // Add confirmation dialog + const confirmMessage = `Are you sure you want to delete this ${nodeType}${ + nodeType === 'folder' ? ' and all its contents' : '' + }?`; + if (!confirm(confirmMessage)) { + return; + } + + // Get the full path of the node + const fullPath = getNodeFullPath(fileTree, nodeId); + if (!fullPath) { + console.error('Could not find full path for node'); + return; + } + + try { + const response = await fetch('/api/files', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + path: fullPath, // Use the full path instead of just the node name + type: nodeType, + }), + }); + + if (!response.ok) { + throw new Error('Failed to delete item'); + } + + // Update the file tree by filtering out the deleted item + const updatedTree = deleteItemFromTree(fileTree, nodeId); + setFileTree(updatedTree); + } catch (error) { + console.error('Error deleting item:', error); + } + }; + + const deleteItemFromTree = (tree: FileNode[], nodeId: string): FileNode[] => { + return tree.filter((node) => { + if (node.id === nodeId) { + return false; + } + if (node.children) { + node.children = deleteItemFromTree(node.children, nodeId); + } + return true; + }); + }; + + // if (!isDevPage) { + // router.push('/docs') + // return null + // } + + return ( +
+

+ Project Explorer +

+
+ + {renderFileTree(fileTree)} + +
+
+ ); +} diff --git a/packages/editor/src/components/layout.tsx b/packages/editor/src/components/layout.tsx new file mode 100644 index 0000000..f3ca610 --- /dev/null +++ b/packages/editor/src/components/layout.tsx @@ -0,0 +1,7 @@ +export default function EditorLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/packages/editor/src/components/ui/resizable.tsx b/packages/editor/src/components/ui/resizable.tsx new file mode 100644 index 0000000..f4bc558 --- /dev/null +++ b/packages/editor/src/components/ui/resizable.tsx @@ -0,0 +1,45 @@ +"use client" + +import { GripVertical } from "lucide-react" +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "@/lib/utils" + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }