Removed notes and removed files that should be ignored.

This commit is contained in:
Jamitz 2025-12-30 22:36:48 +00:00
parent a62e2d744d
commit d1d436b020
19 changed files with 88 additions and 609 deletions

Binary file not shown.

View file

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><sqlb_project><db path="notes.db" readonly="0" foreign_keys="1" case_sensitive_like="0" temp_store="0" wal_autocheckpoint="1000" synchronous="2"/><attached/><window><main_tabs open="structure browser pragmas query" current="1"/></window><tab_structure><column_width id="0" width="300"/><column_width id="1" width="0"/><column_width id="2" width="100"/><column_width id="3" width="2087"/><column_width id="4" width="0"/><expanded_item id="0" parent="1"/><expanded_item id="1" parent="1"/><expanded_item id="2" parent="1"/><expanded_item id="3" parent="1"/></tab_structure><tab_browse><table title="user" custom_title="0" dock_id="1" table="4,4:mainuser"/><dock_state state="000000ff00000000fd00000001000000020000030e000004effc0100000001fb000000160064006f0063006b00420072006f007700730065003101000000000000030e0000012000ffffff000002a80000000000000004000000040000000800000008fc00000000"/><default_encoding codec=""/><browse_table_settings><table schema="main" name="folder" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_" freeze_columns="0"><sort/><column_widths><column index="1" value="23"/><column index="2" value="187"/><column index="3" value="64"/><column index="4" value="210"/><column index="5" value="53"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table><table schema="main" name="session" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_" freeze_columns="0"><sort/><column_widths><column index="1" value="23"/><column index="2" value="300"/><column index="3" value="53"/><column index="4" value="210"/><column index="5" value="210"/><column index="6" value="73"/><column index="7" value="300"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table><table schema="main" name="user" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_" freeze_columns="0"><sort/><column_widths><column index="1" value="23"/><column index="2" value="67"/><column index="3" value="124"/><column index="4" value="300"/><column index="5" value="179"/><column index="6" value="210"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table></browse_table_settings></tab_browse><tab_sql><sql name="SQL 1"></sql><current_tab id="0"/></tab_sql></sqlb_project>

View file

@ -151,7 +151,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",
@ -728,7 +727,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",
@ -818,7 +816,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"
}
@ -828,7 +825,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",
@ -979,7 +975,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -1026,7 +1021,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@ -1503,7 +1497,6 @@
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
@ -1557,7 +1550,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"
},
@ -1922,7 +1914,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"
}
@ -3510,7 +3501,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",
@ -3861,7 +3851,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz",
"integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.90.12"
},
@ -3985,7 +3974,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.12.1.tgz",
"integrity": "sha512-dn5uTnsTUjMze26iRhcus8+2auW9+/vOpk6suXg/lhBp+UzOM+EALKE3S5086ANJNgBh1PDHoBX+r1T7wEmheg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@ -4208,7 +4196,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.12.1.tgz",
"integrity": "sha512-v3WC9TR8QRVwmubuKjUplAXeTzTq2hiVKGHBbW15LTqqfsEJwt1YHUl/Sc+pSAeJfY7th5wheNfZFCsCBCW3qg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@ -4327,7 +4314,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.12.1.tgz",
"integrity": "sha512-Xtg2Ot3oebg6+ponJ3yp8VcxPtdaHaub62Eoh8DKvBexyfqp+lMDtOpJZXA9NImVG3gKn+5EAIq8kx5AtrVlJQ==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@ -4342,7 +4328,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.12.1.tgz",
"integrity": "sha512-YGv8uZrTraXzB3DPQYsyIB90Girx5QZdZOBSDj0R2bWSXc2Huqdb9PaulXqDQjEv/dp9x6w6+Q2VNIagCPUQwA==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
@ -4436,7 +4421,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@ -4583,7 +4569,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@ -4593,7 +4578,6 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -4707,7 +4691,6 @@
"integrity": "sha512-sxSyJMaKp45zI0u+lHrPuZM1ZJQ8FaVD35k+UxVrha1yyvQ+TZuUYllUixwvQXlB7ixoDc7skf3lQPopZIvaQw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/utils": "4.0.15",
"fflate": "^0.8.2",
@ -4743,7 +4726,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"
},
@ -4792,6 +4774,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@ -4802,6 +4785,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@ -4941,7 +4925,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@ -5401,7 +5384,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dot-case": {
"version": "3.0.4",
@ -6224,6 +6208,7 @@
"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"
@ -6272,7 +6257,6 @@
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@ -6379,6 +6363,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"
},
@ -8111,7 +8096,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -8140,6 +8124,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@ -8293,7 +8278,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT",
"peer": true,
"dependencies": {
"orderedmap": "^2.0.0"
}
@ -8323,7 +8307,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
@ -8384,7 +8367,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
@ -8421,7 +8403,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"
},
@ -8443,7 +8424,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"
@ -8938,8 +8918,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",
@ -9128,7 +9107,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -9359,7 +9337,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",
@ -9435,7 +9412,6 @@
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.15",
"@vitest/mocker": "4.0.15",
@ -9984,7 +9960,6 @@
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",

View file

@ -1,89 +0,0 @@
import { client } from "./client";
import { components } from "@/types/api";
import { encryptString, decryptTagTree } from "./encryption";
import { useAuthStore } from "../stores/authStore";
import { CamelCasedPropertiesDeep } from "type-fest";
export type Tag = CamelCasedPropertiesDeep<components["schemas"]["Tag"]>;
export type TagTreeNode = CamelCasedPropertiesDeep<
components["schemas"]["TagTreeNode"]
>;
export type TagCreate = CamelCasedPropertiesDeep<
components["schemas"]["TagCreate"]
>;
export type TagRead = CamelCasedPropertiesDeep<
components["schemas"]["TagRead"]
>;
export type TagTreeResponse = CamelCasedPropertiesDeep<
components["schemas"]["TagTreeResponse"]
>;
const fetchTags = async () => {
const encryptionKey = useAuthStore.getState().encryptionKey;
if (!encryptionKey) throw new Error("Not authenticated");
const response = await client.GET("/api/tags/tree", {});
if (response.error) throw new Error("Failed to fetch tags");
if (!response.data) throw new Error("No data returned");
const data = response.data;
const tags = decryptTagTree(data.tags as any, encryptionKey);
return tags;
};
const createTag = async (tag: TagCreate): Promise<TagTreeNode> => {
const encryptionKey = useAuthStore.getState().encryptionKey;
if (!encryptionKey) throw new Error("Not authenticated");
const tagName = await encryptString(tag.name, encryptionKey);
// Use the exact structure from TagCreate schema
const { data, error } = await client.POST("/api/tags/", {
body: {
name: tagName,
parentId: tag.parentId || null,
},
});
if (error) throw new Error("Failed to create tag");
return data as unknown as TagTreeNode;
};
const addTagToNote = async (tagId: number, noteId: number) => {
const { data, error } = await client.POST(
"/api/tags/note/{note_id}/tag/{tag_id}",
{
params: {
path: {
note_id: noteId,
tag_id: tagId,
},
},
},
);
if (error) throw new Error("Failed to add tag to note");
return data;
};
const deleteTag = async (tagId: number) => {
const { error } = await client.DELETE("/api/tags/{tag_id}", {
params: {
path: {
tag_id: tagId,
},
},
});
if (error) throw new Error("Failed to delete tag");
};
export const tagsApi = {
list: fetchTags,
create: createTag,
addToNote: addTagToNote,
delete: deleteTag,
};

View file

@ -1,62 +0,0 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Tag, TagCreate, TagRead, tagsApi } from "@/api/tags";
import { useAuthStore } from "@/stores/authStore";
import { DecryptedTagNode } from "@/api/encryption";
export const useTagTree = () => {
const { encryptionKey } = useAuthStore();
return useQuery({
queryKey: ["tags", "tree"],
queryFn: tagsApi.list,
enabled: !!encryptionKey,
});
};
export const useCreateTag = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (tag: TagCreate) => tagsApi.create(tag),
onMutate: async (newTag) => {
await queryClient.cancelQueries({ queryKey: ["tags", "tree"] });
const previousTags = queryClient.getQueryData(["tags", "tree"]);
queryClient.setQueryData(["tags", "tree"], (old: Tag[] | undefined) => {
const tempTag: DecryptedTagNode = {
id: -Date.now(),
name: newTag.name,
parentId: newTag.parentId,
parentPath: "",
createdAt: new Date().toISOString(),
children: [],
};
return [...(old || []), tempTag];
});
return { previousTags };
},
onError: (err, newTag, context) => {
queryClient.setQueryData(["tags", "tree"], context?.previousTags);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tags", "tree"] });
},
});
};
export const useAddTagToNote = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ tagId, noteId }: { tagId: number; noteId: number }) =>
tagsApi.addToNote(tagId, noteId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tags", "tree"] });
queryClient.invalidateQueries({ queryKey: ["folders", "tree"] });
},
});
};

View file

@ -7,10 +7,8 @@ import { Login } from "../Login";
import { TiptapEditor } from "../TipTap";
import { Sidebar } from "./components/sidebar/SideBar";
import { StatusIndicator } from "./components/StatusIndicator";
import { useAddTagToNote, useCreateTag, useTagTree } from "@/hooks/useTags";
import { useFolderTree, useUpdateNote } from "@/hooks/useFolders";
import { Note, NoteRead } from "@/api/notes";
import { DecryptedTagNode } from "@/api/encryption";
import { useUpdateNote } from "@/hooks/useFolders";
import { NoteRead } from "@/api/notes";
// @ts-ignore
import XmarkIcon from "@/assets/fontawesome/svg/xmark.svg?react";
// @ts-ignore
@ -31,7 +29,6 @@ function Home() {
const { showModal, setUpdating, selectedNote } = useUIStore();
const newFolderRef = useRef<HTMLInputElement>(null);
const folderTree = useFolderTree();
const updateNoteMutation = useUpdateNote();
// Sync editingNote with selectedNote when selection changes
@ -138,23 +135,6 @@ function Home() {
onChange={(e) => setTitle(e.target.value)}
className="w-full p-4 pb-0 text-3xl font-semibold bg-transparent focus:outline-none border-transparent focus:border-ctp-mauve transition-colors placeholder:text-ctp-overlay0 text-ctp-text"
/>
<TagSelector
editingNote={editingNote}
setEditingNote={setEditingNote}
/>
<div className="px-4 py-2 border-ctp-surface2 flex items-center gap-2 flex-wrap">
{editingNote?.tags &&
editingNote.tags.map((tag) => (
<button
onClick={() => null}
key={tag.id}
className="bg-ctp-surface0 hover:bg-ctp-surface1 px-2 py-0.5 text-sm rounded-full transition-colors"
>
{tag.parentId && "..."}
{tag.name}
</button>
))}
</div>
<TiptapEditor
key={editingNote?.id}
@ -197,310 +177,7 @@ const Modal = () => {
<XmarkIcon className="w-5 h-5 fill-ctp-overlay0 group-hover:fill-ctp-text transition-colors" />
</button>
<Login />
{/*<TagSelector />*/}
</motion.div>
</motion.div>
);
};
export const TagSelector = ({
editingNote,
setEditingNote,
}: {
editingNote: NoteRead | null;
setEditingNote: (note: NoteRead | null) => void;
}) => {
const [value, setValue] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const { data: tagTree, isLoading, error } = useTagTree();
const createTag = useCreateTag();
const addTagToNote = useAddTagToNote();
const [expanded, setExpanded] = useState(false);
// Parse path from input (using > as separator)
const parsedPath = value.includes(">")
? value
.split(">")
.map((part) => part.trim())
.filter(Boolean)
: null;
// Filter existing tags based on search
const filteredTags = tagTree
? tagTree.filter((tag) => {
if (value === "") return false;
// Don't show filtered tags if user is typing a path
if (parsedPath) return false;
return (tag.name + tag.parentPath)
.toLowerCase()
.includes(value.toLowerCase());
})
: [];
// Close when clicking outside the entire component
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
handleClose();
}
};
if (expanded) {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [expanded]);
// Focus input when expanded
useEffect(() => {
if (expanded && inputRef.current) {
inputRef.current.focus();
}
}, [expanded]);
const handleEnter = () => {
createTag.mutate(
{ name: value },
{
onSuccess: (createdTag) => {
if (editingNote && createdTag.id) {
addTagToNote.mutate(
{
tagId: createdTag.id,
noteId: editingNote.id,
},
{
onSuccess: () => {
setEditingNote({
...editingNote,
tags: [
...(editingNote.tags || []),
{ ...createdTag, name: value },
],
});
},
},
);
}
},
},
);
};
const handleSelectExistingTag = (tag: DecryptedTagNode) => {
if (editingNote && tag.id) {
addTagToNote.mutate(
{
tagId: tag.id,
noteId: editingNote.id,
},
{
onSuccess: () => {
setEditingNote({
...editingNote,
tags: [
...(editingNote.tags || []),
{ id: tag.id, name: tag.name, parentId: tag.parentId },
],
});
setValue("");
setExpanded(false);
},
},
);
}
};
function handleClose() {
// If there's a value, submit it; otherwise just collapse
if (value.trim()) {
handleEnter();
}
setValue("");
setExpanded(false);
}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
e.preventDefault();
handleClose();
} else if (e.key === "Escape") {
// Abort without submitting
setValue("");
setExpanded(false);
}
}
return (
<div className="relative ml-4" ref={containerRef}>
<div className="inline-flex bg-ctp-surface0 rounded-full px-1 py-0.5 items-center">
<button
type="button"
onClick={() => setExpanded(true)}
className={`text-xs bg-transparent flex items-center justify-center
focus:outline-none transition-all duration-200 ease-out whitespace-nowrap text-ctp-subtext0
${expanded ? "opacity-0 w-0 px-0 pointer-events-none overflow-hidden" : "opacity-100 px-0.5"}
`}
>
<PlusIcon className="w-3 h-3 mr-1 fill-ctp-subtext0" />
{"Add"}
</button>
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKey}
placeholder={"Type here…"}
className={`text-xs z-30
bg-transparent px-1.5
focus:outline-none focus:ring-0
transition-all duration-200 ease-out
${expanded ? "w-32 opacity-100" : "w-0 opacity-0 pointer-events-none"}
`}
/>
</div>
{/* Dropdown */}
{expanded && value && (
<div className="absolute top-full left-0 mt-1 bg-ctp-surface0 rounded-lg shadow-lg border border-ctp-surface2 overflow-hidden min-w-[200px] max-w-[300px] z-20">
{/* Show hierarchical preview if path is being typed */}
{parsedPath && parsedPath.length > 0 && (
<div className="p-3 border-b border-ctp-surface2">
<div className="text-xs text-ctp-overlay1 mb-2">
Create tag hierarchy:
</div>
<div className="flex flex-col gap-1">
{parsedPath.map((part, index) => (
<div
key={index}
className="flex items-center text-xs text-ctp-text"
style={{ paddingLeft: `${index * 12}px` }}
>
{index > 0 && (
<span className="text-ctp-overlay0 mr-2"></span>
)}
<span className="bg-ctp-surface1 px-2 py-0.5 rounded">
{part}
</span>
</div>
))}
</div>
<div className="text-xs text-ctp-overlay0 mt-2">
Press Enter to create
</div>
</div>
)}
{/* Show filtered existing tags */}
{!parsedPath && filteredTags.length > 0 && (
<div className="max-h-[200px] overflow-y-auto">
{filteredTags.map((tag) => (
<TagTreeClickable
key={tag.id}
tag={tag}
onSelect={handleSelectExistingTag}
/>
))}
</div>
)}
{/* Show "create new" option for simple tags */}
{!parsedPath && filteredTags.length === 0 && (
<div className="p-2 text-xs text-ctp-overlay1">
Press Enter to create "{value}"
</div>
)}
</div>
)}
</div>
);
};
// Clickable version of TagTree for selection
export const TagTreeClickable = ({
tag,
depth = 0,
onSelect,
}: {
tag: DecryptedTagNode;
depth?: number;
onSelect: (tag: DecryptedTagNode) => void;
}) => {
const [collapse, setCollapse] = useState(false);
return (
<div key={tag.id} className="flex flex-col">
<button
onClick={() => onSelect(tag)}
className="flex items-center px-3 py-2 hover:bg-ctp-surface1 transition-colors text-left text-xs"
style={{ paddingLeft: `${12 + depth * 16}px` }}
>
<span className="text-ctp-text">{tag.name}</span>
{tag.parentPath && (
<span className="ml-2 text-ctp-overlay0 text-[10px]">
{tag.parentPath}
</span>
)}
</button>
{/* Show children */}
{tag.children && tag.children.length > 0 && (
<div>
{tag.children.map((child) => (
<TagTreeClickable
key={child.id}
tag={child}
depth={depth + 1}
onSelect={onSelect}
/>
))}
</div>
)}
</div>
);
};
export const TagTree = ({
tag,
depth = 0,
}: {
tag: DecryptedTagNode;
depth?: number;
}) => {
const [collapse, setCollapse] = useState(false);
return (
<div
key={tag.id}
className="flex flex-col relative bg-ctp-surface0 pt-3 -translate-y-3 w-[136px]"
>
<div onClick={() => setCollapse(!collapse)}>{tag.name}</div>
<AnimatePresence>
{collapse && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className="overflow-hidden flex flex-col"
>
{/* The line container */}
<div className="ml-2 pl-3 border-l border-ctp-surface2">
{/* Child tags */}
{tag.children.map((child) => (
<TagTree key={child.id} tag={child} depth={depth + 1} />
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

View file

@ -20,7 +20,6 @@ import { FolderTree } from "./subcomponents/FolderTree.tsx";
import { SidebarHeader } from "./subcomponents/SideBarHeader.tsx";
import { useAuthStore } from "@/stores/authStore.ts";
import { useUIStore } from "@/stores/uiStore.ts";
import { TagSelector } from "../../Home.tsx";
import {
useCreateFolder,
useFolderTree,
@ -42,8 +41,7 @@ export const Sidebar = () => {
const { encryptionKey } = useAuthStore();
const { setSideBarResize, sideBarResize, sideBarView, setSideBarView } =
useUIStore();
const { setSideBarResize, sideBarResize } = useUIStore();
useEffect(() => {
if (newFolder && newFolderRef.current) {
newFolderRef.current.focus();
@ -174,7 +172,6 @@ export const Sidebar = () => {
>
<SidebarHeader setNewFolder={setNewFolder} />
<div className="flex-1 overflow-y-auto bg-ctp-mantle border-r border-ctp-surface2">
{sideBarView == "folders" ? (
<>
<div
className="w-full p-4 sm:block hidden"
@ -223,11 +220,7 @@ export const Sidebar = () => {
<>
<div className="flex flex-col gap-1">
{folderTree?.folders.map((folder) => (
<FolderTree
key={folder.id}
folder={folder}
depth={0}
/>
<FolderTree key={folder.id} folder={folder} depth={0} />
))}
</div>
@ -258,11 +251,6 @@ export const Sidebar = () => {
)}
</DragOverlay>
</>
) : (
<div className="w-full p-4 sm:block hidden">
<TagSelector />
</div>
)}
</div>
</div>
</div>

View file

@ -14,7 +14,7 @@ export const SidebarHeader = ({
}: {
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
}) => {
const { setSideBarView, sideBarView, selectedFolder } = useUIStore();
const { selectedFolder } = useUIStore();
const createNote = useCreateNote();
const handleCreate = async () => {
createNote.mutate({
@ -33,15 +33,6 @@ export const SidebarHeader = ({
>
<FolderPlusIcon className="w-5 h-5 group-hover:fill-ctp-base transition-all duration-200 fill-ctp-mauve" />
</button>
<button
onClick={() =>
setSideBarView(sideBarView == "tags" ? "folders" : "tags")
}
className={`${sideBarView === "tags" ? "bg-ctp-mauve/20" : ""} hover:bg-ctp-mauve active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md`}
title="Tags"
>
<TagsIcon className="w-5 h-5 group-hover:fill-ctp-base transition-all duration-200 fill-ctp-mauve" />
</button>
<button
onClick={handleCreate}
className="hover:bg-ctp-mauve active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"