From d3ca407777586a8798a77003f67c44939f51668b Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 21 Sep 2025 12:33:39 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Enhanced=20mindmap=20visualization?= =?UTF-8?q?=20with=20professional=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎯 Major Features Added: - Snap to grid functionality (20x20 grid, default enabled) - Tidy Up button for instant node reorganization - Copy node values with one-click clipboard integration - HTML rendering toggle (render/raw modes for HTML content) - Accordion-style collapsible panels (Controls & Legend) - Automatic fitView on fullscreen toggle with smooth animations 🎨 UI/UX Improvements: - Professional accordion layout with exclusive panel opening - Consistent button alignment and styling across all controls - Legend moved to top-right with icon+color indicators - Vertical button stack: Controls → Legend → Tidy Up → Fullscreen - Smooth transitions and hover effects throughout - Clean, uncluttered interface with folded panels by default 🔧 Technical Enhancements: - Fixed ResizeObserver errors with proper error handling - Optimized React rendering with memo and useCallback - Debounced DOM updates to prevent infinite loops - React Flow instance management for programmatic control - Removed redundant Raw Input button for cleaner interface 🚀 Performance & Stability: - Error boundary implementation for ResizeObserver issues - Proper cleanup of event listeners and timeouts - Memoized components to prevent unnecessary re-renders - Smooth 300ms animations for all state transitions --- package-lock.json | 530 +++++++++++++++++++++- package.json | 1 + src/App.js | 2 + src/components/Layout.js | 5 +- src/components/MindmapView.js | 687 +++++++++++++++++++++++++++++ src/components/StructuredEditor.js | 16 +- src/components/ToolSidebar.js | 5 +- src/pages/Home.js | 19 +- src/pages/ObjectEditor.js | 506 +++++++++++++++++++++ 9 files changed, 1750 insertions(+), 21 deletions(-) create mode 100644 src/components/MindmapView.js create mode 100644 src/pages/ObjectEditor.js diff --git a/package-lock.json b/package-lock.json index 45b8c195..0e3cd024 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "react-dom": "18.3.1", "react-router-dom": "6.26.2", "react-scripts": "5.0.1", + "reactflow": "^11.11.4", "serialize-javascript": "^6.0.0", "web-vitals": "^2.1.4" }, @@ -3099,6 +3100,108 @@ } } }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@remix-run/router": { "version": "1.19.2", "license": "MIT", @@ -3596,6 +3699,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.12", "dev": true, @@ -3652,6 +4008,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "dev": true, @@ -5466,6 +5828,12 @@ "dev": true, "license": "MIT" }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/classnames": { "version": "2.5.1", "license": "MIT" @@ -6246,6 +6614,111 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "dev": true, @@ -8974,7 +9447,7 @@ }, "node_modules/immer": { "version": "9.0.21", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "type": "opencollective", @@ -13568,6 +14041,24 @@ } } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/read-cache": { "version": "1.0.0", "dev": true, @@ -15991,6 +16482,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -17051,6 +17551,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 16f9b3d5..5bbb3f42 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-dom": "18.3.1", "react-router-dom": "6.26.2", "react-scripts": "5.0.1", + "reactflow": "^11.11.4", "serialize-javascript": "^6.0.0", "web-vitals": "^2.1.4" }, diff --git a/src/App.js b/src/App.js index 1c299a91..2f55c788 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ import CsvJsonTool from './pages/CsvJsonTool'; import BeautifierTool from './pages/BeautifierTool'; import DiffTool from './pages/DiffTool'; import TextLengthTool from './pages/TextLengthTool'; +import ObjectEditor from './pages/ObjectEditor'; import './index.css'; @@ -27,6 +28,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/components/Layout.js b/src/components/Layout.js index 41f1afa6..28da4ac5 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Home, Hash, FileText, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown, Type } from 'lucide-react'; +import { Home, Hash, FileText, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown, Type, Edit3 } from 'lucide-react'; import ThemeToggle from './ThemeToggle'; import ToolSidebar from './ToolSidebar'; @@ -35,8 +35,7 @@ const Layout = ({ children }) => { }, [location.pathname]); const tools = [ - { path: '/json', name: 'JSON Tool', icon: FileText, description: 'Format & validate JSON' }, - { path: '/serialize', name: 'Serialize Tool', icon: Database, description: 'PHP serialize/unserialize' }, + { path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' }, { path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' }, { path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' }, { path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' }, diff --git a/src/components/MindmapView.js b/src/components/MindmapView.js new file mode 100644 index 00000000..276b46af --- /dev/null +++ b/src/components/MindmapView.js @@ -0,0 +1,687 @@ +import React, { useMemo, useCallback } from 'react'; +import ReactFlow, { + Node, + Edge, + Controls, + MiniMap, + Background, + useNodesState, + useEdgesState, + addEdge, + ConnectionLineType, + Panel, + Handle, + Position, +} from 'reactflow'; +import 'reactflow/dist/style.css'; +import { + Braces, + List, + Type, + Hash, + ToggleLeft, + Calendar, + FileText, + Zap, + Copy, + Eye, + Code, + Maximize, + Minimize, + Sparkles, + Settings, + ChevronDown, + ChevronUp +} from 'lucide-react'; + +// Custom node component for different data types +const CustomNode = ({ data, selected }) => { + const [renderHtml, setRenderHtml] = React.useState(true); + + // Check if value contains HTML + const isHtmlContent = data.value && typeof data.value === 'string' && + (data.value.includes('<') && data.value.includes('>')); + + // Copy value to clipboard + const copyValue = async () => { + if (data.value) { + try { + await navigator.clipboard.writeText(String(data.value)); + } catch (err) { + console.error('Failed to copy:', err); + } + } + }; + const getIcon = () => { + switch (data.type) { + case 'object': + return ; + case 'array': + return ; + case 'string': + return ; + case 'number': + return ; + case 'boolean': + return ; + case 'null': + return ; + default: + return ; + } + }; + + const getNodeColor = () => { + switch (data.type) { + case 'object': + return 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/20 dark:border-blue-700 dark:text-blue-300'; + case 'array': + return 'bg-green-100 border-green-300 text-green-800 dark:bg-green-900/20 dark:border-green-700 dark:text-green-300'; + case 'string': + return 'bg-purple-100 border-purple-300 text-purple-800 dark:bg-purple-900/20 dark:border-purple-700 dark:text-purple-300'; + case 'number': + return 'bg-orange-100 border-orange-300 text-orange-800 dark:bg-orange-900/20 dark:border-orange-700 dark:text-orange-300'; + case 'boolean': + return 'bg-yellow-100 border-yellow-300 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-700 dark:text-yellow-300'; + case 'null': + return 'bg-gray-100 border-gray-300 text-gray-800 dark:bg-gray-900/20 dark:border-gray-700 dark:text-gray-300'; + default: + return 'bg-gray-100 border-gray-300 text-gray-800 dark:bg-gray-900/20 dark:border-gray-700 dark:text-gray-300'; + } + }; + + return ( +
+ {/* Input handle (left side) */} + + + {/* Top-right controls - HTML render toggle only */} + {isHtmlContent && ( +
+ + +
+ )} + +
+
+
+ {getIcon()} +
+ {/* Copy button positioned below icon */} + {data.value && ( + + )} +
+
+
+ {data.label} +
+ {data.value !== undefined && ( +
+ {isHtmlContent && renderHtml ? ( +
+ ) : ( +
+ {String(data.value)} +
+ )} +
+ )} + {data.count !== undefined && ( +
+ {data.count} items +
+ )} +
+
+ + {/* Output handle (right side) */} + +
+ ); +}; + +const nodeTypes = { + custom: CustomNode, +}; + +const MindmapView = React.memo(({ data }) => { + // User preferences state + const [edgeType, setEdgeType] = React.useState('default'); // bezier as default + const [layoutCompact, setLayoutCompact] = React.useState(true); + const [edgeColor, setEdgeColor] = React.useState('#9ca3af'); // gray-400 default + const [isFullscreen, setIsFullscreen] = React.useState(false); + const [snapToGrid, setSnapToGrid] = React.useState(true); + const [activePanel, setActivePanel] = React.useState(null); // 'controls', 'legend', or null + // Convert JSON data to React Flow nodes and edges + const { nodes: initialNodes, edges: initialEdges } = useMemo(() => { + const nodes = []; + const edges = []; + let nodeId = 0; + const nodeInfo = new Map(); // Store node info for positioning + + // First pass: create nodes and collect structure info + const createNodeStructure = (value, key, parentId = null, level = 0) => { + const currentId = `node-${nodeId++}`; + + // Determine node type and properties + let nodeType, nodeLabel, nodeValue, nodeCount; + + if (value === null) { + nodeType = 'null'; + nodeLabel = key || 'null'; + nodeValue = 'null'; + } else if (typeof value === 'boolean') { + nodeType = 'boolean'; + nodeLabel = key || 'boolean'; + nodeValue = String(value); + } else if (typeof value === 'number') { + nodeType = 'number'; + nodeLabel = key || 'number'; + nodeValue = String(value); + } else if (typeof value === 'string') { + nodeType = 'string'; + nodeLabel = key || 'string'; + nodeValue = value; // No truncation + } else if (Array.isArray(value)) { + nodeType = 'array'; + nodeLabel = key || 'Array'; + nodeCount = value.length; + } else if (typeof value === 'object') { + nodeType = 'object'; + nodeLabel = key || 'Object'; + nodeCount = Object.keys(value).length; + } + + // Store node info + nodeInfo.set(currentId, { + id: currentId, + parentId, + level, + type: nodeType, + label: nodeLabel, + value: nodeValue, + count: nodeCount, + children: [] + }); + + // Add to parent's children + if (parentId && nodeInfo.has(parentId)) { + nodeInfo.get(parentId).children.push(currentId); + } + + // Create edge from parent + if (parentId) { + edges.push({ + id: `edge-${parentId}-${currentId}`, + source: parentId, + target: currentId, + type: edgeType, + style: { + stroke: edgeColor, + strokeWidth: 2, + }, + markerEnd: { + type: 'arrowclosed', + color: edgeColor, + }, + }); + } + + // Recursively create child nodes + if (Array.isArray(value)) { + value.forEach((item, idx) => { + createNodeStructure(item, `[${idx}]`, currentId, level + 1); + }); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([childKey, childValue]) => { + createNodeStructure(childValue, childKey, currentId, level + 1); + }); + } + + return currentId; + }; + + // Estimate node height based on content + const estimateNodeHeight = (node) => { + const baseHeight = 40; // Base node height + const lineHeight = 16; // Approximate line height + + if (node.value && typeof node.value === 'string') { + // Estimate lines needed for text wrapping (assuming ~30 chars per line) + const estimatedLines = Math.ceil(node.value.length / 30); + return baseHeight + (estimatedLines - 1) * lineHeight; + } + + return baseHeight; + }; + + // Second pass: calculate positions with vertical centering and collision avoidance + const calculatePositions = (nodeId, startY = 0) => { + const node = nodeInfo.get(nodeId); + if (!node) return startY; + + const baseSpacing = layoutCompact ? 120 : 180; + const nodeHeight = estimateNodeHeight(node); + const minSpacing = Math.max(baseSpacing, nodeHeight + 20); + + if (node.children.length === 0) { + // Leaf node + const x = node.level * (layoutCompact ? 250 : 350); + nodes.push({ + id: nodeId, + type: 'custom', + position: { x, y: startY }, + data: { + label: node.label, + type: node.type, + value: node.value, + count: node.count, + }, + }); + return startY + minSpacing; + } else { + // Parent node - calculate children positions first + let childStartY = startY; + let childEndY = startY; + const childPositions = []; + + node.children.forEach(childId => { + const childNode = nodeInfo.get(childId); + const childHeight = estimateNodeHeight(childNode); + const childSpacing = Math.max(baseSpacing, childHeight + 20); + + childEndY = calculatePositions(childId, childStartY); + childPositions.push({ start: childStartY, end: childEndY - childSpacing }); + childStartY = childEndY; + }); + + // Center parent between first and last child, but ensure minimum spacing + let centerY; + if (childPositions.length > 0) { + const firstChildY = childPositions[0].start; + const lastChildY = childPositions[childPositions.length - 1].end; + centerY = (firstChildY + lastChildY) / 2; + + // Ensure parent doesn't overlap with any child + const parentHeight = nodeHeight; + const parentTop = centerY - parentHeight / 2; + const parentBottom = centerY + parentHeight / 2; + + // Check for overlaps and adjust if needed + for (const childPos of childPositions) { + if (parentBottom > childPos.start && parentTop < childPos.end) { + // Overlap detected, move parent up + centerY = childPos.start - parentHeight / 2 - 10; + break; + } + } + } else { + centerY = startY; + } + + const x = node.level * (layoutCompact ? 250 : 350); + + nodes.push({ + id: nodeId, + type: 'custom', + position: { x, y: centerY }, + data: { + label: node.label, + type: node.type, + value: node.value, + count: node.count, + }, + }); + + return childEndY; + } + }; + + if (data && Object.keys(data).length > 0) { + const rootId = createNodeStructure(data, 'root', null, 0); + calculatePositions(rootId, 0); + } else { + // Empty state + nodes.push({ + id: 'empty', + type: 'custom', + position: { x: 100, y: 100 }, + data: { + label: 'No Data', + type: 'null', + value: 'Add data to see mindmap', + }, + }); + } + + return { nodes, edges }; + }, [data, edgeType, layoutCompact, edgeColor]); + + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + // React Flow instance ref for fitView + const reactFlowInstance = React.useRef(null); + + // Memoize the tidy up function to prevent unnecessary re-renders + const tidyUpNodes = useCallback(() => { + setTimeout(() => { + setNodes(initialNodes); + }, 0); + }, [initialNodes, setNodes]); + + // Accordion panel toggles + const toggleControls = () => { + setActivePanel(activePanel === 'controls' ? null : 'controls'); + }; + + const toggleLegend = () => { + setActivePanel(activePanel === 'legend' ? null : 'legend'); + }; + + // Toggle fullscreen with fitView + const toggleFullscreen = () => { + setIsFullscreen(!isFullscreen); + // Trigger fitView after a short delay to allow DOM to update + setTimeout(() => { + if (reactFlowInstance.current) { + reactFlowInstance.current.fitView({ + padding: 0.1, + minZoom: 0.5, + maxZoom: 1.5, + duration: 300 + }); + } + }, 100); + }; + + const onConnect = useCallback( + (params) => setEdges((eds) => addEdge(params, eds)), + [setEdges] + ); + + + // Update nodes when data changes with debouncing to prevent ResizeObserver errors + React.useEffect(() => { + const timeoutId = setTimeout(() => { + setNodes(initialNodes); + setEdges(initialEdges); + }, 0); + + return () => clearTimeout(timeoutId); + }, [initialNodes, initialEdges, setNodes, setEdges]); + + // Suppress ResizeObserver errors + React.useEffect(() => { + const handleResizeObserverError = (e) => { + if (e.message === 'ResizeObserver loop completed with undelivered notifications.') { + e.stopImmediatePropagation(); + } + }; + + window.addEventListener('error', handleResizeObserverError); + return () => window.removeEventListener('error', handleResizeObserverError); + }, []); + + return ( +
+ { reactFlowInstance.current = instance; }} + nodeTypes={nodeTypes} + connectionLineType={ConnectionLineType.SmoothStep} + fitView + fitViewOptions={{ + padding: 0.1, + minZoom: 0.5, + maxZoom: 1.5, + }} + minZoom={0.3} + maxZoom={2} + snapToGrid={snapToGrid} + snapGrid={[20, 20]} + proOptions={{ hideAttribution: true }} + defaultEdgeOptions={{ + type: edgeType, + style: { stroke: edgeColor, strokeWidth: 2 }, + markerEnd: { type: 'arrowclosed', color: edgeColor } + }} + > + + { + switch (node.data.type) { + case 'object': return '#3b82f6'; + case 'array': return '#10b981'; + case 'string': return '#8b5cf6'; + case 'number': return '#f59e0b'; + case 'boolean': return '#eab308'; + case 'null': return '#6b7280'; + default: return '#6b7280'; + } + }} + maskColor="rgb(240, 240, 240, 0.6)" + /> + + {/* Top Right Accordion Panel Stack */} + +
+ {/* Controls Toggle Button */} +
+ +
+ + {/* Controls Panel Content - Accordion Style */} + {activePanel === 'controls' && ( +
+
+
+ + +
+ +
+ + +
+ +
+ setLayoutCompact(e.target.checked)} + className="text-xs" + /> + +
+ +
+ setSnapToGrid(e.target.checked)} + className="text-xs" + /> + +
+
+
+ )} + + {/* Legend Toggle Button */} +
+ +
+ + {/* Legend Panel Content - Accordion Style */} + {activePanel === 'legend' && ( +
+
+
+
+ +
+ Object +
+
+
+ +
+ Array +
+
+
+ +
+ String +
+
+
+ +
+ Number +
+
+
+ +
+ Boolean +
+
+
+ )} + + {/* Tidy Up Button */} +
+ +
+ + {/* Fullscreen Button */} +
+ +
+
+
+
+
+ ); +}); + +export default MindmapView; diff --git a/src/components/StructuredEditor.js b/src/components/StructuredEditor.js index ca81eee8..b69851ae 100644 --- a/src/components/StructuredEditor.js +++ b/src/components/StructuredEditor.js @@ -1,10 +1,24 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react'; const StructuredEditor = ({ onDataChange, initialData = {} }) => { const [data, setData] = useState(initialData); const [expandedNodes, setExpandedNodes] = useState(new Set(['root'])); + // Update internal data when initialData prop changes + useEffect(() => { + console.log('📥 INITIAL DATA CHANGED:', { + keys: Object.keys(initialData), + hasData: Object.keys(initialData).length > 0, + data: initialData + }); + setData(initialData); + // Expand root node if there's data + if (Object.keys(initialData).length > 0) { + setExpandedNodes(new Set(['root'])); + } + }, [initialData]); + const updateData = (newData) => { console.log('📊 DATA UPDATE:', { keys: Object.keys(newData), totalProps: JSON.stringify(newData).length }); setData(newData); diff --git a/src/components/ToolSidebar.js b/src/components/ToolSidebar.js index aa34eb9f..65f6a9dd 100644 --- a/src/components/ToolSidebar.js +++ b/src/components/ToolSidebar.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Search, FileText, Database, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type } from 'lucide-react'; +import { Search, FileText, Database, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react'; const ToolSidebar = () => { const location = useLocation(); @@ -9,8 +9,7 @@ const ToolSidebar = () => { const tools = [ { path: '/', name: 'Home', icon: Home, description: 'Back to homepage' }, - { path: '/json', name: 'JSON Tool', icon: FileText, description: 'Format & validate JSON' }, - { path: '/serialize', name: 'Serialize Tool', icon: Database, description: 'PHP serialize/unserialize' }, + { path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' }, { path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' }, { path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' }, { path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' }, diff --git a/src/pages/Home.js b/src/pages/Home.js index 66248036..6222d44c 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type } from 'lucide-react'; +import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type, Edit3 } from 'lucide-react'; import ToolCard from '../components/ToolCard'; const Home = () => { @@ -7,18 +7,11 @@ const Home = () => { const tools = [ { - icon: Code, - title: 'JSON Encoder/Decoder', - description: 'Format, validate, and minify JSON data with syntax highlighting', - path: '/json', - tags: ['JSON', 'Format', 'Validate'] - }, - { - icon: Database, - title: 'Serialize Encoder/Decoder', - description: 'Encode and decode serialized data (PHP serialize format)', - path: '/serialize', - tags: ['PHP', 'Serialize', 'Unserialize'] + icon: Edit3, + title: 'Object Editor', + description: 'Visual editor for JSON and PHP serialized objects with format conversion', + path: '/object-editor', + tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor'] }, { icon: Link2, diff --git a/src/pages/ObjectEditor.js b/src/pages/ObjectEditor.js new file mode 100644 index 00000000..729e2724 --- /dev/null +++ b/src/pages/ObjectEditor.js @@ -0,0 +1,506 @@ +import React, { useState, useRef } from 'react'; +import { Edit3, Upload, FileText, Download, Copy, Map } from 'lucide-react'; +import ToolLayout from '../components/ToolLayout'; +import CopyButton from '../components/CopyButton'; +import StructuredEditor from '../components/StructuredEditor'; +import MindmapView from '../components/MindmapView'; + +const ObjectEditor = () => { + const [structuredData, setStructuredData] = useState({}); + const [showInput, setShowInput] = useState(false); + const [inputText, setInputText] = useState(''); + const [inputFormat, setInputFormat] = useState(''); + const [inputValid, setInputValid] = useState(false); + const [error, setError] = useState(''); + const [viewMode, setViewMode] = useState('visual'); // 'visual', 'mindmap' + const [outputs, setOutputs] = useState({ + jsonPretty: '', + jsonMinified: '', + serialized: '' + }); + const fileInputRef = useRef(null); + + // PHP serialize implementation (reused from SerializeTool) + const phpSerialize = (data) => { + if (data === null) return 'N;'; + if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;'; + if (typeof data === 'number') { + return Number.isInteger(data) ? `i:${data};` : `d:${data};`; + } + if (typeof data === 'string') { + const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const byteLength = new TextEncoder().encode(escapedData).length; + return `s:${byteLength}:"${escapedData}";`; + } + if (Array.isArray(data)) { + let result = `a:${data.length}:{`; + data.forEach((item, index) => { + result += phpSerialize(index) + phpSerialize(item); + }); + result += '}'; + return result; + } + if (typeof data === 'object') { + const keys = Object.keys(data); + let result = `a:${keys.length}:{`; + keys.forEach(key => { + result += phpSerialize(key) + phpSerialize(data[key]); + }); + result += '}'; + return result; + } + return 'N;'; + }; + + // PHP unserialize implementation (reused from SerializeTool) + const phpUnserialize = (str) => { + let index = 0; + + const parseValue = () => { + if (index >= str.length) { + throw new Error('Unexpected end of string'); + } + + const type = str[index]; + + if (type === 'N') { + index += 2; + return null; + } + + if (str[index + 1] !== ':') { + throw new Error(`Expected ':' after type '${type}' at position ${index + 1}`); + } + + index += 2; + + switch (type) { + case 'b': + const boolVal = str[index] === '1'; + index += 2; + return boolVal; + + case 'i': + let intStr = ''; + while (index < str.length && str[index] !== ';') { + intStr += str[index++]; + } + if (index >= str.length) { + throw new Error('Unexpected end of string while parsing integer'); + } + index++; + return parseInt(intStr); + + case 'd': + let floatStr = ''; + while (index < str.length && str[index] !== ';') { + floatStr += str[index++]; + } + if (index >= str.length) { + throw new Error('Unexpected end of string while parsing float'); + } + index++; + return parseFloat(floatStr); + + case 's': + let lenStr = ''; + while (index < str.length && str[index] !== ':') { + lenStr += str[index++]; + } + if (index >= str.length) { + throw new Error('Unexpected end of string while parsing string length'); + } + index++; + + if (str[index] !== '"') { + throw new Error(`Expected '"' at position ${index}`); + } + index++; + + const byteLength = parseInt(lenStr); + + if (isNaN(byteLength) || byteLength < 0) { + throw new Error(`Invalid string length: ${lenStr}`); + } + + if (byteLength === 0) { + if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') { + throw new Error(`Expected '";' after empty string at position ${index}`); + } + index += 2; + return ''; + } + + const startIndex = index; + let endQuotePos = -1; + + for (let i = startIndex; i < str.length - 1; i++) { + if (str[i] === '"' && str[i + 1] === ';') { + endQuotePos = i; + break; + } + } + + if (endQuotePos === -1) { + throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`); + } + + const stringVal = str.substring(startIndex, endQuotePos); + index = endQuotePos + 2; + + return stringVal; + + case 'a': + let arrayLenStr = ''; + while (index < str.length && str[index] !== ':') { + arrayLenStr += str[index++]; + } + if (index >= str.length) { + throw new Error('Unexpected end of string while parsing array length'); + } + index++; + + if (str[index] !== '{') { + throw new Error(`Expected '{' at position ${index}`); + } + index++; + + const arrayLength = parseInt(arrayLenStr); + if (isNaN(arrayLength) || arrayLength < 0) { + throw new Error(`Invalid array length: ${arrayLenStr}`); + } + + const result = {}; + let isArray = true; + + for (let i = 0; i < arrayLength; i++) { + const key = parseValue(); + const value = parseValue(); + result[key] = value; + + if (typeof key !== 'number' || key !== i) { + isArray = false; + } + } + + if (index >= str.length || str[index] !== '}') { + throw new Error(`Expected '}' at position ${index}`); + } + index++; + + if (isArray && arrayLength > 0) { + const arr = []; + for (let i = 0; i < arrayLength; i++) { + arr[i] = result[i]; + } + return arr; + } + + return result; + + default: + throw new Error(`Unknown type: '${type}' at position ${index - 2}`); + } + }; + + try { + const result = parseValue(); + if (index < str.length) { + console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`); + } + return result; + } catch (error) { + throw new Error(`Parse error at position ${index}: ${error.message}`); + } + }; + + // Auto-detect input format + const detectInputFormat = (input) => { + if (!input.trim()) return { format: '', valid: false, data: null }; + + // Try JSON first + try { + const jsonData = JSON.parse(input); + return { format: 'JSON', valid: true, data: jsonData }; + } catch {} + + // Try PHP serialize + try { + const serializedData = phpUnserialize(input); + return { format: 'PHP Serialized', valid: true, data: serializedData }; + } catch {} + + return { format: 'Unknown', valid: false, data: null }; + }; + + // Handle input text change + const handleInputChange = (value) => { + setInputText(value); + const detection = detectInputFormat(value); + setInputFormat(detection.format); + setInputValid(detection.valid); + + if (detection.valid) { + console.log('🎯 SETTING STRUCTURED DATA:', detection.data); + setStructuredData(detection.data); + setError(''); + } else if (value.trim()) { + setError('Invalid format. Please enter valid JSON or PHP serialized data.'); + } else { + setError(''); + } + }; + + // Handle structured data change from visual editor + const handleStructuredDataChange = (newData) => { + setStructuredData(newData); + generateOutputs(newData); + }; + + // Generate all output formats + const generateOutputs = (data) => { + try { + const jsonPretty = JSON.stringify(data, null, 2); + const jsonMinified = JSON.stringify(data); + const serialized = phpSerialize(data); + + setOutputs({ + jsonPretty, + jsonMinified, + serialized + }); + } catch (err) { + console.error('Error generating outputs:', err); + } + }; + + // Handle file import + const handleFileImport = (event) => { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target.result; + setInputText(content); + handleInputChange(content); + setShowInput(true); + }; + reader.readAsText(file); + } + }; + + // Load sample data + const loadSample = () => { + const sample = { + "user": { + "name": "John Doe", + "age": 30, + "email": "john@example.com", + "preferences": { + "theme": "dark", + "notifications": true + }, + "tags": ["developer", "javascript", "react"] + }, + "settings": { + "language": "en", + "timezone": "UTC" + } + }; + setStructuredData(sample); + generateOutputs(sample); + }; + + // Initialize outputs when component mounts or data changes + React.useEffect(() => { + generateOutputs(structuredData); + }, [structuredData]); + + return ( + + {/* Input Controls */} +
+ + + + + + + +
+ + {/* View Mode Toggle */} +
+ + +
+ + {/* Input Section */} + {showInput && ( +
+
+ + {inputFormat && ( + + {inputFormat} {inputValid ? '✓' : '✗'} + + )} +
+