Bulk update
This commit is contained in:
parent
c79ac06b58
commit
9645f411f3
29 changed files with 12284 additions and 215 deletions
|
|
@ -1,13 +1,10 @@
|
||||||
# ---- Build stage (optional) ----
|
FROM python:3.11-slim
|
||||||
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
|
|
||||||
|
|
||||||
# ---- Runtime stage ----
|
|
||||||
FROM python:3.12-slim
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
|
||||||
COPY app ./app
|
COPY requirements.txt .
|
||||||
EXPOSE 8000
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
CMD ["uvicorn", "app.main:app", "--port", "8000"]
|
|
||||||
|
COPY ./app ./app
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
|
||||||
BIN
backend/app/__pycache__/database.cpython-314.pyc
Normal file
BIN
backend/app/__pycache__/database.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
16
backend/app/database.py
Normal file
16
backend/app/database.py
Normal 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
|
||||||
|
|
@ -1,34 +1,30 @@
|
||||||
# backend/app/main.py
|
from fastapi import FastAPI # type: ignore
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi.middleware.cors import CORSMiddleware # type:ignore
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
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):
|
@app.on_event("startup")
|
||||||
title: str
|
def on_startup():
|
||||||
body: str
|
create_db_and_tables()
|
||||||
|
|
||||||
|
|
||||||
class NoteOut(NoteIn):
|
app.include_router(notes.router, prefix="/api")
|
||||||
id: int
|
app.include_router(folders.router, prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/")
|
||||||
def health():
|
def root():
|
||||||
return {"status": "ok"}
|
return {"message": "Notes API"}
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,69 @@
|
||||||
# backend/app/models.py
|
from datetime import datetime
|
||||||
from typing import Dict, List
|
from typing import List, Optional
|
||||||
|
|
||||||
# In‑memory “database”
|
from sqlmodel import Field, Relationship, SQLModel # type: ignore
|
||||||
_notes: List[Dict] = [] # each note is a dict with id, title, body
|
|
||||||
|
|
||||||
|
|
||||||
def add_note(title: str, body: str) -> Dict:
|
class Folder(SQLModel, table=True): # type: ignore
|
||||||
note_id = len(_notes) + 1
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
note = {"id": note_id, "title": title, "body": body}
|
name: str = Field(max_length=255)
|
||||||
_notes.append(note)
|
parent_id: Optional[int] = Field(default=None, foreign_key="folder.id")
|
||||||
return note
|
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]:
|
class Note(SQLModel, table=True): # type: ignore
|
||||||
return _notes
|
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
|
||||||
|
|
|
||||||
BIN
backend/app/routes/__pycache__/folders.cpython-314.pyc
Normal file
BIN
backend/app/routes/__pycache__/folders.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/app/routes/__pycache__/notes.cpython-314.pyc
Normal file
BIN
backend/app/routes/__pycache__/notes.cpython-314.pyc
Normal file
Binary file not shown.
76
backend/app/routes/folders.py
Normal file
76
backend/app/routes/folders.py
Normal 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"}
|
||||||
61
backend/app/routes/notes.py
Normal file
61
backend/app/routes/notes.py
Normal 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"}
|
||||||
BIN
backend/notes.db
Normal file
BIN
backend/notes.db
Normal file
Binary file not shown.
3
backend/pyproject.toml
Normal file
3
backend/pyproject.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"reportGeneralTypeIssues": "warning"
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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>
|
</head>
|
||||||
<body>
|
<body style="margin: 0">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
4750
frontend/package-lock.json
generated
4750
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,11 +8,22 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"axios": "^1.13.2",
|
||||||
"react": "^18.3.1",
|
"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": {
|
"devDependencies": {
|
||||||
|
"@catppuccin/tailwindcss": "^1.0.0",
|
||||||
|
"@types/react": "^19.2.6",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"vite": "^5.4.21"
|
"vite": "^5.4.21"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,19 @@
|
||||||
import React, { useState, useEffect } from "react";
|
// src/App.tsx
|
||||||
import axios from "axios";
|
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 = {
|
<Routes>
|
||||||
id: number;
|
<Route path="/" element={<Home />} />
|
||||||
title: string;
|
<Route path="/markdown" element={<MarkdownPage />} />
|
||||||
body: string;
|
</Routes>
|
||||||
};
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
export default function App() {
|
export default 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
44
frontend/src/api/folders.tsx
Normal file
44
frontend/src/api/folders.tsx
Normal 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}`),
|
||||||
|
};
|
||||||
27
frontend/src/api/notes.tsx
Normal file
27
frontend/src/api/notes.tsx
Normal 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}`),
|
||||||
|
};
|
||||||
3690
frontend/src/assets/fontawesome/js/duotone-regular.js
Normal file
3690
frontend/src/assets/fontawesome/js/duotone-regular.js
Normal file
File diff suppressed because one or more lines are too long
3051
frontend/src/assets/fontawesome/js/fontawesome.js
Normal file
3051
frontend/src/assets/fontawesome/js/fontawesome.js
Normal file
File diff suppressed because one or more lines are too long
40
frontend/src/components/sidebar/DraggableNote.tsx
Normal file
40
frontend/src/components/sidebar/DraggableNote.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
40
frontend/src/components/sidebar/DroppableFolder.tsx
Normal file
40
frontend/src/components/sidebar/DroppableFolder.tsx
Normal 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
39
frontend/src/main.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
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
302
frontend/src/pages/Home.tsx
Normal 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;
|
||||||
68
frontend/src/pages/Markdown.tsx
Normal file
68
frontend/src/pages/Markdown.tsx
Normal 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(),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [tailwindcss(), react()],
|
||||||
server: {
|
server: {
|
||||||
// Proxy API calls to the FastAPI container (or local dev server)
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8000",
|
target: "http://localhost:8000",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Loading…
Reference in a new issue