Merge pull request #11 from jamitz440/feature/add-tags

Feature/add tags
This commit is contained in:
jamitz440 2025-12-18 18:13:21 +00:00 committed by GitHub
commit 03b71c2b64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 493 additions and 96 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
node_modules
frontend/src/assets/fontawesome/svg/*
frontend/src/assets/fontawesome/svg/0.svg
*.db
.zed/settings.json

View file

@ -2,7 +2,7 @@ import secrets
from datetime import datetime, timedelta
from typing import Optional
import bcrypt # Use bcrypt directly instead of passlib
import bcrypt
from fastapi import Cookie, Depends, HTTPException, Request, status
from sqlmodel import Session, select
@ -11,7 +11,6 @@ from app.models import Session as SessionModel
from app.models import User
# Password hashing with bcrypt directly
def hash_password(password: str) -> str:
password_bytes = password.encode("utf-8")
salt = bcrypt.gensalt()
@ -25,12 +24,11 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
return bcrypt.checkpw(password_bytes, hashed_bytes)
# Session management
def create_session(
user_id: int, request: Request, db: Session, expires_in_days: int = 30
) -> str:
session_id = secrets.token_urlsafe(32)
expires_at = datetime.utcnow() + timedelta(days=expires_in_days)
expires_at = datetime.now() + timedelta(days=expires_in_days)
db_session = SessionModel(
session_id=session_id,
@ -53,13 +51,12 @@ def get_session_user(session_id: Optional[str], db: Session) -> Optional[User]:
select(SessionModel).where(SessionModel.session_id == session_id)
).first()
if not session or session.expires_at < datetime.utcnow():
if not session or session.expires_at < datetime.now():
return None
return session.user
# Dependency for protected routes
async def require_auth(
session_id: Optional[str] = Cookie(None), db: Session = Depends(get_session)
) -> User:

View file

@ -2,7 +2,7 @@ from fastapi import FastAPI # type: ignore
from fastapi.middleware.cors import CORSMiddleware # type:ignore
from app.database import create_db_and_tables
from app.routes import auth, folders, notes
from app.routes import auth, folders, notes, tags
app = FastAPI(title="Notes API")
@ -24,6 +24,7 @@ def on_startup():
app.include_router(notes.router, prefix="/api")
app.include_router(folders.router, prefix="/api")
app.include_router(auth.router, prefix="/api")
app.include_router(tags.router, prefix="/api")
@app.get("/")

View file

@ -4,26 +4,27 @@ from typing import List, Optional
from sqlmodel import Field, Relationship, SQLModel # type: ignore
class User(SQLModel, table=True):
class User(SQLModel, table=True): # type: ignore
id: Optional[int] = Field(default=None, primary_key=True)
username: str = Field(unique=True, index=True)
email: str = Field(unique=True, index=True)
hashed_password: str
salt: str
wrapped_master_key: str
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=datetime.now)
# Add relationships to existing models
notes: List["Note"] = Relationship(back_populates="user")
folders: List["Folder"] = Relationship(back_populates="user")
sessions: List["Session"] = Relationship(back_populates="user")
tags: List["Tag"] = Relationship(back_populates="user")
class Session(SQLModel, table=True):
class Session(SQLModel, table=True): # type: ignore
id: Optional[int] = Field(default=None, primary_key=True)
session_id: str = Field(unique=True, index=True)
user_id: int = Field(foreign_key="user.id")
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=datetime.now)
expires_at: datetime
ip_address: Optional[str] = None
user_agent: Optional[str] = None
@ -35,7 +36,7 @@ class Folder(SQLModel, table=True): # type: ignore
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(max_length=255)
parent_id: Optional[int] = Field(default=None, foreign_key="folder.id")
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=datetime.now)
user_id: int = Field(foreign_key="user.id")
# Relationships
@ -47,20 +48,73 @@ class Folder(SQLModel, table=True): # type: ignore
user: User = Relationship(back_populates="folders")
class NoteTag(SQLModel, table=True): #type: ignore
note_id: int = Field(foreign_key="note.id", primary_key=True)
tag_id: int = Field(foreign_key="tag.id", primary_key=True)
class Tag(SQLModel, table=True): # type: ignore
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(max_length=255)
parent_id: Optional[int] = Field(default=None, foreign_key="tag.id")
user_id: int = Field(foreign_key="user.id")
created_at: datetime = Field(default_factory=datetime.now)
# Relationships
user: User = Relationship(back_populates="tags")
parent: Optional["Tag"] = Relationship(
back_populates="children",
sa_relationship_kwargs={"remote_side": "Tag.id"}
)
children: List["Tag"] = Relationship(back_populates="parent")
notes: List["Note"] = Relationship(back_populates="tags", link_model=NoteTag)
class Note(SQLModel, table=True): # type: ignore
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(max_length=255)
content: str
folder_id: Optional[int] = Field(default=None, foreign_key="folder.id")
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
user_id: int = Field(foreign_key="user.id")
#Relationships
folder: Optional[Folder] = Relationship(back_populates="notes")
user: User = Relationship(back_populates="notes")
tags: List[Tag] = Relationship(back_populates="notes", link_model=NoteTag)
# API Response models
class TagRead(SQLModel):
id: int
name: str
parent_id: Optional[int] = None
created_at: datetime
class TagCreate(SQLModel):
name: str
parent_id: Optional[int] = None
class TagUpdate(SQLModel):
name: Optional[str] = None
parent_id: Optional[int] = None
class TagTreeNode(SQLModel):
id: int
name: str
children: List["TagTreeNode"] = []
class TagTreeResponse(SQLModel):
tags: List[TagTreeNode]
# API Response models (what gets sent to frontend)
class NoteRead(SQLModel):
id: int
title: str
@ -68,6 +122,7 @@ class NoteRead(SQLModel):
folder_id: Optional[int] = None
created_at: datetime
updated_at: datetime
tags: List[TagRead] = []
class FolderTreeNode(SQLModel):

Binary file not shown.

View file

@ -1,8 +1,5 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException # type: ignore
from sqlmodel import Session, select # type: ignore
from app.auth import require_auth
from app.database import get_session
from app.models import (
@ -15,6 +12,9 @@ from app.models import (
NoteRead,
User,
)
from fastapi import APIRouter, Depends, HTTPException # type: ignore
from sqlalchemy.orm import selectinload
from sqlmodel import Session, select # type: ignore
router = APIRouter(prefix="/folders", tags=["folders"])
@ -35,21 +35,20 @@ def get_folder_tree(
):
"""Get complete folder tree with notes"""
# Get all top-level folders (parent_id is None) for current user
top_level_folders = session.exec(
select(Folder)
.options(selectinload(Folder.notes).selectinload(Note.tags))
.where(Folder.parent_id == None)
.where(Folder.user_id == current_user.id)
).all()
# Get all orphaned notes (folder_id is None) for current user
orphaned_notes = session.exec(
select(Note)
.options(selectinload(Note.tags))
.where(Note.folder_id == None)
.where(Note.user_id == current_user.id)
).all()
# Build tree recursively
tree = [build_folder_tree_node(folder) for folder in top_level_folders]
return FolderTreeResponse(

View file

@ -1,11 +1,10 @@
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from app.auth import require_auth
from app.database import get_session
from app.models import Note, NoteCreate, NoteUpdate, User
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
router = APIRouter(prefix="/notes", tags=["notes"])
@ -16,6 +15,7 @@ def list_notes(session: Session = Depends(get_session)):
return notes
@router.post("/", response_model=Note)
def create_note(
note: NoteCreate,

View file

@ -0,0 +1,94 @@
from tkinter.constants import TOP
from app.auth import require_auth
from app.database import get_session
from app.models import (
Note,
NoteCreate,
NoteTag,
NoteUpdate,
Tag,
TagCreate,
TagTreeNode,
TagTreeResponse,
User,
)
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import selectinload
from sqlmodel import Session, select
router = APIRouter(prefix="/tags", tags=["tags"])
@router.get("/")
def list_tags(session: Session = Depends(get_session)):
tags = session.exec(select(Tag)).all()
return tags
@router.post('/', response_model=Tag)
def create_tag(
tag: TagCreate,
current_user: User = Depends(require_auth),
session: Session = Depends(get_session)
):
tag_data = tag.model_dump()
tag_data["user_id"] = current_user.id
db_tag = Tag.model_validate(tag_data)
session.add(db_tag)
session.commit()
session.refresh(db_tag)
return db_tag
def build_tag_tree_node(tag: Tag) -> TagTreeNode:
return TagTreeNode(
id= tag.id,
name = tag.name,
children = [build_tag_tree_node(child) for child in tag.children]
)
@router.get("/tree")
def get_tag_tree(session: Session = Depends(get_session)):
top_level_tags = session.exec(
select(Tag)
.options(selectinload(Tag.children))
.where(Tag.parent_id == None)
).all()
tree = [build_tag_tree_node(tag) for tag in top_level_tags]
return TagTreeResponse(tags=tree)
@router.post("/note/{note_id}/tag/{tag_id}")
def add_tag_to_note(
note_id: int,
tag_id: int,
current_user: User = Depends(require_auth),
session: Session = Depends(get_session)
):
existing = session.exec(
select(NoteTag)
.where(NoteTag.note_id == note_id)
.where(NoteTag.tag_id == tag_id)
).first()
if existing:
return {"message": "Tag already added"}
note_tag = NoteTag(note_id=note_id, tag_id=tag_id)
session.add(note_tag)
session.commit()
return note_tag
@router.delete("/{tag_id}")
def delete_note(tag_id: int, session: Session = Depends(get_session)):
tag = session.get(Tag, tag_id)
if not tag:
raise HTTPException(status_code=404, detail="Tag not found")
session.delete(tag)
session.commit()
return {"message": "tag deleted"}

View file

@ -1,4 +1,5 @@
import { FolderTreeResponse, FolderTreeNode } from "./folders";
import { Tag } from "./tags";
export async function deriveKey(password: string, salt: string) {
const enc = new TextEncoder();
@ -114,6 +115,12 @@ export async function decryptFolderTree(
...note,
title: await decryptString(note.title, encryptionKey),
content: await decryptString(note.content, encryptionKey),
tags: await Promise.all(
note.tags.map(async (tag) => ({
...tag,
name: await decryptString(tag.name, encryptionKey),
})),
),
})),
),
children: await Promise.all(
@ -131,7 +138,35 @@ export async function decryptFolderTree(
...note,
title: await decryptString(note.title, encryptionKey),
content: await decryptString(note.content, encryptionKey),
tags: await Promise.all(
note.tags.map(async (tag) => ({
...tag,
name: await decryptString(tag.name, encryptionKey),
})),
),
})),
),
};
}
export const decryptTagTree = async (
tags: Tag[],
key: CryptoKey,
parentPath = "",
): Promise<Tag[]> => {
return Promise.all(
tags.map(async (tag) => {
const decryptedName = await decryptString(tag.name, key);
const currentPath = parentPath
? `${parentPath} ${decryptedName}`
: decryptedName;
return {
...tag,
name: decryptedName,
parent_path: parentPath,
children: await decryptTagTree(tag.children, key, currentPath),
};
}),
);
};

View file

@ -1,6 +1,7 @@
import axios from "axios";
import { decryptFolderTree } from "./encryption";
import { useAuthStore } from "../stores/authStore";
import { Tag } from "./tags";
axios.defaults.withCredentials = true;
@ -22,6 +23,7 @@ export interface NoteRead {
folder_id: number | null;
created_at: string;
updated_at: string;
tags: Tag[];
}
export interface FolderTreeNode {

View file

@ -1,7 +1,7 @@
import axios from "axios";
import { NoteRead } from "./folders";
import { encryptString, decryptString } from "./encryption";
import { useAuthStore } from "../stores/authStore";
import { Tag } from "./tags";
axios.defaults.withCredentials = true;
const API_URL = (import.meta as any).env.PROD
? "/api"
@ -14,6 +14,7 @@ export interface Note {
content: string;
created_at: string;
updated_at: string;
tags: Tag[];
}
export interface NoteCreate {
@ -50,9 +51,14 @@ const fetchNotes = async () => {
...note,
title: await decryptString(note.title, encryptionKey),
content: await decryptString(note.content, encryptionKey),
tags: await Promise.all(
note.tags.map(async (tag) => ({
...tag,
name: await decryptString(tag.name, encryptionKey),
})),
),
})),
);
return decryptedNotes;
};

63
frontend/src/api/tags.tsx Normal file
View file

@ -0,0 +1,63 @@
import axios from "axios";
import { encryptString, decryptTagTree } from "./encryption";
import { useAuthStore } from "../stores/authStore";
axios.defaults.withCredentials = true;
const API_URL = (import.meta as any).env.PROD
? "/api"
: "http://localhost:8000/api";
export interface Tag {
id: string;
name: string;
parent_id?: number;
created_at: string;
children: Tag[];
parent_path: string;
}
export interface TagCreate {
name: string;
parent_id?: number;
}
const fetchTags = async () => {
const encryptionKey = useAuthStore.getState().encryptionKey;
if (!encryptionKey) throw new Error("Not authenticated");
const { data } = await axios.get(`${API_URL}/tags/tree`);
const tags = decryptTagTree(data.tags, encryptionKey);
console.log(await tags);
return tags;
};
const createTag = async (tag: TagCreate, noteId?: number) => {
const encryptionKey = useAuthStore.getState().encryptionKey;
if (!encryptionKey) throw new Error("Not authenticated");
const tagName = await encryptString(tag.name, encryptionKey);
const encryptedTag = {
name: tagName,
parent_id: tag.parent_id,
};
const r = await axios.post(`${API_URL}/tags/`, encryptedTag);
console.log(r);
if (noteId) {
return await addTagToNote(r.data.id, noteId);
}
};
const addTagToNote = async (tagId: number, noteId: number) => {
return axios.post(`${API_URL}/tags/note/${noteId}/tag/${tagId}`);
};
const deleteTag = async (tagId: number) => {
return axios.delete(`${API_URL}/tags/${tagId}`);
};
export const tagsApi = {
list: async () => await fetchTags(),
create: (tag: TagCreate, noteId?: number) => createTag(tag, noteId),
delete: (tagId: number) => deleteTag(tagId),
};

View file

@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react";
import "../../main.css";
import { motion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { useAuthStore } from "@/stores/authStore";
import { useNoteStore } from "@/stores/notesStore";
import { useUIStore } from "@/stores/uiStore";
@ -9,6 +9,9 @@ import { TiptapEditor } from "../TipTap";
import { Sidebar } from "./components/sidebar/SideBar";
import { StatusIndicator } from "./components/StatusIndicator";
import { Tag, tagsApi } from "@/api/tags";
import { useTagStore } from "@/stores/tagStore";
function Home() {
const [newFolder] = useState(false);
const [lastSavedNote, setLastSavedNote] = useState<{
@ -92,12 +95,29 @@ function Home() {
}
};
const { getTagTree, tagTree } = useTagStore();
const getTags = () => {
getTagTree();
};
return (
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
{/* Sidebar */}
{showModal && <Modal />}
<Sidebar />
<button onClick={getTags}>create</button>
{/*<div className="flex flex-col">
<input
type="text"
value={tagName}
onChange={(e) => setTagName(e.target.value)}
/>
{tags.map((tag) => (
<button onClick={() => deleteTag(tag.id)} key={tag.id}>
{tag.name}
</button>
))}
</div>*/}
{/* Main editor area */}
<div className="flex flex-col w-full h-screen overflow-hidden">
@ -109,6 +129,20 @@ function Home() {
onChange={(e) => setTitle(e.target.value)}
className="w-full px-4 py-3 text-3xl font-semibold bg-transparentfocus:outline-none focus:border-ctp-mauve transition-colors placeholder:text-ctp-overlay0 text-ctp-text"
/>
<div className="px-4 py-2 border-b border-ctp-surface2 flex items-center gap-2 flex-wrap">
{selectedNote?.tags &&
selectedNote.tags.map((tag) => (
<button
onClick={() => null}
key={tag.id}
className="bg-ctp-surface0 px-1.5 text-sm rounded-full"
>
{tag.parent_id && "..."}
{tag.name}
</button>
))}
</div>
<TiptapEditor
key={selectedNote?.id}
content={selectedNote?.content || ""}
@ -136,8 +170,53 @@ const Modal = () => {
onClick={(e) => e.stopPropagation()}
className="w-2/3 h-2/3 bg-ctp-base rounded-xl border-ctp-surface2 border p-5"
>
<Login />
{/*<Login />*/}
<TagSelector />
</div>
</motion.div>
);
};
export const TagSelector = () => {
const { tagTree } = useTagStore();
const [value, setValue] = useState("");
return (
<div>
{/*<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>*/}
{tagTree && tagTree.map((tag) => <TagTree tag={tag} />)}
</div>
);
};
export const TagTree = ({ tag, depth = 0 }: { tag: Tag; depth?: number }) => {
const [collapse, setCollapse] = useState(false);
return (
<div key={tag.id} className="flex flex-col relative">
<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

@ -2,6 +2,8 @@ import React, { useState, useRef, useEffect } from "react";
// @ts-ignore
import FolderIcon from "@/assets/fontawesome/svg/folder.svg?react";
// @ts-ignore
import TagsIcon from "@/assets/fontawesome/svg/tags.svg?react";
import { DraggableNote } from "./subcomponents/DraggableNote";
import {
@ -19,6 +21,7 @@ import { SidebarHeader } from "./subcomponents/SideBarHeader.tsx";
import { useAuthStore } from "@/stores/authStore.ts";
import { useNoteStore } from "@/stores/notesStore.ts";
import { useUIStore } from "@/stores/uiStore.ts";
import { TagSelector } from "../../Home.tsx";
export const Sidebar = () => {
const [newFolder, setNewFolder] = useState(false);
@ -39,7 +42,8 @@ export const Sidebar = () => {
const { encryptionKey } = useAuthStore();
const { setSideBarResize, sideBarResize } = useUIStore();
const { setSideBarResize, sideBarResize, sideBarView, setSideBarView } =
useUIStore();
useEffect(() => {
if (newFolder && newFolderRef.current) {
newFolderRef.current.focus();
@ -169,6 +173,8 @@ export const Sidebar = () => {
style={{ width: `${sideBarResize}px` }}
>
<SidebarHeader setNewFolder={setNewFolder} />
{sideBarView == "folders" ? (
<>
<div
className="bg-ctp-mantle min-h-full border-r border-ctp-surface2 w-full p-4 overflow-y-auto sm:block hidden flex-col gap-3"
onDragOver={(e) => e.preventDefault()}
@ -228,6 +234,12 @@ export const Sidebar = () => {
</div>
)}
</DragOverlay>
</>
) : (
<div className="bg-ctp-mantle min-h-full border-r border-ctp-surface2 w-full p-4 overflow-y-auto sm:block hidden flex-col gap-3">
<TagSelector />
</div>
)}
</div>
</div>
</DndContext>

View file

@ -2,8 +2,11 @@ import { SetStateAction } from "react";
// @ts-ignore
import FolderPlusIcon from "@assets/fontawesome/svg/folder-plus.svg?react";
// @ts-ignore
import TagsIcon from "@assets/fontawesome/svg/tags.svg?react";
// @ts-ignore
import FileCirclePlusIcon from "@assets/fontawesome/svg/file-circle-plus.svg?react";
import { useNoteStore } from "@/stores/notesStore";
import { useUIStore } from "@/stores/uiStore";
export const SidebarHeader = ({
setNewFolder,
@ -11,6 +14,7 @@ export const SidebarHeader = ({
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
}) => {
const { createNote, selectedFolder } = useNoteStore();
const { setSideBarView, sideBarView } = useUIStore();
const handleCreate = async () => {
await createNote({
title: "Untitled",
@ -22,17 +26,26 @@ export const SidebarHeader = ({
<div className="flex items-center justify-center w-full gap-2 bg-ctp-mantle border-b border-ctp-surface0 p-1">
<button
onClick={() => setNewFolder(true)}
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-2"
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1"
title="New folder"
>
<FolderPlusIcon className="w-4 h-4 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
<FolderPlusIcon className="w-5 h-5 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
</button>
<button
onClick={() =>
setSideBarView(sideBarView == "tags" ? "folders" : "tags")
}
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1"
title="Tags"
>
<TagsIcon className="w-5 h-5 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
</button>
<button
onClick={handleCreate}
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-2 fill-ctp-mauve hover:fill-ctp-base"
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1 fill-ctp-mauve hover:fill-ctp-base"
title="New note"
>
<FileCirclePlusIcon className="w-4 h-4 text-ctp-mauve group-hover:text-ctp-base transition-colors" />
<FileCirclePlusIcon className="w-5 h-5 text-ctp-mauve group-hover:text-ctp-base transition-colors" />
</button>
</div>
);

View file

@ -13,15 +13,15 @@ interface User {
id: number;
username: string;
email: string;
salt: string; // For key derivation
salt: string;
}
interface AuthState {
user: User | null;
encryptionKey: CryptoKey | null; // Memory only!
encryptionKey: CryptoKey | null;
isAuthenticated: boolean;
rememberMe: boolean;
setRememberMe: (boolean) => void;
setRememberMe: (remember: boolean) => void;
login: (username: string, password: string) => Promise<void>;
register: (
@ -48,7 +48,6 @@ export const useAuthStore = create<AuthState>()(
set({ rememberMe: bool });
},
initEncryptionKey: async (password: string, salt: string) => {
// Use user-specific salt instead of hardcoded
const key = await deriveKey(password, salt);
set({ encryptionKey: key });
},
@ -79,7 +78,6 @@ export const useAuthStore = create<AuthState>()(
const data = await response.json();
// Store the master key directly (not derived from password)
set({
user: data.user,
isAuthenticated: true,
@ -102,11 +100,9 @@ export const useAuthStore = create<AuthState>()(
const { user } = await response.json();
// Derive KEK and unwrap master key
const kek = await deriveKey(password, user.salt);
const masterKey = await unwrapMasterKey(user.wrapped_master_key, kek);
// Store master key in memory
set({ encryptionKey: masterKey, user, isAuthenticated: true });
},

View file

@ -78,7 +78,7 @@ export const useNoteStore = create<NoteState>()(
(set, get) => ({
loadFolderTree: async () => {
const data = await folderApi.tree();
console.log("getting tree");
// console.log(data);
set({ folderTree: data });
},
folderTree: null,

View file

@ -0,0 +1,36 @@
import { tagsApi } from "@/api/tags";
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface Tag {
id: string;
name: string;
parent_id?: number;
created_at: string;
parent_path: string;
children: Tag[];
}
interface TagStore {
tagTree: Tag[] | null;
getTagTree: () => void;
}
export const useTagStore = create<TagStore>()(
persist(
(set, get) => ({
tagTree: null,
getTagTree: async () => {
const tags = await tagsApi.list();
set({ tagTree: tags });
},
}),
{
name: "tags-storage",
partialize: (state) => ({
tagTree: state.tagTree,
}),
},
),
);

View file

@ -10,6 +10,9 @@ interface UIState {
sideBarResize: number;
setSideBarResize: (size: number) => void;
sideBarView: string;
setSideBarView: (view: string) => void;
}
export const useUIStore = create<UIState>()(
@ -19,7 +22,7 @@ export const useUIStore = create<UIState>()(
setUpdating: (update) => {
set({ updating: update });
},
showModal: false,
showModal: true,
setShowModal: (show) => {
set({ showModal: show });
},
@ -27,6 +30,10 @@ export const useUIStore = create<UIState>()(
setSideBarResize: (size) => {
set({ sideBarResize: size });
},
sideBarView: "folders",
setSideBarView: (view) => {
set({ sideBarView: view });
},
}),
{
name: "ui-store",