feat: complete a simple json visualiser

This commit is contained in:
2026-01-15 15:22:17 +08:00
parent f8eda9ba2f
commit c06dacb38e
4 changed files with 297 additions and 121 deletions
+4
View File
@@ -16,6 +16,8 @@
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.2", "axios": "^1.13.2",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"jsonpath": "^1.1.1",
"lodash": "^4.17.21",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
@@ -25,6 +27,8 @@
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@types/jsonpath": "^0.2.4",
"@types/lodash": "^4.17.23",
"@types/node": "^22.19.2", "@types/node": "^22.19.2",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
+147
View File
@@ -20,6 +20,12 @@ importers:
dayjs: dayjs:
specifier: ^1.11.19 specifier: ^1.11.19
version: 1.11.19 version: 1.11.19
jsonpath:
specifier: ^1.1.1
version: 1.1.1
lodash:
specifier: ^4.17.21
version: 4.17.21
react: react:
specifier: ^19.2.3 specifier: ^19.2.3
version: 19.2.3 version: 19.2.3
@@ -42,6 +48,12 @@ importers:
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18 version: 4.1.18
devDependencies: devDependencies:
'@types/jsonpath':
specifier: ^0.2.4
version: 0.2.4
'@types/lodash':
specifier: ^4.17.23
version: 4.17.23
'@types/node': '@types/node':
specifier: ^22.19.2 specifier: ^22.19.2
version: 22.19.2 version: 22.19.2
@@ -559,6 +571,12 @@ packages:
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/jsonpath@0.2.4':
resolution: {integrity: sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==}
'@types/lodash@4.17.23':
resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
'@types/node@22.19.2': '@types/node@22.19.2':
resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==} resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==}
@@ -627,6 +645,9 @@ packages:
supports-color: supports-color:
optional: true optional: true
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
delayed-stream@1.0.0: delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@@ -671,6 +692,32 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'} engines: {node: '>=6'}
escodegen@1.14.3:
resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==}
engines: {node: '>=4.0'}
hasBin: true
esprima@1.2.2:
resolution: {integrity: sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==}
engines: {node: '>=0.4.0'}
hasBin: true
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
estraverse@4.3.0:
resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
engines: {node: '>=4.0'}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -756,6 +803,13 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jsonpath@1.1.1:
resolution: {integrity: sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==}
levn@0.3.0:
resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==}
engines: {node: '>= 0.8.0'}
lightningcss-android-arm64@1.30.2: lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@@ -826,6 +880,9 @@ packages:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -855,6 +912,10 @@ packages:
node-releases@2.0.27: node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
optionator@0.8.3:
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
engines: {node: '>= 0.8.0'}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -866,6 +927,10 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.1.2:
resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==}
engines: {node: '>= 0.8.0'}
prettier@3.7.4: prettier@3.7.4:
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -955,6 +1020,13 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
static-eval@2.0.2:
resolution: {integrity: sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==}
tailwindcss@4.1.18: tailwindcss@4.1.18:
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
@@ -966,11 +1038,18 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
type-check@0.3.2:
resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
engines: {node: '>= 0.8.0'}
typescript@5.9.3: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
underscore@1.12.1:
resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==}
undici-types@6.21.0: undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -1025,6 +1104,10 @@ packages:
yaml: yaml:
optional: true optional: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -1414,6 +1497,10 @@ snapshots:
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/jsonpath@0.2.4': {}
'@types/lodash@4.17.23': {}
'@types/node@22.19.2': '@types/node@22.19.2':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
@@ -1483,6 +1570,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
deep-is@0.1.4: {}
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
@@ -1546,6 +1635,25 @@ snapshots:
escalade@3.2.0: {} escalade@3.2.0: {}
escodegen@1.14.3:
dependencies:
esprima: 4.0.1
estraverse: 4.3.0
esutils: 2.0.3
optionator: 0.8.3
optionalDependencies:
source-map: 0.6.1
esprima@1.2.2: {}
esprima@4.0.1: {}
estraverse@4.3.0: {}
esutils@2.0.3: {}
fast-levenshtein@2.0.6: {}
fdir@6.5.0(picomatch@4.0.3): fdir@6.5.0(picomatch@4.0.3):
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
@@ -1611,6 +1719,17 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
jsonpath@1.1.1:
dependencies:
esprima: 1.2.2
static-eval: 2.0.2
underscore: 1.12.1
levn@0.3.0:
dependencies:
prelude-ls: 1.1.2
type-check: 0.3.2
lightningcss-android-arm64@1.30.2: lightningcss-android-arm64@1.30.2:
optional: true optional: true
@@ -1660,6 +1779,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2
lodash@4.17.21: {}
lru-cache@5.1.1: lru-cache@5.1.1:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
@@ -1682,6 +1803,15 @@ snapshots:
node-releases@2.0.27: {} node-releases@2.0.27: {}
optionator@0.8.3:
dependencies:
deep-is: 0.1.4
fast-levenshtein: 2.0.6
levn: 0.3.0
prelude-ls: 1.1.2
type-check: 0.3.2
word-wrap: 1.2.5
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@4.0.3: {} picomatch@4.0.3: {}
@@ -1692,6 +1822,8 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
prelude-ls@1.1.2: {}
prettier@3.7.4: {} prettier@3.7.4: {}
proxy-from-env@1.1.0: {} proxy-from-env@1.1.0: {}
@@ -1778,6 +1910,13 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map@0.6.1:
optional: true
static-eval@2.0.2:
dependencies:
escodegen: 1.14.3
tailwindcss@4.1.18: {} tailwindcss@4.1.18: {}
tapable@2.3.0: {} tapable@2.3.0: {}
@@ -1787,8 +1926,14 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
type-check@0.3.2:
dependencies:
prelude-ls: 1.1.2
typescript@5.9.3: {} typescript@5.9.3: {}
underscore@1.12.1: {}
undici-types@6.21.0: {} undici-types@6.21.0: {}
update-browserslist-db@1.2.2(browserslist@4.28.1): update-browserslist-db@1.2.2(browserslist@4.28.1):
@@ -1815,4 +1960,6 @@ snapshots:
jiti: 2.6.1 jiti: 2.6.1
lightningcss: 1.30.2 lightningcss: 1.30.2
word-wrap@1.2.5: {}
yallist@3.1.1: {} yallist@3.1.1: {}
+52
View File
@@ -0,0 +1,52 @@
import { useMemo } from "react"
import jp, { PathComponent } from "jsonpath"
import _ from "lodash"
export interface JsonTreeNodeProps {
data: unknown
path: PathComponent[]
matchedPaths: string[]
}
export default function JsonTreeNode({ data, path, matchedPaths }: JsonTreeNodeProps) {
const currentPathString = useMemo(() => jp.stringify(path), [path])
const isMatched = matchedPaths.includes(currentPathString)
const highlightClass = isMatched ? "bg-yellow-200 ring-2 ring-yellow-400 rounded-sm" : ""
if (_.isObject(data) && data != null) {
const isArray = Array.isArray(data)
const entries = Object.entries(data)
return (
<div className="ml-6 border-l border-slate-200 pl-3 transition-all">
<span className="text-slate-500">{isArray ? "[" : "{"}</span>
{entries.map(([key, value], index) => {
const nextPath = [...path, isArray ? Number(key) : key]
return (
<div key={key} className="my-1">
{!isArray && <span className="text-indigo-600 font-medium">"{key}": </span>}
<JsonTreeNode data={value} path={nextPath} matchedPaths={matchedPaths} />
{index < entries.length - 1 && <span className="text-slate-400">,</span>}
</div>
)
})}
<span className="text-slate-500">{isArray ? "]" : "}"}</span>
</div>
)
}
const renderValue = () => {
if (typeof data === "string") return <span className="text-emerald-600">"{data}"</span>
if (typeof data === "number") return <span className="text-blue-600">{data}</span>
if (typeof data === "boolean") return <span className="text-orange-600">{String(data)}</span>
return <span className="text-gray-400">null</span>
}
return (
<span className={`inline-block px-1 transition-colors duration-200 ${highlightClass}`}>
{renderValue()}
</span>
)
}
+89 -116
View File
@@ -1,138 +1,111 @@
import { Link } from "react-router-dom" import { useMemo, useState } from "react"
import jp from "jsonpath"
import JsonTreeNode from "@/components/json-tree-node"
/** /**
* Home page component that displays the main landing content. * Home page component that displays the main landing content.
*/ */
export default function Home() { export default function Home() {
const initialData = {
centre_id: "LON-01",
location: "London",
is_active: true,
staff_members: [
{ id: 101, name: "Alice", roles: ["Admin", "Manager"] },
{ id: 102, name: "Bob", roles: ["Developer"] },
],
config: {
colour_scheme: "Dark Mode",
retention_days: 30,
},
}
const [jsonInput, setJsonInput] = useState<string>(JSON.stringify(initialData, null, 2))
const [query, setQuery] = useState<string>("$.staff_members[*].name")
// 计算匹配结果
const result = useMemo(() => {
try {
const parsed = JSON.parse(jsonInput)
const nodes = jp.nodes(parsed, query)
return {
parsed,
matchedPaths: nodes.map((n) => jp.stringify(n.path)),
error: null,
}
} catch (e) {
return { parsed: null, matchedPaths: [], error: (e as Error).message }
}
}, [jsonInput, query])
return ( return (
<div className="space-y-8"> <div className="min-h-screen bg-slate-50 p-6 font-sans text-slate-900">
{/* Hero Section */} <div className="max-w-7xl mx-auto space-y-6">
<div className="text-center"> <header className="flex flex-col gap-2">
<h1 className="text-4xl font-bold text-gray-900 sm:text-5xl md:text-6xl"> <h1 className="text-2xl font-bold tracking-tight text-slate-800">
Welcome to OnixByte React Template TypeScript JSONPath Explorer
</h1> </h1>
<p className="mt-3 max-w-md mx-auto text-base text-gray-500 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl"> <p className="text-slate-500 text-sm">
A modern React application template with <b>TypeScript</b>, <b>Tailwind CSS</b>,{" "} Debug and visualises your JSONPath queries with real-time highlighting.
<b>Redux</b>, and <b>React Router</b> for seamless navigation.
</p> </p>
</div> </header>
{/* Features Grid */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="mt-12"> {/* 控制面板 */}
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"> <div className="flex flex-col gap-4">
{/* Feature 1 */} <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="bg-white overflow-hidden shadow rounded-lg"> <div className="bg-slate-50 px-4 py-2 border-b border-slate-200">
<div className="p-6"> <span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
<div className="flex items-center"> JSON Source
<div className="flex-shrink-0"> </span>
<svg </div>
className="h-8 w-8 text-blue-600" <textarea
fill="none" className="w-full h-100 p-4 font-mono text-sm outline-none focus:ring-2 focus:ring-indigo-500/20 transition-all resize-none"
viewBox="0 0 24 24" value={jsonInput}
stroke="currentColor"> onChange={(e) => setJsonInput(e.target.value)}
<path spellCheck={false}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/> />
</svg>
</div>
<div className="ml-4">
<h3 className="text-lg font-medium text-gray-900">Fast Development</h3>
</div>
</div>
<div className="mt-4">
<p className="text-sm text-gray-500">
Built with Vite for lightning-fast development experience and hot module
replacement.
</p>
</div>
</div>
</div> </div>
{/* Feature 2 */} <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<div className="bg-white overflow-hidden shadow rounded-lg"> <label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">
<div className="p-6"> JSONPath Expression
<div className="flex items-center"> </label>
<div className="flex-shrink-0"> <input
<svg type="text"
className="h-8 w-8 text-green-600" className="w-full p-3 font-mono text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm"
fill="none" value={query}
viewBox="0 0 24 24" onChange={(e) => setQuery(e.target.value)}
stroke="currentColor"> placeholder="e.g. $..roles"
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg>
</div>
<div className="ml-4">
<h3 className="text-lg font-medium text-gray-900">Type Safety</h3>
</div>
</div>
<div className="mt-4">
<p className="text-sm text-gray-500">
Full TypeScript support with strict type checking for better code quality and
developer experience.
</p>
</div>
</div> </div>
</div> </div>
{/* Feature 3 */} {/* 可视化面板 */}
<div className="bg-white overflow-hidden shadow rounded-lg"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
<div className="p-6"> <div className="bg-slate-50 px-4 py-2 border-b border-slate-200 flex justify-between items-center">
<div className="flex items-center"> <span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
<div className="flex-shrink-0"> Visualised Result
<svg </span>
className="h-8 w-8 text-purple-600" <span className="text-xs font-medium px-2 py-0.5 bg-indigo-100 text-indigo-700 rounded-full">
fill="none" {result.matchedPaths.length} matches
viewBox="0 0 24 24" </span>
stroke="currentColor"> </div>
<path
strokeLinecap="round" <div className="p-6 overflow-auto max-h-[580px] font-mono text-sm leading-relaxed">
strokeLinejoin="round" {result.error ? (
strokeWidth={2} <div className="bg-red-50 text-red-600 p-4 rounded-lg border border-red-100 text-xs">
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z" <strong>Error:</strong> {result.error}
</div>
) : (
<JsonTreeNode
data={result.parsed}
path={["$"]}
matchedPaths={result.matchedPaths}
/> />
</svg> )}
</div>
<div className="ml-4">
<h3 className="text-lg font-medium text-gray-900">Modern Styling</h3>
</div> </div>
</div> </div>
<div className="mt-4">
<p className="text-sm text-gray-500">
Tailwind CSS for utility-first styling with responsive design and modern UI
components.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Call to Action */}
<div className="bg-blue-50 rounded-lg p-6 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Ready to start building?</h2>
<p className="text-gray-600 mb-6">
Explore the navigation above to see React Router in action, or check out the source code
to understand the implementation.
</p>
<div className="flex justify-center space-x-4">
<Link
to="/about"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Learn More
</Link>
<Link
to="/contact"
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Get in Touch
</Link>
</div> </div>
</div> </div>
</div> </div>