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:
parent
60ddf0520e
commit
45dcbdaee9
14 changed files with 362 additions and 62 deletions
BIN
backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/database.cpython-313.pyc
Normal file
BIN
backend/app/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/main.cpython-313.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/app/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routes/__pycache__/folders.cpython-313.pyc
Normal file
BIN
backend/app/routes/__pycache__/folders.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routes/__pycache__/notes.cpython-313.pyc
Normal file
BIN
backend/app/routes/__pycache__/notes.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/notes.db
BIN
backend/notes.db
Binary file not shown.
113
frontend/package-lock.json
generated
113
frontend/package-lock.json
generated
|
|
@ -15,6 +15,7 @@
|
|||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"axios": "^1.13.2",
|
||||
"jszip": "^3.10.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.9.6",
|
||||
|
|
@ -60,7 +61,6 @@
|
|||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
@ -637,7 +637,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
|
|
@ -727,7 +726,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
|
|
@ -737,7 +735,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
|
||||
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
|
|
@ -1274,7 +1271,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
||||
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||
},
|
||||
|
|
@ -1639,7 +1635,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
|
|
@ -3161,7 +3156,6 @@
|
|||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
|
|
@ -3586,7 +3580,6 @@
|
|||
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -3597,7 +3590,6 @@
|
|||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
|
|
@ -3634,7 +3626,6 @@
|
|||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -3742,7 +3733,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
|
|
@ -3961,6 +3951,12 @@
|
|||
"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": {
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||
|
|
@ -4636,6 +4632,12 @@
|
|||
],
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
|
|
@ -4653,6 +4655,12 @@
|
|||
"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": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz",
|
||||
|
|
@ -4711,11 +4719,18 @@
|
|||
"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": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
|
|
@ -4781,6 +4796,18 @@
|
|||
"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": {
|
||||
"version": "4.1.5",
|
||||
"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",
|
||||
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"isomorphic.js": "^0.2.4"
|
||||
},
|
||||
|
|
@ -4817,6 +4845,15 @@
|
|||
"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": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
|
|
@ -6187,6 +6224,12 @@
|
|||
"integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
|
@ -6323,6 +6366,12 @@
|
|||
"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": {
|
||||
"version": "15.8.1",
|
||||
"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",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
|
|
@ -6373,7 +6421,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
|
@ -6600,6 +6662,12 @@
|
|||
"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": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
|
|
@ -6625,6 +6693,12 @@
|
|||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
|
||||
|
|
@ -6663,6 +6737,15 @@
|
|||
"integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==",
|
||||
"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": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||
|
|
@ -6700,8 +6783,7 @@
|
|||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
|
|
@ -6922,7 +7004,6 @@
|
|||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"axios": "^1.13.2",
|
||||
"jszip": "^3.10.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.9.6",
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@
|
|||
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
|
||||
import Home from "./pages/Home"; // existing home page
|
||||
import { MarkdownPage } from "./pages/Markdown";
|
||||
import { Import } from "./pages/Import";
|
||||
const App = () => (
|
||||
<BrowserRouter>
|
||||
{/* Simple nav – you can replace with your own UI later */}
|
||||
{/*<nav style={{ marginBottom: "1rem" }}>
|
||||
<BrowserRouter>
|
||||
{/* Simple nav – you can replace with your own UI later */}
|
||||
{/*<nav style={{ marginBottom: "1rem" }}>
|
||||
<Link to="/">Home</Link> | <Link to="/markdown">MD</Link>
|
||||
</nav>*/}
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/markdown" element={<MarkdownPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/markdown" element={<MarkdownPage />} />
|
||||
<Route path="/import" element={<Import />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -7,11 +7,15 @@ export const DroppableFolder = ({
|
|||
setSelectedFolder,
|
||||
selectedFolder,
|
||||
selectedNote,
|
||||
setCollapse,
|
||||
collapse,
|
||||
}: {
|
||||
folder: Partial<Folder>;
|
||||
setSelectedFolder: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
selectedFolder: number | null;
|
||||
selectedNote: NoteRead | null;
|
||||
setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
collapse: boolean;
|
||||
}) => {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: folder.id!,
|
||||
|
|
@ -34,6 +38,9 @@ export const DroppableFolder = ({
|
|||
>
|
||||
<i className="fadr fa-folder text-sm"></i>
|
||||
{folder.name}
|
||||
<div onClick={() => setCollapse(!collapse)} className="ml-auto">
|
||||
x
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
._mdxeditor-root-content-editable,
|
||||
.mdxeditor-root-contenteditable,
|
||||
div[contenteditable="true"] {
|
||||
color: var(--ctp-text) !important;
|
||||
color: var(--color-ctp-text) !important;
|
||||
}
|
||||
|
||||
/* Override prose specifically */
|
||||
.prose,
|
||||
.prose * {
|
||||
color: var(--ctp-text) !important;
|
||||
color: var(--color-ctp-text) !important;
|
||||
}
|
||||
|
||||
/* Override list markers */
|
||||
|
|
@ -21,21 +21,37 @@ div[contenteditable="true"] {
|
|||
.prose ol li::marker,
|
||||
ul li::marker,
|
||||
ol li::marker {
|
||||
color: var(--ctp-text) !important;
|
||||
color: var(--color-ctp-text) !important;
|
||||
}
|
||||
|
||||
.my-class {
|
||||
background-color: var(--ctp-mantle) !important;
|
||||
border-bottom: 1px solid var(--ctp-surface2) !important;
|
||||
background-color: var(--color-ctp-mantle) !important;
|
||||
border-bottom: 1px solid var(--color-ctp-surface2) !important;
|
||||
}
|
||||
|
||||
.my-class button {
|
||||
color: var(--ctp-text) !important;
|
||||
background-color: var(--ctp-surface0) !important;
|
||||
color: var(--color-ctp-text) !important;
|
||||
background-color: var(--color-ctp-surface0) !important;
|
||||
}
|
||||
|
||||
_listItemChecked_1tncs_73::before {
|
||||
background-color: var(--color-ctp-mauve);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
BoldItalicUnderlineToggles,
|
||||
codeBlockPlugin,
|
||||
codeMirrorPlugin,
|
||||
diffSourcePlugin,
|
||||
|
|
@ -13,8 +14,11 @@ import {
|
|||
sandpackPlugin,
|
||||
tablePlugin,
|
||||
thematicBreakPlugin,
|
||||
toolbarPlugin,
|
||||
UndoRedo,
|
||||
DiffSourceToggleWrapper,
|
||||
} from "@mdxeditor/editor";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
folderApi,
|
||||
FolderCreate,
|
||||
|
|
@ -155,32 +159,6 @@ function Home() {
|
|||
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 { active, over } = event;
|
||||
if (!over) return;
|
||||
|
|
@ -247,7 +225,17 @@ function Home() {
|
|||
|
||||
{/* Folder tree */}
|
||||
<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>
|
||||
|
||||
{/* Orphaned notes */}
|
||||
|
|
@ -291,9 +279,20 @@ function Home() {
|
|||
markdown={content}
|
||||
key={selectedNote?.id || "new"}
|
||||
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={[
|
||||
headingsPlugin(),
|
||||
toolbarPlugin({
|
||||
toolbarClassName: "toolbar",
|
||||
toolbarContents: () => (
|
||||
<>
|
||||
<UndoRedo />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<DiffSourceToggleWrapper />
|
||||
</>
|
||||
),
|
||||
}),
|
||||
|
||||
tablePlugin(),
|
||||
listsPlugin(),
|
||||
quotePlugin(),
|
||||
|
|
@ -307,12 +306,14 @@ function Home() {
|
|||
css: "CSS",
|
||||
python: "Python",
|
||||
typescript: "TypeScript",
|
||||
html: "HTML",
|
||||
},
|
||||
}),
|
||||
imagePlugin(),
|
||||
markdownShortcutPlugin(),
|
||||
diffSourcePlugin({
|
||||
viewMode: "rich-text",
|
||||
diffMarkdown: "boo",
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
|
|
@ -390,3 +391,65 @@ function 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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
130
frontend/src/pages/Import.tsx
Normal file
130
frontend/src/pages/Import.tsx
Normal 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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in a new issue