diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index 2d2c250..d3ca386 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.3", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", @@ -38,6 +39,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", @@ -70,7 +72,6 @@ "version": "0.17.2", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.17.2.tgz", "integrity": "sha512-xDfL/OblarYcwTSN2xBhynXJTkaTaxr8/v1fRKdT3grOZ4TrzIdrFfaTM771proR4g3uLe76PFSF3+gPjI6Gpw==", - "license": "MIT", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -87,7 +88,6 @@ "version": "18.19.67", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", - "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -95,14 +95,12 @@ "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==", - "license": "MIT" + "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==", - "license": "MIT", "engines": { "node": ">= 8" } @@ -273,7 +271,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.2.1.tgz", "integrity": "sha512-gNmMFadfwi7qf/6M9gImgyGJXY1jKQ/de8vGOqgJ0PPYgQ7WwzZDavbKrIuXS2zdqZZaYtxW3EFN6aG9x5wtFw==", - "license": "Apache-2.0", "engines": { "node": ">=18.0.0" } @@ -1110,6 +1107,334 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.3.tgz", + "integrity": "sha512-i4ZjZNoiAKwxcaKBR5XdiOyEJQdBT4P6TeMtzP4fjlcDJpxwIcmmWkdd13YEzCHHcWYZOyl7fVHKT8dFMHdo3w==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-arrow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz", + "integrity": "sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-menu": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.3.tgz", + "integrity": "sha512-wY5SY6yCiJYP+DMIy7RrjF4shoFpB9LJltliVwejBm8T2yepWDJgKBhIFYOGWYR/lFHOCtbstN9duZFu6gmveQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-popper": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", @@ -2710,7 +3035,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", "engines": { "node": "*" } @@ -3038,7 +3362,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", - "license": "ISC", "dependencies": { "base-64": "^0.1.0", "md5": "^2.3.0" @@ -4424,8 +4747,7 @@ "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==", - "license": "MIT" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-bun-module": { "version": "1.2.1", @@ -5855,6 +6177,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..debcd91 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -9,12 +9,15 @@ "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", + "@radix-ui/react-context-menu": "^2.2.3", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", @@ -34,11 +37,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/api/files/route.ts b/packages/editor/src/app/api/files/route.ts index 5aab3d4..e2bfccc 100644 --- a/packages/editor/src/app/api/files/route.ts +++ b/packages/editor/src/app/api/files/route.ts @@ -1,122 +1,125 @@ -import { writeFile, mkdir, rm, unlink, readFile } from 'fs/promises' -import { NextResponse } from 'next/server' -import path from 'path' +import { writeFile, mkdir, rm, unlink, readFile } from "fs/promises"; +import { NextResponse } from "next/server"; +import path from "path"; function getBasePath() { - return path.join(process.cwd(), '..', 'compiled') + return path.join(process.cwd(), "..", "compiled"); } export async function POST(request: Request) { try { - const { path: filePath, content } = await request.json() - const fullPath = path.join(getBasePath(), filePath) - + const { path: filePath, content, isDirectory } = await request.json(); + const fullPath = path.join(getBasePath(), filePath); + // Ensure the directory exists - await mkdir(path.dirname(fullPath), { recursive: true }) - - // Write the file - await writeFile(fullPath, JSON.stringify(content, null, 2)) - return NextResponse.json({ success: true }) + await mkdir(path.dirname(fullPath), { recursive: true }); + + if (isDirectory) { + // If it's a directory, just create it + await mkdir(fullPath, { recursive: true }); + } else { + // If it's a file, write the content + await writeFile(fullPath, JSON.stringify(content, null, 2)); + } + + return NextResponse.json({ success: true }); } catch (error) { - console.error('Error creating file:', error) + console.error("Error creating file:", error); return NextResponse.json( - { error: 'Failed to create file' }, + { error: "Failed to create file" }, { status: 500 } - ) + ); } } export async function DELETE(request: Request) { try { - const { path: itemPath, type } = await request.json() - const fullPath = path.join(getBasePath(), itemPath) + const { path: itemPath, type } = await request.json(); + const fullPath = path.join(getBasePath(), itemPath); - if (type === 'folder') { - await rm(fullPath, { recursive: true, force: true }) + if (type === "folder") { + await rm(fullPath, { recursive: true, force: true }); } else { - await unlink(fullPath) + await unlink(fullPath); } - return NextResponse.json({ success: true }) + return NextResponse.json({ success: true }); } catch (error) { - console.error('Error deleting item:', error) + console.error("Error deleting item:", error); return NextResponse.json( - { error: 'Failed to delete item' }, + { error: "Failed to delete item" }, { status: 500 } - ) + ); } } export async function GET(request: Request) { try { - const { searchParams } = new URL(request.url) - const filePath = searchParams.get('path') - + const { searchParams } = new URL(request.url); + const filePath = searchParams.get("path"); + if (!filePath) { return NextResponse.json( - { error: 'No file path provided' }, + { error: "No file path provided" }, { status: 400 } - ) + ); } - const fullPath = path.join(getBasePath(), filePath) - const fileContent = await readFile(fullPath, 'utf-8') - const parsedContent = JSON.parse(fileContent) - - return NextResponse.json(parsedContent) + const fullPath = path.join(getBasePath(), filePath); + const fileContent = await readFile(fullPath, "utf-8"); + const parsedContent = JSON.parse(fileContent); + + return NextResponse.json(parsedContent); } catch (error) { - console.error('Error reading file:', error) - return NextResponse.json( - { error: 'Failed to read file' }, - { status: 500 } - ) + console.error("Error reading file:", error); + return NextResponse.json({ error: "Failed to read file" }, { status: 500 }); } } export async function PUT(request: Request) { try { - const { path: filePath, content } = await request.json() - + const { path: filePath, content } = await request.json(); + if (!filePath) { return NextResponse.json( - { error: 'No file path provided' }, + { error: "No file path provided" }, { status: 400 } - ) + ); } // Validate content if (!content) { return NextResponse.json( - { error: 'No content provided' }, + { error: "No content provided" }, { status: 400 } - ) + ); } - const fullPath = path.join(getBasePath(), filePath) - + const fullPath = path.join(getBasePath(), filePath); + // Check if directory exists, if not create it - await mkdir(path.dirname(fullPath), { recursive: true }) - + await mkdir(path.dirname(fullPath), { recursive: true }); + // Add retry logic const maxRetries = 3; for (let i = 0; i < maxRetries; i++) { try { - const jsonContent = JSON.stringify(content, null, 2) - await writeFile(fullPath, jsonContent, 'utf-8') - return NextResponse.json({ success: true }) + const jsonContent = JSON.stringify(content, null, 2); + await writeFile(fullPath, jsonContent, "utf-8"); + return NextResponse.json({ success: true }); } catch (writeError) { if (i === maxRetries - 1) throw writeError; - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s before retry + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1s before retry } } } catch (error) { - console.error('Error writing file:', error) + console.error("Error writing file:", error); return NextResponse.json( - { - error: 'Failed to write file', - details: error instanceof Error ? error.message : 'Unknown error' + { + error: "Failed to write file", + details: error instanceof Error ? error.message : "Unknown error", }, { status: 500 } - ) + ); } -} \ No newline at end of file +} diff --git a/packages/editor/src/app/editMode/[...slug]/page.tsx b/packages/editor/src/app/editMode/[...slug]/page.tsx index d43e4b1..90dbf39 100644 --- a/packages/editor/src/app/editMode/[...slug]/page.tsx +++ b/packages/editor/src/app/editMode/[...slug]/page.tsx @@ -1,343 +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: {} - } - - // Add default content for spacer blocks - if (newBlock.type === 'spacer') { - newBlock.content = 'medium' - } - - if (newBlock.type === 'table') { - newBlock.metadata = { - headers: ['Column 1', 'Column 2'], - rows: [['', '']] - } - } - - if (newBlock.type === 'checkList') { - newBlock.content = JSON.stringify([{ text: '', checked: false }]); - } - - if (newBlock.type === 'button') { - newBlock.content = 'Click me' - newBlock.metadata = { - buttonUrl: '#', - buttonStyle: { - variant: 'default', - size: 'default', - radius: 'md' - } - } - } - - 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/context-menu.tsx b/packages/editor/src/components/context-menu.tsx new file mode 100644 index 0000000..d618253 --- /dev/null +++ b/packages/editor/src/components/context-menu.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from '@/components/ui/context-menu'; +import { File, Folder, Trash2 } from 'lucide-react'; + +interface FileExplorerContextMenuProps { + children: React.ReactNode; + onNewFile: () => void; + onNewFolder: () => void; + onDelete: () => void; + isFolder: boolean; + disableDelete?: boolean; +} + +export function FileExplorerContextMenu({ + children, + onNewFile, + onNewFolder, + onDelete, + isFolder, + disableDelete = false, +}: FileExplorerContextMenuProps) { + return ( + + {children} + + {isFolder && ( + <> + + + New File + + + + New Folder + + + )} + {!disableDelete && ( + + + Delete + + )} + + + ); +} + 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..6aac432 --- /dev/null +++ b/packages/editor/src/components/file-explorer.tsx @@ -0,0 +1,623 @@ +"use client"; + +import { useState, KeyboardEvent, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Folder, + File, + Check, + X, + ChevronRight, + ChevronDown, +} from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { fetchAllContent } from "@/lib/getContents"; +import { FileExplorerContextMenu } from "@/components/context-menu"; +import { LanguageSelector } from "./language-selector"; +import { InsertionPoint } from "./insertion-point"; +import * as React from "react"; + +type FileNode = { + id: string; + name: string; + type: "file" | "folder"; + children?: FileNode[]; +}; + +interface MetaItem { + title: string + path?: string + items?: Record +} + +interface FileExplorerProps { + onFileSelect: (path: string) => void; +} + +interface RootMeta { + defaultRoute?: string + [key: string]: MetaItem | string | undefined +} + +const API_URL = + process.env.NEXT_PUBLIC_BACKEND_API_URL || "http://localhost:3001"; + +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(""); + const [selectedLanguage, setSelectedLanguage] = useState("all"); + + useEffect(() => { + const content = fetchAllContent(); + const transformedTree = transformContentToFileTree(content); + setFileTree(transformedTree); + + // Expand the selected language folder + if (selectedLanguage !== "all") { + const languageFolder = transformedTree.find( + (node) => node.name === selectedLanguage + ); + if (languageFolder) { + setExpandedFolders((prev) => new Set(prev).add(languageFolder.id)); + } + } + }, [selectedLanguage]); + + useEffect(() => { + if (selectedLanguage !== "all") { + const languageFolder = fileTree.find( + (node) => node.name === selectedLanguage + ); + if (languageFolder) { + setExpandedFolders((prev) => new Set(prev).add(languageFolder.id)); + } + } + }, [selectedLanguage, fileTree]); + + 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") => { + console.log("Starting new item:", { parentId, type }); // Add debug logging + 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}`; + + try { + if (newItemType === "file") { + // 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 = { + id: fileId, + title: fileId.split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '), + description: "", + author: "Anonymous", + date: new Date().toISOString().split("T")[0], + blocks: [], + }; + + const fileResponse = await fetch(`${API_URL}/api/files`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + path: fullPath, + content: defaultContent, + isDirectory: false, + }), + }); + + if (!fileResponse.ok) { + const errorText = await fileResponse.text(); + console.error("File creation failed:", errorText); + throw new Error(`Failed to create file: ${errorText}`); + } + + await updateMetadata(language, section, pathParts, fileId, defaultContent); + } else if (newItemType === "folder") { + const folderResponse = await fetch(`${API_URL}/api/files`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + path: fullPath, + isDirectory: true, + }), + }); + + if (!folderResponse.ok) { + const errorText = await folderResponse.text(); + console.error("Folder creation failed:", errorText); + throw new Error(`Failed to create folder: ${errorText}`); + } + } + + const updatedTree = addItemToTree(fileTree, newItemParent, newItem); + setFileTree(updatedTree); + + if (newItemType === "folder") { + setExpandedFolders((prev) => new Set(prev).add(newItem.id)); + } + + cancelNewItem(); + } catch (error) { + console.error("addNewItem: Error creating item:", error); + } + }; + + const updateMetadata = async ( + language: string, + section: string, + pathParts: string[], + fileId: string, + defaultContent: any + ) => { + + try { + 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('/')}${pathParts.slice(2).length > 0 ? '/' : ''}${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 updating metadata:", error); + } + }; + + 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 filterTreeByLanguage = (nodes: FileNode[]): FileNode[] => { + if (selectedLanguage === "all") return nodes; + + const languageFolder = nodes.find((node) => node.name === selectedLanguage); + if (languageFolder && languageFolder.children) { + return languageFolder.children; + } + + return []; + }; + + const renderFileTree = (nodes: FileNode[], level: number = 0, parentId?: string) => { + return ( +
    0 ? "ml-4 pl-4" : ""}`}> + {nodes.map((node, index) => ( + +
  • + startNewItem(node.id, "file")} + onNewFolder={() => startNewItem(node.id, "folder")} + onDelete={() => deleteItem(node.id, node.name, node.type)} + isFolder={node.type === "folder"} + disableDelete={ + node.type === "folder" && ( + level === 0 || // Language folders (en, es, etc) + (level === 1 && ["docs", "articles", "api"].includes(node.name)) // Protected subfolders + ) + } + > +
    +
    + {node.type === "folder" && ( + + )} +
    + {node.type === "folder" ? ( + + ) : ( + + )} +
    + + node.type === "file" + ? handleFileClick(node) + : toggleFolder(node.id) + } + > + {node.type === "file" + ? node.name.split(".").slice(0, -1).join(".") + : node.name} + +
    +
    +
    + {node.type === "folder" && node.children && ( + + {expandedFolders.has(node.id) && ( + +
    + {renderFileTree(node.children, level + 1, node.id)} +
    +
    + )} +
    + )} + {newItemParent === node.id && ( +
    + setNewItemName(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={`New ${newItemType}`} + className="h-8 text-sm bg-background text-foreground flex-grow" + /> + + +
    + )} +
  • + startNewItem(parentId || node.id, "file")} + onNewFolder={() => startNewItem(parentId || node.id, "folder")} + /> +
    + ))} +
+ ); + }; + const transformContentToFileTree = (content: { + [key: string]: any; + }): FileNode[] => { + const tree: { [key: string]: FileNode } = {}; + let rootNodes: FileNode[] = []; + + 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" + ) => { + const confirmMessage = `Are you sure you want to delete this ${nodeType}${ + nodeType === "folder" ? " and all its contents" : "" + }?`; + if (!confirm(confirmMessage)) { + return; + } + + const fullPath = getNodeFullPath(fileTree, nodeId); + if (!fullPath) { + console.error("Could not find full path for node"); + return; + } + + try { + // Delete the file/folder + 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"); + } + + // Update metadata + const pathParts = fullPath.split('/'); + const language = pathParts[0]; + const section = pathParts[1]; + const fileId = nodeName.replace('.json', ''); + + // Get current metadata + const rootMetaPath = `${language}/${section}/_meta.json`; + const metaResponse = await fetch(`${API_URL}/api/files?path=${encodeURIComponent(rootMetaPath)}`); + + if (metaResponse.ok) { + const rootMeta = await metaResponse.json() as RootMeta; + + // Navigate through the path to find the right section + let currentSection = rootMeta; + for (let i = 2; i < pathParts.length - 1; 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(''); + + if (currentSection[sectionKey] && (currentSection[sectionKey] as MetaItem).items) { + currentSection = (currentSection[sectionKey] as MetaItem).items!; + } + } + + // Remove the entry + if (nodeType === 'file' && currentSection[fileId]) { + delete currentSection[fileId]; + + // Save updated metadata + await fetch(`${API_URL}/api/files`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + path: rootMetaPath, + content: rootMeta + }) + }); + } + } + + // Update the file tree state + 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; + }); + }; + + return ( +
+
+

Project Explorer

+ +
+ + + {renderFileTree(filterTreeByLanguage(fileTree))} + +
+ ); +} + diff --git a/packages/editor/src/components/insertion-point.tsx b/packages/editor/src/components/insertion-point.tsx new file mode 100644 index 0000000..91a87f7 --- /dev/null +++ b/packages/editor/src/components/insertion-point.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { Plus, File, Folder } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; + +export function InsertionPoint({ + onNewFile, + onNewFolder, +}: { + onNewFile: () => void; + onNewFolder: () => void; +}) { + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setMenuOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const toggleMenu = () => { + setMenuOpen((prev) => !prev); + }; + + return ( +
+
+ + + {menuOpen && ( + + + + + )} +
+ ); +} diff --git a/packages/editor/src/components/language-selector.tsx b/packages/editor/src/components/language-selector.tsx new file mode 100644 index 0000000..b36ff59 --- /dev/null +++ b/packages/editor/src/components/language-selector.tsx @@ -0,0 +1,40 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +interface LanguageSelectorProps { + value: string; + onValueChange: (value: string) => void; +} + +const languages = [ + { value: 'all', label: 'All Languages' }, + { value: 'de', label: 'German' }, + { value: 'en', label: 'English' }, + { value: 'es', label: 'Spanish' }, + { value: 'fr', label: 'French' }, +]; + +export function LanguageSelector({ + value, + onValueChange, +}: LanguageSelectorProps) { + return ( + + ); +} diff --git a/packages/editor/src/components/ui/context-menu.tsx b/packages/editor/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..93ef37b --- /dev/null +++ b/packages/editor/src/components/ui/context-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} 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 } diff --git a/packages/editor/src/lib/getAkiradocsConfig.ts b/packages/editor/src/lib/getAkiradocsConfig.ts index 39dbce9..3311ee5 100644 --- a/packages/editor/src/lib/getAkiradocsConfig.ts +++ b/packages/editor/src/lib/getAkiradocsConfig.ts @@ -1,8 +1,8 @@ -import { AkiraDocsConfig } from '@/types/AkiraConfigType'; +import { AkiraDocsConfig } from "@/types/AkiraConfigType"; -const config = require('../../../akiradocs.config.json'); +const config = require("../../../akiradocs.config.json"); export function getAkiradocsConfig(): AkiraDocsConfig { - // Cast the entire config to AkiraDocsConfig since we know it matches the shape - return config as AkiraDocsConfig; -} \ No newline at end of file + // Cast the entire config to AkiraDocsConfig since we know it matches the shape + return config as AkiraDocsConfig; +}