This commit is contained in:
james fitzsimons 2025-11-19 21:16:32 +00:00
commit c79ac06b58
16 changed files with 2180 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

13
backend/Dockerfile Normal file
View file

@ -0,0 +1,13 @@
# ---- 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
# ---- 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"]

0
backend/app/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

34
backend/app/main.py Normal file
View file

@ -0,0 +1,34 @@
# backend/app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from .models import add_note, get_all_notes
app = FastAPI(title="Simple Note API")
class NoteIn(BaseModel):
title: str
body: str
class NoteOut(NoteIn):
id: int
@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)

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

@ -0,0 +1,16 @@
# backend/app/models.py
from typing import Dict, List
# Inmemory “database”
_notes: List[Dict] = [] # each note is a dict with id, title, body
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
def get_all_notes() -> List[Dict]:
return _notes

0
backend/app/schemas.py Normal file
View file

54
backend/requirements.txt Normal file
View file

@ -0,0 +1,54 @@
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.11.0
bcrypt==5.0.0
certifi==2025.11.12
cffi==2.0.0
click==8.3.1
cryptography==46.0.3
dnspython==2.8.0
ecdsa==0.19.1
email-validator==2.3.0
fastapi==0.121.3
fastapi-cli==0.0.16
fastapi-cloud-cli==0.4.0
fastar==0.6.0
greenlet==3.2.4
h11==0.16.0
httpcore==1.0.9
httptools==0.7.1
httpx==0.28.1
idna==3.11
Jinja2==3.1.6
markdown-it-py==4.0.0
MarkupSafe==3.0.3
mdurl==0.1.2
passlib==1.7.4
pyasn1==0.6.1
pycparser==2.23
pydantic==2.12.4
pydantic_core==2.41.5
Pygments==2.19.2
python-dotenv==1.2.1
python-jose==3.5.0
python-multipart==0.0.20
PyYAML==6.0.3
rich==14.2.0
rich-toolkit==0.16.0
rignore==0.7.6
rsa==4.9.1
sentry-sdk==2.45.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.44
sqlmodel==0.0.27
starlette==0.50.0
typer==0.20.0
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.5.0
uvicorn==0.38.0
uvloop==0.22.1
watchfiles==1.1.1
websockets==15.0.1

11
frontend/index.html Normal file
View file

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>FastAPI+Vite Note Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1913
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

19
frontend/package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "note-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.7.0",
"vite": "^5.4.21"
}
}

98
frontend/src/App.tsx Normal file
View file

@ -0,0 +1,98 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
type Note = {
id: number;
title: string;
body: string;
};
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>
);
}

5
frontend/src/main.tsx Normal file
View file

@ -0,0 +1,5 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(<App />);

16
frontend/vite.config.ts Normal file
View file

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