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' && (
+
+ )}
+
+ {/* 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 ? '✓' : '✗'}
+
+ )}
+
+
+ )}
+
+ {/* Main Editor Area */}
+
+
+ {viewMode === 'visual' && 'Visual Editor'}
+ {viewMode === 'mindmap' && 'Mindmap Visualization'}
+
+
+ {viewMode === 'visual' && (
+
+
+
+ )}
+
+ {viewMode === 'mindmap' && (
+
+ )}
+
+
+
+ {/* Output Actions */}
+
+ {/* JSON Pretty */}
+
+
+
+
+ {outputs.jsonPretty && }
+
+
+
+ {/* JSON Minified */}
+
+
+
+
+ {outputs.jsonMinified && }
+
+
+
+ {/* PHP Serialized */}
+
+
+
+
+ {outputs.serialized && }
+
+
+
+
+ {/* Usage Tips */}
+
+
Usage Tips
+
+ - • Visual Editor: Create and modify object structures with forms
+ - • Mindmap View: Visualize complex JSON structures as interactive diagrams
+ - • Input Data: Paste JSON/PHP serialized data with auto-detection in the input field
+ - • Import data from files or use the sample data to get started
+ - • Export your data in any format: JSON pretty, minified, or PHP serialized
+ - • Use copy buttons to quickly copy any output format
+
+
+
+ );
+};
+
+export default ObjectEditor;