diff --git a/.gitignore b/.gitignore index 2d5f545..58926dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules frontend/src/assets/fontawesome/svg/* frontend/src/assets/fontawesome/svg/0.svg +*.db +.zed/settings.json diff --git a/backend/app/__pycache__/auth.cpython-314.pyc b/backend/app/__pycache__/auth.cpython-314.pyc index 4e8d24e..a45ea6f 100644 Binary files a/backend/app/__pycache__/auth.cpython-314.pyc and b/backend/app/__pycache__/auth.cpython-314.pyc differ diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc index ab30837..10d41a4 100644 Binary files a/backend/app/__pycache__/main.cpython-314.pyc and b/backend/app/__pycache__/main.cpython-314.pyc differ diff --git a/backend/app/__pycache__/models.cpython-314.pyc b/backend/app/__pycache__/models.cpython-314.pyc index 00bb4e2..ec9635d 100644 Binary files a/backend/app/__pycache__/models.cpython-314.pyc and b/backend/app/__pycache__/models.cpython-314.pyc differ diff --git a/backend/app/auth.py b/backend/app/auth.py index fec6785..b7498ca 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -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: diff --git a/backend/app/main.py b/backend/app/main.py index 728376a..9471f10 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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("/") diff --git a/backend/app/models.py b/backend/app/models.py index afeafa9..2f13376 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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): diff --git a/backend/app/routes/__pycache__/folders.cpython-314.pyc b/backend/app/routes/__pycache__/folders.cpython-314.pyc index 5075343..d4377a9 100644 Binary files a/backend/app/routes/__pycache__/folders.cpython-314.pyc and b/backend/app/routes/__pycache__/folders.cpython-314.pyc differ diff --git a/backend/app/routes/__pycache__/notes.cpython-314.pyc b/backend/app/routes/__pycache__/notes.cpython-314.pyc index 9098d40..5592dd8 100644 Binary files a/backend/app/routes/__pycache__/notes.cpython-314.pyc and b/backend/app/routes/__pycache__/notes.cpython-314.pyc differ diff --git a/backend/app/routes/__pycache__/tags.cpython-314.pyc b/backend/app/routes/__pycache__/tags.cpython-314.pyc new file mode 100644 index 0000000..bd6a99d Binary files /dev/null and b/backend/app/routes/__pycache__/tags.cpython-314.pyc differ diff --git a/backend/app/routes/folders.py b/backend/app/routes/folders.py index f77ea22..09b393d 100644 --- a/backend/app/routes/folders.py +++ b/backend/app/routes/folders.py @@ -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( diff --git a/backend/app/routes/notes.py b/backend/app/routes/notes.py index 283168a..ba90306 100644 --- a/backend/app/routes/notes.py +++ b/backend/app/routes/notes.py @@ -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, diff --git a/backend/app/routes/tags.py b/backend/app/routes/tags.py new file mode 100644 index 0000000..566c4eb --- /dev/null +++ b/backend/app/routes/tags.py @@ -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"} diff --git a/frontend/src/api/encryption.tsx b/frontend/src/api/encryption.tsx index 3f094b9..cd10770 100644 --- a/frontend/src/api/encryption.tsx +++ b/frontend/src/api/encryption.tsx @@ -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 => { + 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), + }; + }), + ); +}; diff --git a/frontend/src/api/folders.tsx b/frontend/src/api/folders.tsx index 7f09b9d..ec82412 100644 --- a/frontend/src/api/folders.tsx +++ b/frontend/src/api/folders.tsx @@ -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 { diff --git a/frontend/src/api/notes.tsx b/frontend/src/api/notes.tsx index 3407a21..8bb6caa 100644 --- a/frontend/src/api/notes.tsx +++ b/frontend/src/api/notes.tsx @@ -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; }; diff --git a/frontend/src/api/tags.tsx b/frontend/src/api/tags.tsx new file mode 100644 index 0000000..6544137 --- /dev/null +++ b/frontend/src/api/tags.tsx @@ -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), +}; diff --git a/frontend/src/pages/Home/Home.tsx b/frontend/src/pages/Home/Home.tsx index 4dc8cfa..0cdd481 100644 --- a/frontend/src/pages/Home/Home.tsx +++ b/frontend/src/pages/Home/Home.tsx @@ -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 (
{/* Sidebar */} {showModal && } + + {/*
+ setTagName(e.target.value)} + /> + {tags.map((tag) => ( + + ))} +
*/} {/* Main editor area */}
@@ -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" /> +
+ {selectedNote?.tags && + selectedNote.tags.map((tag) => ( + + ))} +
+ { onClick={(e) => e.stopPropagation()} className="w-2/3 h-2/3 bg-ctp-base rounded-xl border-ctp-surface2 border p-5" > - + {/**/} +
); }; + +export const TagSelector = () => { + const { tagTree } = useTagStore(); + const [value, setValue] = useState(""); + return ( +
+ {/* setValue(e.target.value)} + />*/} + {tagTree && tagTree.map((tag) => )} +
+ ); +}; + +export const TagTree = ({ tag, depth = 0 }: { tag: Tag; depth?: number }) => { + const [collapse, setCollapse] = useState(false); + + return ( +
+
setCollapse(!collapse)}>{tag.name}
+ + {collapse && ( + + {/* The line container */} +
+ {/* Child tags */} + {tag.children.map((child) => ( + + ))} +
+
+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/Home/components/sidebar/SideBar.tsx b/frontend/src/pages/Home/components/sidebar/SideBar.tsx index 69d9324..e98a46c 100644 --- a/frontend/src/pages/Home/components/sidebar/SideBar.tsx +++ b/frontend/src/pages/Home/components/sidebar/SideBar.tsx @@ -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,65 +173,73 @@ export const Sidebar = () => { style={{ width: `${sideBarResize}px` }} > -
e.preventDefault()} - onTouchMove={(e) => e.preventDefault()} - > - {/* New folder input */} - {newFolder && ( -
- setNewFolder(false)} - onChange={(e) => setNewFolderText(e.target.value)} - value={newFolderText} - type="text" - placeholder="Folder name..." - className="standard-input" - ref={newFolderRef} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleCreateFolder(); - } - if (e.key === "Escape") { - setNewFolder(false); - } - }} - /> -
- )} + {sideBarView == "folders" ? ( + <> +
e.preventDefault()} + onTouchMove={(e) => e.preventDefault()} + > + {/* New folder input */} + {newFolder && ( +
+ setNewFolder(false)} + onChange={(e) => setNewFolderText(e.target.value)} + value={newFolderText} + type="text" + placeholder="Folder name..." + className="standard-input" + ref={newFolderRef} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateFolder(); + } + if (e.key === "Escape") { + setNewFolder(false); + } + }} + /> +
+ )} - {/* Folder tree */} -
- {folderTree?.folders.map((folder) => ( - - ))} -
- - {/* Orphaned notes */} - {folderTree?.orphaned_notes && - folderTree.orphaned_notes.length > 0 && ( -
- {folderTree.orphaned_notes.map((note) => ( - + {/* Folder tree */} +
+ {folderTree?.folders.map((folder) => ( + ))}
- )} -
- - {activeItem?.type === "note" && ( -
- {activeItem.data.title} + {/* Orphaned notes */} + {folderTree?.orphaned_notes && + folderTree.orphaned_notes.length > 0 && ( +
+ {folderTree.orphaned_notes.map((note) => ( + + ))} +
+ )}
- )} - {activeItem?.type === "folder" && ( -
- - {activeItem.data.name} -
- )} -
+ + + {activeItem?.type === "note" && ( +
+ {activeItem.data.title} +
+ )} + {activeItem?.type === "folder" && ( +
+ + {activeItem.data.name} +
+ )} +
+ + ) : ( +
+ +
+ )}
diff --git a/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx b/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx index 2af39d4..218edfc 100644 --- a/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx +++ b/frontend/src/pages/Home/components/sidebar/subcomponents/SideBarHeader.tsx @@ -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>; }) => { const { createNote, selectedFolder } = useNoteStore(); + const { setSideBarView, sideBarView } = useUIStore(); const handleCreate = async () => { await createNote({ title: "Untitled", @@ -22,17 +26,26 @@ export const SidebarHeader = ({
+
); diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts index 8916bef..d97bcb9 100644 --- a/frontend/src/stores/authStore.ts +++ b/frontend/src/stores/authStore.ts @@ -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; register: ( @@ -48,7 +48,6 @@ export const useAuthStore = create()( 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()( 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()( 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 }); }, diff --git a/frontend/src/stores/notesStore.ts b/frontend/src/stores/notesStore.ts index 8c0fd8a..dc508c4 100644 --- a/frontend/src/stores/notesStore.ts +++ b/frontend/src/stores/notesStore.ts @@ -78,7 +78,7 @@ export const useNoteStore = create()( (set, get) => ({ loadFolderTree: async () => { const data = await folderApi.tree(); - console.log("getting tree"); + // console.log(data); set({ folderTree: data }); }, folderTree: null, diff --git a/frontend/src/stores/tagStore.ts b/frontend/src/stores/tagStore.ts new file mode 100644 index 0000000..ef7fc77 --- /dev/null +++ b/frontend/src/stores/tagStore.ts @@ -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()( + persist( + (set, get) => ({ + tagTree: null, + + getTagTree: async () => { + const tags = await tagsApi.list(); + set({ tagTree: tags }); + }, + }), + { + name: "tags-storage", + partialize: (state) => ({ + tagTree: state.tagTree, + }), + }, + ), +); diff --git a/frontend/src/stores/uiStore.ts b/frontend/src/stores/uiStore.ts index 399c4cc..83e5659 100644 --- a/frontend/src/stores/uiStore.ts +++ b/frontend/src/stores/uiStore.ts @@ -10,6 +10,9 @@ interface UIState { sideBarResize: number; setSideBarResize: (size: number) => void; + + sideBarView: string; + setSideBarView: (view: string) => void; } export const useUIStore = create()( @@ -19,7 +22,7 @@ export const useUIStore = create()( setUpdating: (update) => { set({ updating: update }); }, - showModal: false, + showModal: true, setShowModal: (show) => { set({ showModal: show }); }, @@ -27,6 +30,10 @@ export const useUIStore = create()( setSideBarResize: (size) => { set({ sideBarResize: size }); }, + sideBarView: "folders", + setSideBarView: (view) => { + set({ sideBarView: view }); + }, }), { name: "ui-store",