Add ZIP import page and folder collapse UI

- Add a ZIP-based import page that builds a folder/note tree from a ZIP
  and creates items via the API
- Introduce collapsible folder UI with RenderFolder and updates to
  DroppableFolder to support collapsing
- Wire a new /import route into App and add jszip as a frontend
  dependency
This commit is contained in:
James Fitzsimons 2025-11-26 21:38:58 +00:00
parent 60ddf0520e
commit 45dcbdaee9
14 changed files with 362 additions and 62 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -15,6 +15,7 @@
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2", "axios": "^1.13.2",
"jszip": "^3.10.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
@ -60,7 +61,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -637,7 +637,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@ -727,7 +726,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@ -737,7 +735,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@ -1274,7 +1271,6 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-common-types": "7.1.0" "@fortawesome/fontawesome-common-types": "7.1.0"
}, },
@ -1639,7 +1635,6 @@
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.3.0" "@lezer/common": "^1.3.0"
} }
@ -3161,7 +3156,6 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0", "@svgr/babel-preset": "8.1.0",
@ -3586,7 +3580,6 @@
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -3597,7 +3590,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -3634,7 +3626,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3742,7 +3733,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001754",
@ -3961,6 +3951,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "8.3.6", "version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@ -4636,6 +4632,12 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -4653,6 +4655,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/intersection-observer": { "node_modules/intersection-observer": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz", "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz",
@ -4711,11 +4719,18 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isomorphic.js": { "node_modules/isomorphic.js": {
"version": "0.2.5", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "GitHub Sponsors ❤", "type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
@ -4781,6 +4796,18 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@ -4801,6 +4828,7 @@
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"isomorphic.js": "^0.2.4" "isomorphic.js": "^0.2.4"
}, },
@ -4817,6 +4845,15 @@
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
} }
}, },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.2", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@ -6187,6 +6224,12 @@
"integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -6323,6 +6366,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -6351,7 +6400,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@ -6373,7 +6421,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@ -6537,6 +6584,21 @@
} }
} }
}, },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -6600,6 +6662,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@ -6625,6 +6693,12 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/snake-case": { "node_modules/snake-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
@ -6663,6 +6737,15 @@
"integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==", "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/stringify-entities": { "node_modules/stringify-entities": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@ -6700,8 +6783,7 @@
"version": "4.1.17", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.0", "version": "2.3.0",
@ -6922,7 +7004,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",

View file

@ -15,6 +15,7 @@
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2", "axios": "^1.13.2",
"jszip": "^3.10.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",

View file

@ -2,6 +2,7 @@
import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home"; // existing home page import Home from "./pages/Home"; // existing home page
import { MarkdownPage } from "./pages/Markdown"; import { MarkdownPage } from "./pages/Markdown";
import { Import } from "./pages/Import";
const App = () => ( const App = () => (
<BrowserRouter> <BrowserRouter>
{/* Simple nav you can replace with your own UI later */} {/* Simple nav you can replace with your own UI later */}
@ -12,6 +13,7 @@ const App = () => (
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/markdown" element={<MarkdownPage />} /> <Route path="/markdown" element={<MarkdownPage />} />
<Route path="/import" element={<Import />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
); );

View file

@ -7,11 +7,15 @@ export const DroppableFolder = ({
setSelectedFolder, setSelectedFolder,
selectedFolder, selectedFolder,
selectedNote, selectedNote,
setCollapse,
collapse,
}: { }: {
folder: Partial<Folder>; folder: Partial<Folder>;
setSelectedFolder: React.Dispatch<React.SetStateAction<number | null>>; setSelectedFolder: React.Dispatch<React.SetStateAction<number | null>>;
selectedFolder: number | null; selectedFolder: number | null;
selectedNote: NoteRead | null; selectedNote: NoteRead | null;
setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
collapse: boolean;
}) => { }) => {
const { isOver, setNodeRef } = useDroppable({ const { isOver, setNodeRef } = useDroppable({
id: folder.id!, id: folder.id!,
@ -34,6 +38,9 @@ export const DroppableFolder = ({
> >
<i className="fadr fa-folder text-sm"></i> <i className="fadr fa-folder text-sm"></i>
{folder.name} {folder.name}
<div onClick={() => setCollapse(!collapse)} className="ml-auto">
x
</div>
</div> </div>
</div> </div>
); );

View file

@ -7,13 +7,13 @@
._mdxeditor-root-content-editable, ._mdxeditor-root-content-editable,
.mdxeditor-root-contenteditable, .mdxeditor-root-contenteditable,
div[contenteditable="true"] { div[contenteditable="true"] {
color: var(--ctp-text) !important; color: var(--color-ctp-text) !important;
} }
/* Override prose specifically */ /* Override prose specifically */
.prose, .prose,
.prose * { .prose * {
color: var(--ctp-text) !important; color: var(--color-ctp-text) !important;
} }
/* Override list markers */ /* Override list markers */
@ -21,21 +21,37 @@ div[contenteditable="true"] {
.prose ol li::marker, .prose ol li::marker,
ul li::marker, ul li::marker,
ol li::marker { ol li::marker {
color: var(--ctp-text) !important; color: var(--color-ctp-text) !important;
} }
.my-class { .my-class {
background-color: var(--ctp-mantle) !important; background-color: var(--color-ctp-mantle) !important;
border-bottom: 1px solid var(--ctp-surface2) !important; border-bottom: 1px solid var(--color-ctp-surface2) !important;
} }
.my-class button { .my-class button {
color: var(--ctp-text) !important; color: var(--color-ctp-text) !important;
background-color: var(--ctp-surface0) !important; background-color: var(--color-ctp-surface0) !important;
}
_listItemChecked_1tncs_73::before {
background-color: var(--color-ctp-mauve);
} }
.mdxeditor-popup-container > * { .mdxeditor-popup-container > * {
background-color: var(--ctp-base) !important; background-color: var(--color-ctp-base) !important;
} }
.toolbar {
background-color: var(--color-ctp-crust) !important;
}
._listItemChecked_1tncs_73::before {
--accentSolid: var(--color-ctp-mauve) !important;
border-color: var(--color-ctp-mauve-900) !important;
border: 2px;
}
._listItemChecked_1tncs_73::after {
border-color: var(--color-ctp-mauve-900) !important;
}

View file

@ -1,4 +1,5 @@
import { import {
BoldItalicUnderlineToggles,
codeBlockPlugin, codeBlockPlugin,
codeMirrorPlugin, codeMirrorPlugin,
diffSourcePlugin, diffSourcePlugin,
@ -13,8 +14,11 @@ import {
sandpackPlugin, sandpackPlugin,
tablePlugin, tablePlugin,
thematicBreakPlugin, thematicBreakPlugin,
toolbarPlugin,
UndoRedo,
DiffSourceToggleWrapper,
} from "@mdxeditor/editor"; } from "@mdxeditor/editor";
import { useEffect, useRef, useState } from "react"; import { SetStateAction, useEffect, useRef, useState } from "react";
import { import {
folderApi, folderApi,
FolderCreate, FolderCreate,
@ -155,32 +159,6 @@ function Home() {
setContent(""); setContent("");
}; };
const renderFolder = (folder: FolderTreeNode, depth: number = 0) => (
<div
key={folder.id}
className="flex flex-col"
style={{ marginLeft: depth > 0 ? "1.5rem" : "0" }}
>
<DroppableFolder
folder={folder}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
/>
<div className="flex flex-col gap-0.5 ml-6">
{folder.notes.map((note) => (
<DraggableNote
key={note.id}
note={note}
selectNote={selectNote}
selectedNote={selectedNote}
/>
))}
</div>
{folder.children.map((child) => renderFolder(child, depth + 1))}
</div>
);
const handleDragEnd = async (event: DragEndEvent) => { const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
if (!over) return; if (!over) return;
@ -247,7 +225,17 @@ function Home() {
{/* Folder tree */} {/* Folder tree */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{folderTree?.folders.map((folder) => renderFolder(folder))} {folderTree?.folders.map((folder) => (
<RenderFolder
key={folder.id}
folder={folder}
depth={0}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
selectNote={selectNote}
/>
))}
</div> </div>
{/* Orphaned notes */} {/* Orphaned notes */}
@ -291,9 +279,20 @@ function Home() {
markdown={content} markdown={content}
key={selectedNote?.id || "new"} key={selectedNote?.id || "new"}
onChange={setContent} onChange={setContent}
className="prose prose-invert max-w-none text-ctp-text h-full" className="prose prose-invert max-w-none text-ctp-text h-full dark-editor dark-mode"
plugins={[ plugins={[
headingsPlugin(), headingsPlugin(),
toolbarPlugin({
toolbarClassName: "toolbar",
toolbarContents: () => (
<>
<UndoRedo />
<BoldItalicUnderlineToggles />
<DiffSourceToggleWrapper />
</>
),
}),
tablePlugin(), tablePlugin(),
listsPlugin(), listsPlugin(),
quotePlugin(), quotePlugin(),
@ -307,12 +306,14 @@ function Home() {
css: "CSS", css: "CSS",
python: "Python", python: "Python",
typescript: "TypeScript", typescript: "TypeScript",
html: "HTML",
}, },
}), }),
imagePlugin(), imagePlugin(),
markdownShortcutPlugin(), markdownShortcutPlugin(),
diffSourcePlugin({ diffSourcePlugin({
viewMode: "rich-text", viewMode: "rich-text",
diffMarkdown: "boo",
}), }),
]} ]}
/> />
@ -390,3 +391,65 @@ function Home() {
} }
export default Home; export default Home;
interface RenderFolderProps {
folder: FolderTreeNode;
depth?: number;
setSelectedFolder: React.Dispatch<SetStateAction<number | null>>;
selectedFolder: number | null;
selectedNote: NoteRead | null;
selectNote: (note: NoteRead) => void;
}
const RenderFolder = ({
folder,
depth = 0,
setSelectedFolder,
selectedFolder,
selectedNote,
selectNote,
}: RenderFolderProps) => {
const [collapse, setCollapse] = useState(false);
return (
<div
key={folder.id}
className="flex flex-col"
style={{ marginLeft: depth > 0 ? "1.5rem" : "0" }}
>
<DroppableFolder
folder={folder}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
setCollapse={setCollapse}
collapse={collapse}
/>
{collapse && (
<>
<div className="flex flex-col gap-0.5 ml-6">
{folder.notes.map((note) => (
<DraggableNote
key={note.id}
note={note}
selectNote={selectNote}
selectedNote={selectedNote}
/>
))}
</div>
{folder.children.map((child) => (
<RenderFolder
key={child.id}
folder={child}
depth={depth + 1}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
selectNote={selectNote}
/>
))}
</>
)}
</div>
);
};

View file

@ -0,0 +1,130 @@
import { useState } from "react";
import JSZip from "jszip";
import { folderApi } from "../api/folders";
import { notesApi } from "../api/notes";
export const Import = () => {
const [file, setFile] = useState<File | null>(null);
// Recursive function to create folders and notes
const createFromStructure = async (
structure: any,
parentFolderId: number | null = null,
): Promise<void> => {
for (const [name, item] of Object.entries(structure)) {
if (item.type === "folder") {
// Create the folder
const { data: newFolder } = await folderApi.create({
name: name,
parent_id: parentFolderId,
});
console.log(`Created folder: ${name} (id: ${newFolder.id})`);
// Recursively process children
if (item.children && Object.keys(item.children).length > 0) {
await createFromStructure(item.children, newFolder.id);
}
} else if (item.type === "file") {
// Parse the markdown file
const fileName = name.replace(".md", "");
const { title, content } = parseFrontmatter(item.content, fileName);
// Create the note
await notesApi.create({
title,
content,
folder_id: parentFolderId,
encrypted: false,
});
console.log(`Created note: ${title} in folder ${parentFolderId}`);
}
}
};
// Helper to parse frontmatter (if you used it in export)
const parseFrontmatter = (markdown: string, defaultTitle: string) => {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = markdown.match(frontmatterRegex);
if (match) {
const frontmatter = match[1];
const content = match[2].trim();
const titleMatch = frontmatter.match(/title:\s*(.+)/);
const title = titleMatch ? titleMatch[1].trim() : defaultTitle;
return { title, content };
}
return { title: defaultTitle, content: markdown };
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (!selectedFile) return;
setFile(selectedFile);
try {
const zip = new JSZip();
const contents = await zip.loadAsync(selectedFile);
const structure: any = {};
console.log("Files in zip:");
// Build structure (your existing code)
for (const [relativePath, file] of Object.entries(contents.files)) {
if (relativePath.includes(".md")) {
const splitPath = relativePath.split("/");
let current = structure;
for (let i = 0; i < splitPath.length; i++) {
const part = splitPath[i];
if (i === splitPath.length - 1) {
if (part.includes(".md")) {
const content = await file.async("string");
current[part] = {
type: "file",
content: content,
};
}
} else {
if (!current[part]) {
current[part] = {
type: "folder",
children: {},
};
}
current = current[part].children;
}
}
}
}
console.log("Structure:", structure);
// Create folders and notes from structure
await createFromStructure(structure);
console.log("Import complete!");
// Optional: Show success message or redirect
alert("Import successful!");
} catch (error) {
console.error("Error processing zip:", error);
alert("Import failed: " + error.message);
}
};
return (
<>
{file && <div>Selected: {file.name}</div>}
<input
type="file"
accept=".zip"
onChange={handleFileChange} // Remove value prop
/>
</>
);
};