Bulk update

This commit is contained in:
james fitzsimons 2025-11-23 09:08:01 +00:00
parent c79ac06b58
commit 9645f411f3
29 changed files with 12284 additions and 215 deletions

View file

@ -1,13 +1,10 @@
# ---- Build stage (optional) ----
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
FROM python:3.11-slim
# ---- Runtime stage ----
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--port", "8000"]
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Binary file not shown.

16
backend/app/database.py Normal file
View file

@ -0,0 +1,16 @@
from sqlmodel import Session, SQLModel, create_engine # type: ignore
DATABASE_URL = "sqlite:///./notes.db"
engine = create_engine(
DATABASE_URL, echo=True, connect_args={"check_same_thread": False}
)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session

View file

@ -1,34 +1,30 @@
# backend/app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from fastapi import FastAPI # type: ignore
from fastapi.middleware.cors import CORSMiddleware # type:ignore
from .models import add_note, get_all_notes
from app.database import create_db_and_tables
from app.routes import folders, notes
app = FastAPI(title="Simple Note API")
app = FastAPI(title="Notes API")
# CORS - adjust origins for production
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"], # Vite dev server
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class NoteIn(BaseModel):
title: str
body: str
@app.on_event("startup")
def on_startup():
create_db_and_tables()
class NoteOut(NoteIn):
id: int
app.include_router(notes.router, prefix="/api")
app.include_router(folders.router, prefix="/api")
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/notes", response_model=list[NoteOut])
def list_notes():
return get_all_notes()
@app.post("/notes", response_model=NoteOut, status_code=201)
def create_note(note: NoteIn):
# Very tiny validation you can expand later
if not note.title.strip():
raise HTTPException(status_code=400, detail="Title cannot be empty")
return add_note(note.title, note.body)
@app.get("/")
def root():
return {"message": "Notes API"}

View file

@ -1,16 +1,69 @@
# backend/app/models.py
from typing import Dict, List
from datetime import datetime
from typing import List, Optional
# Inmemory “database”
_notes: List[Dict] = [] # each note is a dict with id, title, body
from sqlmodel import Field, Relationship, SQLModel # type: ignore
def add_note(title: str, body: str) -> Dict:
note_id = len(_notes) + 1
note = {"id": note_id, "title": title, "body": body}
_notes.append(note)
return note
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)
# Relationships
parent: Optional["Folder"] = Relationship(
back_populates="children", sa_relationship_kwargs={"remote_side": "Folder.id"}
)
children: List["Folder"] = Relationship(back_populates="parent")
notes: List["Note"] = Relationship(back_populates="folder")
def get_all_notes() -> List[Dict]:
return _notes
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)
folder: Optional[Folder] = Relationship(back_populates="notes")
# API Response models (what gets sent to frontend)
class NoteRead(SQLModel):
id: int
title: str
content: str
folder_id: Optional[int] = None
created_at: datetime
updated_at: datetime
class FolderTreeNode(SQLModel):
id: int
name: str
notes: List[NoteRead] = []
children: List["FolderTreeNode"] = []
class FolderTreeResponse(SQLModel):
folders: List[FolderTreeNode]
orphaned_notes: List[NoteRead]
# Create/Update models
class NoteCreate(SQLModel):
title: str
content: str
folder_id: Optional[int] = None
class NoteUpdate(SQLModel):
title: Optional[str] = None
content: Optional[str] = None
folder_id: Optional[int] = None
class FolderCreate(SQLModel):
name: str
parent_id: Optional[int] = None

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,76 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException # type: ignore
from sqlmodel import Session, select # type: ignore
from app.database import get_session
from app.models import (
Folder,
FolderCreate,
FolderTreeNode,
FolderTreeResponse,
Note,
NoteRead,
)
router = APIRouter(prefix="/folders", tags=["folders"])
def build_folder_tree_node(folder: Folder) -> FolderTreeNode:
"""Recursively build a folder tree node with notes and children"""
return FolderTreeNode(
id=folder.id,
name=folder.name,
notes=[NoteRead.model_validate(note) for note in folder.notes],
children=[build_folder_tree_node(child) for child in folder.children],
)
@router.get("/tree", response_model=FolderTreeResponse)
def get_folder_tree(session: Session = Depends(get_session)):
"""Get complete folder tree with notes"""
# Get all top-level folders (parent_id is None)
top_level_folders = session.exec(
select(Folder).where(Folder.parent_id == None)
).all()
# Get all orphaned notes (folder_id is None)
orphaned_notes = session.exec(select(Note).where(Note.folder_id == None)).all()
# Build tree recursively
tree = [build_folder_tree_node(folder) for folder in top_level_folders]
return FolderTreeResponse(
folders=tree,
orphaned_notes=[NoteRead.model_validate(note) for note in orphaned_notes],
)
@router.get("/", response_model=List[Folder])
def list_folders(session: Session = Depends(get_session)):
"""Get flat list of all folders"""
folders = session.exec(select(Folder)).all()
return folders
@router.post("/", response_model=Folder)
def create_folder(folder: FolderCreate, session: Session = Depends(get_session)):
"""Create a new folder"""
db_folder = Folder.model_validate(folder)
session.add(db_folder)
session.commit()
session.refresh(db_folder)
return db_folder
@router.delete("/{folder_id}")
def delete_folder(folder_id: int, session: Session = Depends(get_session)):
"""Delete a folder"""
folder = session.get(Folder, folder_id)
if not folder:
raise HTTPException(status_code=404, detail="Folder not found")
session.delete(folder)
session.commit()
return {"message": "Folder deleted"}

View file

@ -0,0 +1,61 @@
from datetime import datetime
from app.database import get_session
from app.models import Note, NoteCreate, NoteUpdate
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
router = APIRouter(prefix="/notes", tags=["notes"])
@router.get("/")
def list_notes(session: Session = Depends(get_session)):
notes = session.exec(select(Note).order_by(Note.updated_at.desc())).all()
return notes
@router.post("/", response_model=Note)
def create_note(note: NoteCreate, session: Session = Depends(get_session)):
db_note = Note.model_validate(note)
session.add(db_note)
session.commit()
session.refresh(db_note)
return db_note
@router.get("/{note_id}", response_model=Note)
def get_note(note_id: int, session: Session = Depends(get_session)):
note = session.get(Note, note_id)
if not note:
raise HTTPException(status_code=404, detail="Note not found")
return note
@router.patch("/{note_id}", response_model=Note)
def update_note(
note_id: int, note_update: NoteUpdate, session: Session = Depends(get_session)
):
note = session.get(Note, note_id)
if not note:
raise HTTPException(status_code=404, detail="Note not found")
update_data = note_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(note, key, value)
note.updated_at = datetime.utcnow()
session.add(note)
session.commit()
session.refresh(note)
return note
@router.delete("/{note_id}")
def delete_note(note_id: int, session: Session = Depends(get_session)):
note = session.get(Note, note_id)
if not note:
raise HTTPException(status_code=404, detail="Note not found")
session.delete(note)
session.commit()
return {"message": "Note deleted"}

View file

BIN
backend/notes.db Normal file

Binary file not shown.

3
backend/pyproject.toml Normal file
View file

@ -0,0 +1,3 @@
{
"reportGeneralTypeIssues": "warning"
}

View file

@ -2,9 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>FastAPI+Vite Note Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Notes App</title>
</head>
<body>
<body style="margin: 0">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

File diff suppressed because it is too large Load diff

View file

@ -8,11 +8,22 @@
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.0",
"@mdxeditor/editor": "^3.49.3",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-router-dom": "^7.9.6",
"tailwindcss": "^4.1.17"
},
"devDependencies": {
"@catppuccin/tailwindcss": "^1.0.0",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"vite": "^5.4.21"
}

View file

@ -1,98 +1,19 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
// src/App.tsx
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home"; // existing home page
import { MarkdownPage } from "./pages/Markdown";
const App = () => (
<BrowserRouter>
{/* Simple nav you can replace with your own UI later */}
{/*<nav style={{ marginBottom: "1rem" }}>
<Link to="/">Home</Link> | <Link to="/markdown">MD</Link>
</nav>*/}
type Note = {
id: number;
title: string;
body: string;
};
<Routes>
<Route path="/" element={<Home />} />
<Route path="/markdown" element={<MarkdownPage />} />
</Routes>
</BrowserRouter>
);
export default function App() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [saved, setSaved] = useState<Note | null>(null);
const [allNotes, setAllNotes] = useState<Note[]>([]);
// Load existing notes on mount
useEffect(() => {
axios.get<Note[]>("/api/notes").then((res) => setAllNotes(res.data));
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const resp = await axios.post<Note>("/api/notes", { title, body });
setSaved(resp.data);
setAllNotes((prev) => [...prev, resp.data]);
setTitle("");
setBody("");
} catch (err) {
console.error(err);
alert("Failed to save note check console.");
}
};
return (
<div
style={{
maxWidth: "600px",
margin: "2rem auto",
fontFamily: "sans-serif",
}}
>
<h1>📝 Simple Note</h1>
<form onSubmit={handleSubmit} style={{ marginBottom: "2rem" }}>
<div>
<input
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
style={{ width: "100%", padding: "0.5rem", fontSize: "1.1rem" }}
/>
</div>
<div style={{ marginTop: "0.5rem" }}>
<textarea
placeholder="Body"
value={body}
onChange={(e) => setBody(e.target.value)}
rows={4}
style={{ width: "100%", padding: "0.5rem" }}
/>
</div>
<button
type="submit"
style={{ marginTop: "0.5rem", padding: "0.5rem 1rem" }}
>
Save note
</button>
</form>
{saved && (
<div
style={{
background: "#f0f8ff",
padding: "1rem",
marginBottom: "2rem",
}}
>
<strong>Saved:</strong> #{saved.id} {saved.title}
</div>
)}
<h2>All notes</h2>
{allNotes.length === 0 ? (
<p>No notes yet.</p>
) : (
<ul>
{allNotes.map((n) => (
<li key={n.id}>
<strong>{n.title}</strong>: {n.body}
</li>
))}
</ul>
)}
</div>
);
}
export default App;

View file

@ -0,0 +1,44 @@
import axios from "axios";
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api";
export interface Folder {
id: number;
name: string;
parent_id: number | null;
created_at: string;
}
export interface NoteRead {
id: number;
title: string;
content: string;
folder_id: number | null;
created_at: string;
updated_at: string;
}
export interface FolderTreeNode {
id: number;
name: string;
notes: NoteRead[];
children: FolderTreeNode[];
}
export interface FolderTreeResponse {
folders: FolderTreeNode[];
orphaned_notes: NoteRead[];
}
export interface FolderCreate {
name: string;
parent_id: number | null;
}
export const folderApi = {
tree: () => axios.get<FolderTreeResponse>(`${API_URL}/folders/tree`),
list: () => axios.get<Folder[]>(`${API_URL}/folders`),
create: (folder: FolderCreate) =>
axios.post<Folder>(`${API_URL}/folders`, folder),
delete: (id: number) => axios.delete(`${API_URL}/folders/${id}`),
};

View file

@ -0,0 +1,27 @@
import axios from "axios";
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api";
export interface Note {
id: number;
title: string;
folder_id?: number;
content: string;
created_at: string;
updated_at: string;
}
export interface NoteCreate {
title: string;
content: string;
folder_id: number | null;
}
export const notesApi = {
list: () => axios.get(`${API_URL}/notes`),
get: (id: number) => axios.get(`${API_URL}/notes/${id}`),
create: (note: NoteCreate) => axios.post(`${API_URL}/notes`, note),
update: (id: number, note: Partial) =>
axios.patch(`${API_URL}/notes/${id}`, note),
delete: (id: number) => axios.delete(`${API_URL}/notes/${id}`),
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,40 @@
import React from "react";
import { useDraggable } from "@dnd-kit/core";
import { Note } from "../../api/notes";
import { NoteRead } from "../../api/folders";
export const DraggableNote = ({
note,
selectNote,
selectedNote,
}: {
note: NoteRead;
selectNote: (note: NoteRead) => void;
selectedNote: NoteRead | null;
}) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: note.id,
data: { type: "note", note },
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
<div
key={note.id}
onClick={() => selectNote(note)}
className={`ml-5 rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${
selectedNote?.id === note.id
? "bg-ctp-mauve text-ctp-base"
: "hover:bg-ctp-surface1"
}`}
>
<span>{note.title}</span>
</div>
</button>
);
};

View file

@ -0,0 +1,40 @@
import React from "react";
import { useDroppable } from "@dnd-kit/core";
import { Folder, NoteRead } from "../../api/folders";
export const DroppableFolder = ({
folder,
setSelectedFolder,
selectedFolder,
selectedNote,
}: {
folder: Partial<Folder>;
setSelectedFolder: React.Dispatch<React.SetStateAction<number | null>>;
selectedFolder: number | null;
selectedNote: NoteRead | null;
}) => {
const { isOver, setNodeRef } = useDroppable({
id: folder.id!,
data: { type: "folder", folder },
});
const style = {
color: isOver ? "green" : undefined,
};
return (
<div ref={setNodeRef} style={style}>
<div
onClick={() => setSelectedFolder(folder.id as number)}
className={`font-semibold mb-1 flex items-center gap-1 px-2 py-1 rounded cursor-pointer ${
selectedFolder === folder.id &&
(selectedNote?.folder_id == folder.id || selectedNote == null)
? "bg-ctp-surface1"
: "hover:bg-ctp-surface0"
}`}
>
<i className="fadr fa-folder text-sm"></i>
{folder.name}
</div>
</div>
);
};

39
frontend/src/main.css Normal file
View file

@ -0,0 +1,39 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@import "@catppuccin/tailwindcss/macchiato.css";
/* Override MDXEditor and all its children */
[class*="mdxeditor"],
._mdxeditor-root-content-editable,
.mdxeditor-root-contenteditable,
div[contenteditable="true"] {
color: var(--ctp-text) !important;
}
/* Override prose specifically */
.prose,
.prose * {
color: var(--ctp-text) !important;
}
/* Override list markers */
.prose ul li::marker,
.prose ol li::marker,
ul li::marker,
ol li::marker {
color: var(--ctp-text) !important;
}
.my-class {
background-color: var(--ctp-mantle) !important;
border-bottom: 1px solid var(--ctp-surface2) !important;
}
.my-class button {
color: var(--ctp-text) !important;
background-color: var(--ctp-surface0) !important;
}
.mdxeditor-popup-container > * {
background-color: var(--ctp-base) !important;
}

View file

@ -1,5 +1,12 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./main.css";
import "./assets/fontawesome/js/duotone-regular.js";
import "./assets/fontawesome/js/fontawesome.js ";
createRoot(document.getElementById("root")!).render(<App />);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

302
frontend/src/pages/Home.tsx Normal file
View file

@ -0,0 +1,302 @@
import {
codeBlockPlugin,
codeMirrorPlugin,
headingsPlugin,
linkPlugin,
listsPlugin,
markdownShortcutPlugin,
MDXEditor,
quotePlugin,
SandpackConfig,
sandpackPlugin,
thematicBreakPlugin,
} from "@mdxeditor/editor";
import { useEffect, useRef, useState } from "react";
import {
folderApi,
FolderCreate,
FolderTreeNode,
FolderTreeResponse,
NoteRead,
} from "../api/folders";
import { NoteCreate, notesApi } from "../api/notes";
import "../main.css";
import { DndContext, DragEndEvent } from "@dnd-kit/core";
import "@mdxeditor/editor/style.css";
import { DroppableFolder } from "../components/sidebar/DroppableFolder";
import { DraggableNote } from "../components/sidebar/DraggableNote";
const simpleSandpackConfig: SandpackConfig = {
defaultPreset: "react",
presets: [
{
label: "React",
name: "react",
meta: "live react",
sandpackTemplate: "react",
sandpackTheme: "dark",
snippetFileName: "/App.js",
snippetLanguage: "jsx",
},
],
};
function Home() {
const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
const [selectedNote, setSelectedNote] = useState<NoteRead | null>(null);
const [title, setTitle] = useState("");
const [content, setContent] = useState("#");
const [newFolder, setNewFolder] = useState(false);
const [newFolderText, setNewFolderText] = useState("");
const [selectedFolder, setSelectedFolder] = useState<number | null>(null);
useEffect(() => {
loadFolderTree();
}, []);
const loadFolderTree = async () => {
const { data } = await folderApi.tree();
setFolderTree(data);
};
const handleCreate = async () => {
if (!title.trim()) return;
const newNote: NoteCreate = { title, content, folder_id: selectedFolder };
await notesApi.create(newNote);
setTitle("");
setContent("#");
loadFolderTree();
};
const handleCreateFolder = async () => {
if (!newFolderText.trim()) return;
const newFolderData: FolderCreate = {
name: newFolderText,
parent_id: null,
};
await folderApi.create(newFolderData);
setNewFolderText("");
loadFolderTree();
setNewFolder(false);
};
const handleUpdate = async () => {
if (!selectedNote) return;
await notesApi.update(selectedNote.id, { title, content });
setSelectedNote(null);
setTitle("");
setContent("#");
loadFolderTree();
};
const handleDelete = async (id: number) => {
await notesApi.delete(id);
loadFolderTree();
};
const selectNote = (note: NoteRead) => {
setSelectedNote(note);
setTitle(note.title);
setContent(note.content);
};
const clearSelection = () => {
setSelectedNote(null);
setTitle("");
setContent("#");
};
const newFolderRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (newFolder && newFolderRef.current) {
newFolderRef.current.focus();
}
}, [newFolder]);
const renderFolder = (folder: FolderTreeNode, depth: number = 0) => (
<div
key={folder.id}
style={{ marginLeft: depth > 0 ? "1rem" : "0" }}
className="flex flex-col"
>
<DroppableFolder
key={folder.id}
folder={folder}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
/>
{folder.notes.map((note) => (
<DraggableNote
key={note.id}
note={note}
selectNote={selectNote}
selectedNote={selectedNote}
/>
))}
{folder.children.map((child) => renderFolder(child, depth + 1))}
</div>
);
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
console.log(active.data);
console.log(over.data);
await notesApi.update(active.id as number, {
folder_id: over.id as number,
});
loadFolderTree();
};
return (
<DndContext onDragEnd={handleDragEnd} autoScroll={false}>
<div className="flex bg-ctp-base min-h-screen text-ctp-text">
<div
className="bg-ctp-mantle border-r-ctp-surface2 border-r overflow-hidden"
style={{
width: "300px",
padding: "1rem",
overflowY: "auto",
}}
onDragOver={(e) => e.preventDefault()} // Add this
onTouchMove={(e) => e.preventDefault()} // And this for touch devices
>
<h2>Notes</h2>
<button
onClick={clearSelection}
style={{ marginBottom: "1rem", width: "100%" }}
>
New Note
</button>
<div className="flex gap-2 mb-2">
<button
onClick={() => {
if (newFolder && newFolderRef.current) {
newFolderRef.current.focus();
}
setNewFolder(true);
}}
className="hover:bg-ctp-mauve group transition-colors rounded-md p-1 text-center flex"
>
<i className="fadr fa-folder-plus text-xl text-ctp-mauve group-hover:text-ctp-base transition-colors"></i>
</button>
<button
onClick={clearSelection}
className="hover:bg-ctp-mauve group transition-colors rounded-md p-1 text-center flex"
>
<i className="fadr fa-file-circle-plus text-xl text-ctp-mauve group-hover:text-ctp-base transition-colors"></i>
</button>
</div>
{newFolder && (
<div className="px-1 mb-1">
<input
onBlur={() => setNewFolder(false)}
onChange={(e) => setNewFolderText(e.target.value)}
value={newFolderText}
type="text"
placeholder="new folder"
className="border-ctp-mauve border rounded-md px-2 w-full focus:outline-none focus:ring-1 focus:ring-ctp-mauve focus:border-ctp-mauve bg-ctp-base"
ref={newFolderRef}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleCreateFolder();
}
}}
/>
</div>
)}
{/* Render folder tree */}
{folderTree?.folders.map((folder) => renderFolder(folder))}
{/* Render orphaned notes */}
{folderTree?.orphaned_notes &&
folderTree.orphaned_notes.length > 0 && (
<div className="mt-4">
<div className="text-ctp-subtext0 text-sm mb-1">Unsorted</div>
{folderTree.orphaned_notes.map((note) => (
<div
key={note.id}
onClick={() => selectNote(note)}
className={`rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${
selectedNote?.id === note.id
? "bg-ctp-mauve text-ctp-base"
: "hover:bg-ctp-surface1"
}`}
>
<i className="fadr fa-file text-xs"></i>
<span>{note.title}</span>
</div>
))}
</div>
)}
</div>
<div
style={{
flex: 1,
padding: "1rem",
display: "flex",
flexDirection: "column",
}}
>
<input
type="text"
placeholder="Note title..."
value={title}
onChange={(e) => setTitle(e.target.value)}
style={{
padding: "0.5rem",
marginBottom: "1rem",
fontSize: "1.5rem",
border: "1px solid #ccc",
}}
/>
<MDXEditor
markdown={content}
key={selectedNote?.id || "new"}
onChange={setContent}
className="prose text-ctp-text"
plugins={[
headingsPlugin(),
listsPlugin(),
quotePlugin(),
thematicBreakPlugin(),
linkPlugin(),
codeBlockPlugin({ defaultCodeBlockLanguage: "js" }),
sandpackPlugin({ sandpackConfig: simpleSandpackConfig }),
codeMirrorPlugin({
codeBlockLanguages: { js: "JavaScript", css: "CSS" },
}),
markdownShortcutPlugin(),
]}
/>
<div style={{ marginTop: "1rem", display: "flex", gap: "0.5rem" }}>
{selectedNote ? (
<>
<button onClick={handleUpdate}>Update Note</button>
<button
onClick={() => handleDelete(selectedNote.id)}
className="bg-ctp-red rounded-md px-1 text-ctp-crust"
>
Delete
</button>
<button onClick={clearSelection}>Cancel</button>
</>
) : (
<button onClick={handleCreate}>Create Note</button>
)}
</div>
</div>
</div>
</DndContext>
);
}
export default Home;

View file

@ -0,0 +1,68 @@
// src/pages/TestPage.tsx
import { FC } from "react";
import Markdown from "react-markdown";
import { MDXEditor, SandpackConfig } from "@mdxeditor/editor";
import {
headingsPlugin,
listsPlugin,
quotePlugin,
thematicBreakPlugin,
linkPlugin,
codeBlockPlugin,
codeMirrorPlugin,
sandpackPlugin,
markdownShortcutPlugin,
toolbarPlugin,
BoldItalicUnderlineToggles,
} from "@mdxeditor/editor";
import "@mdxeditor/editor/style.css";
const simpleSandpackConfig: SandpackConfig = {
defaultPreset: "react",
presets: [
{
label: "React",
name: "react",
meta: "live react",
sandpackTemplate: "react",
sandpackTheme: "dark",
snippetFileName: "/App.js",
snippetLanguage: "jsx",
},
],
};
export const MarkdownPage: FC = () => {
const markdown = `
# This is *perfect*!
- TestPage
- te
`;
return (
<MDXEditor
markdown={markdown}
plugins={[
toolbarPlugin({
toolbarClassName: "my-class",
toolbarContents: () => (
<>
<BoldItalicUnderlineToggles />
</>
),
}),
headingsPlugin(),
listsPlugin(),
quotePlugin(),
thematicBreakPlugin(),
linkPlugin(),
codeBlockPlugin({ defaultCodeBlockLanguage: "js" }),
sandpackPlugin({ sandpackConfig: simpleSandpackConfig }),
codeMirrorPlugin({
codeBlockLanguages: { js: "JavaScript", css: "CSS" },
}),
markdownShortcutPlugin(),
]}
/>
);
};

View file

@ -1,15 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"
export default defineConfig({
plugins: [react()],
plugins: [tailwindcss(), react()],
server: {
// Proxy API calls to the FastAPI container (or local dev server)
port: 5173,
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},