Update notes response model and implement error handling
This commit is contained in:
parent
3fe4b9ea88
commit
ffbf485935
12 changed files with 213 additions and 126 deletions
|
|
@ -2,14 +2,14 @@ from datetime import datetime
|
|||
|
||||
from app.auth import require_auth
|
||||
from app.database import get_session
|
||||
from app.models import Note, NoteCreate, NoteUpdate, User
|
||||
from app.models import Note, NoteCreate, NoteRead, NoteUpdate, User
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
router = APIRouter(prefix="/notes", tags=["notes"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
@router.get("/", response_model=list[NoteRead])
|
||||
def list_notes(session: Session = Depends(get_session)):
|
||||
notes = session.exec(select(Note).order_by(Note.updated_at.desc())).all() # pyright: ignore[reportAttributeAccessIssue]
|
||||
return notes
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export type NoteRead = CamelCasedPropertiesDeep<
|
|||
export type NoteCreate = CamelCasedPropertiesDeep<
|
||||
components["schemas"]["NoteCreate"]
|
||||
>;
|
||||
export type Note = CamelCasedPropertiesDeep<components["schemas"]["Note"]>;
|
||||
|
||||
const createNote = async (note: NoteCreate) => {
|
||||
const encryptionKey = useAuthStore.getState().encryptionKey;
|
||||
|
|
@ -31,23 +32,31 @@ const fetchNotes = async () => {
|
|||
const encryptionKey = useAuthStore.getState().encryptionKey;
|
||||
if (!encryptionKey) throw new Error("Not authenticated");
|
||||
|
||||
const { data } = await client.GET(`/api/notes/`);
|
||||
const { data, error } = await client.GET(`/api/notes/`);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
console.log(data);
|
||||
|
||||
if (data) {
|
||||
const decryptedNotes = await Promise.all(
|
||||
data.map(async (note: NoteRead) => ({
|
||||
data.map(async (note) => ({
|
||||
...note,
|
||||
title: await decryptString(note.title, encryptionKey),
|
||||
content: await decryptString(note.content, encryptionKey),
|
||||
tags: await Promise.all(
|
||||
tags: note.tags
|
||||
? await Promise.all(
|
||||
note.tags.map(async (tag) => ({
|
||||
...tag,
|
||||
name: await decryptString(tag.name, encryptionKey),
|
||||
})),
|
||||
),
|
||||
)
|
||||
: [],
|
||||
})),
|
||||
);
|
||||
return decryptedNotes;
|
||||
}
|
||||
};
|
||||
|
||||
const updateNote = async (id: number, note: Partial<NoteRead>) => {
|
||||
|
|
@ -64,9 +73,7 @@ const updateNote = async (id: number, note: Partial<NoteRead>) => {
|
|||
if (note.folderId) {
|
||||
encryptedNote.folderId = note.folderId;
|
||||
}
|
||||
// if (!note.folderId){
|
||||
// throw new Error("Folder id missing from note.")
|
||||
// }
|
||||
|
||||
const { data, error } = await client.PATCH(`/api/notes/{note_id}`, {
|
||||
body: encryptedNote,
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -104,10 +104,14 @@ export const useUpdateFolder = () => {
|
|||
folders: FolderTreeNode[],
|
||||
): FolderTreeNode[] => {
|
||||
return folders.map((f) => {
|
||||
if (f.id == folderId) {
|
||||
if (f.id === folderId) {
|
||||
return {
|
||||
...f,
|
||||
...folder,
|
||||
...(folder.name !== undefined &&
|
||||
folder.name !== null && { name: folder.name }),
|
||||
...(folder.parentId !== undefined && {
|
||||
parentId: folder.parentId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,16 @@ import { Sidebar } from "./components/sidebar/SideBar";
|
|||
import { StatusIndicator } from "./components/StatusIndicator";
|
||||
import { useCreateTag, useTagTree } from "@/hooks/useTags";
|
||||
import { useFolderTree, useUpdateNote } from "@/hooks/useFolders";
|
||||
import { Note } from "@/api/notes";
|
||||
import { Note, NoteRead } from "@/api/notes";
|
||||
import { DecryptedTagNode } from "@/api/encryption";
|
||||
// @ts-ignore
|
||||
import XmarkIcon from "@/assets/fontawesome/svg/xmark.svg?react";
|
||||
|
||||
function Home() {
|
||||
const [newFolder] = useState(false);
|
||||
|
||||
// Local state for editing the current note
|
||||
const [editingNote, setEditingNote] = useState<Note | null>(null);
|
||||
const [editingNote, setEditingNote] = useState<NoteRead | null>(null);
|
||||
const [lastSavedNote, setLastSavedNote] = useState<{
|
||||
id: number;
|
||||
title: string;
|
||||
|
|
@ -85,6 +87,7 @@ function Home() {
|
|||
}
|
||||
|
||||
try {
|
||||
if (!editingNote.id) throw new Error("Editing note has no id.");
|
||||
await updateNoteMutation.mutateAsync({
|
||||
noteId: editingNote.id,
|
||||
note: {
|
||||
|
|
@ -116,45 +119,34 @@ function Home() {
|
|||
return (
|
||||
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
{showModal && <Modal />}
|
||||
<AnimatePresence>{showModal && <Modal />}</AnimatePresence>
|
||||
|
||||
<Sidebar />
|
||||
{/*<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">
|
||||
<div className="flex flex-col w-full h-screen overflow-y-auto items-center justify-center">
|
||||
{/*<Editor />*/}
|
||||
<div className="h-full lg:w-3xl w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Untitled note..."
|
||||
value={editingNote?.title || ""}
|
||||
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"
|
||||
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"
|
||||
/>
|
||||
<div className="px-4 py-2 border-b border-ctp-surface2 flex items-center gap-2 flex-wrap">
|
||||
{/*<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 px-1.5 text-sm rounded-full"
|
||||
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>
|
||||
</div>*/}
|
||||
|
||||
<TiptapEditor
|
||||
key={editingNote?.id}
|
||||
|
|
@ -162,6 +154,7 @@ function Home() {
|
|||
onChange={setContent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusIndicator />
|
||||
</div>
|
||||
|
|
@ -176,16 +169,28 @@ const Modal = () => {
|
|||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setShowModal(false)}
|
||||
className="absolute h-screen w-screen flex items-center justify-center bg-ctp-crust/60 z-50"
|
||||
className="fixed inset-0 h-screen w-screen flex items-center justify-center bg-ctp-crust/70 backdrop-blur-sm z-50"
|
||||
>
|
||||
<div
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.3 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-2/3 h-2/3 bg-ctp-base rounded-xl border-ctp-surface2 border p-5"
|
||||
className="relative w-full max-w-md mx-4 bg-ctp-base rounded-xl border-ctp-surface2 border p-8 shadow-2xl"
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="absolute top-4 right-4 p-2 hover:bg-ctp-surface0 rounded-sm transition-colors group"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<XmarkIcon className="w-5 h-5 fill-ctp-overlay0 group-hover:fill-ctp-text transition-colors" />
|
||||
</button>
|
||||
<Login />
|
||||
{/*<TagSelector />*/}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,29 +25,29 @@ export const SidebarHeader = ({
|
|||
};
|
||||
return (
|
||||
<div className="w-full p-2 border-b border-ctp-surface2 bg-ctp-mantle">
|
||||
<div className="flex items-center justify-around bg-ctp-surface0 rounded-lg p-0.5">
|
||||
<div className="flex items-center justify-around bg-ctp-surface0 rounded-lg p-1 gap-1">
|
||||
<button
|
||||
onClick={() => setNewFolder(true)}
|
||||
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1"
|
||||
className="hover:bg-ctp-mauve active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"
|
||||
title="New folder"
|
||||
>
|
||||
<FolderPlusIcon className="w-5 h-5 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
|
||||
<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="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1"
|
||||
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-colors fill-ctp-mauve" />
|
||||
<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 group transition-colors rounded-sm p-1 m-1 fill-ctp-mauve hover:fill-ctp-base"
|
||||
className="hover:bg-ctp-mauve active:scale-95 group transition-all duration-200 rounded-md p-2 hover:shadow-md"
|
||||
title="New note"
|
||||
>
|
||||
<FileCirclePlusIcon className="w-5 h-5 text-ctp-mauve group-hover:text-ctp-base transition-colors" />
|
||||
<FileCirclePlusIcon className="w-5 h-5 group-hover:fill-ctp-base transition-all duration-200 fill-ctp-mauve" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,31 +26,68 @@ export const Login = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="gap-2 flex flex-col">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="gap-4 flex flex-col max-w-md mx-auto"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-ctp-text mb-2">
|
||||
Welcome Back
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-ctp-subtext0">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
placeholder="Enter your username"
|
||||
className="standard-input"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-ctp-subtext0">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="standard-input"
|
||||
placeholder="Password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
{error && <div>{error}</div>}
|
||||
<button type="submit">Login</button>
|
||||
<div className="flex gap-2">
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-ctp-red/10 border border-ctp-red text-ctp-red px-3 py-2 rounded-sm text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="check box"
|
||||
type="checkbox"
|
||||
id="remember"
|
||||
checked={remember}
|
||||
onChange={(e) => setRemember(e.target.checked)}
|
||||
className="accent-ctp-mauve cursor-pointer"
|
||||
/>
|
||||
<div>Remember me?</div>
|
||||
<label
|
||||
htmlFor="remember"
|
||||
className="text-sm text-ctp-subtext0 cursor-pointer"
|
||||
>
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-ctp-mauve hover:bg-ctp-mauve/90 text-ctp-base font-semibold px-4 py-2.5 rounded-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ctp-mauve focus:ring-offset-2 focus:ring-offset-ctp-base"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,29 +23,63 @@ export const Register = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="gap-4 flex flex-col max-w-md mx-auto"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-ctp-text mb-2">
|
||||
Create Account
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-ctp-subtext0">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
placeholder="Choose a username"
|
||||
className="standard-input"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-ctp-subtext0">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
placeholder="Enter your email"
|
||||
className="standard-input"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-ctp-subtext0">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
className="standard-input"
|
||||
placeholder="Create a password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
{error && <div>{error}</div>}
|
||||
<button type="submit">Login</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-ctp-red/10 border border-ctp-red text-ctp-red px-3 py-2 rounded-sm text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-ctp-mauve hover:bg-ctp-mauve/90 text-ctp-base font-semibold px-4 py-2.5 rounded-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ctp-mauve focus:ring-offset-2 focus:ring-offset-ctp-base"
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// Similar pattern for Register.tsx
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const TiptapEditor = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="tiptap-editor h-full">
|
||||
<div className="tiptap-editor pt-0">
|
||||
{/* Toolbar */}
|
||||
{/*<div className="editor-toolbar">
|
||||
<div className="toolbar-group">
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
}
|
||||
|
||||
.ProseMirror h3 {
|
||||
@apply text-xl font-semibold text-ctp-teal mt-5 mb-2;
|
||||
@apply text-xl font-semibold text-ctp-mauve mt-5 mb-2;
|
||||
}
|
||||
|
||||
.ProseMirror code {
|
||||
|
|
@ -149,11 +149,12 @@
|
|||
.ProseMirror li[data-checked="true"] > div > p {
|
||||
@apply line-through text-ctp-overlay0;
|
||||
text-decoration-style: wavy;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
.ProseMirror u {
|
||||
@apply decoration-ctp-mauve;
|
||||
text-decoration-style: wavy;
|
||||
/*text-decoration-style: wavy;*/
|
||||
}
|
||||
|
||||
.ProseMirror li::marker {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { useNoteStore } from "./notesStore";
|
||||
import {
|
||||
deriveKey,
|
||||
generateMasterKey,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Note } from "@/api/notes";
|
||||
import { Note, NoteRead } from "@/api/notes";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
|
|
@ -15,8 +15,8 @@ interface UIState {
|
|||
sideBarView: string;
|
||||
setSideBarView: (view: string) => void;
|
||||
|
||||
selectedNote: Note | null;
|
||||
setSelectedNote: (note: Note | null) => void;
|
||||
selectedNote: NoteRead | null;
|
||||
setSelectedNote: (note: NoteRead | null) => void;
|
||||
|
||||
selectedFolder: number | null;
|
||||
setSelectedFolder: (id: number | null) => void;
|
||||
|
|
@ -43,7 +43,7 @@ export const useUIStore = create<UIState>()(
|
|||
},
|
||||
selectedNote: null,
|
||||
|
||||
setSelectedNote: (id: Note | null) => {
|
||||
setSelectedNote: (id: NoteRead | null) => {
|
||||
set({ selectedNote: id });
|
||||
},
|
||||
selectedFolder: null,
|
||||
|
|
|
|||
2
frontend/src/types/api.d.ts
vendored
2
frontend/src/types/api.d.ts
vendored
|
|
@ -548,7 +548,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
"application/json": components["schemas"]["NoteRead"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue