diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..876107d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +.env +.venv/ +venv/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7342a5f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..6547074 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,79 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +import psycopg2 +import psycopg2.extras +import os +from typing import Optional + +app = FastAPI(title="Notes API") + +def get_db(): + return psycopg2.connect( + host=os.environ["DB_HOST"], + port=int(os.environ.get("DB_PORT", "5432")), + database=os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASSWORD"], + ) + +@app.on_event("startup") +def startup(): + conn = get_db() + cur = conn.cursor() + cur.execute(""" + CREATE TABLE IF NOT EXISTS notes ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + content TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + conn.commit() + cur.close() + conn.close() + +class NoteCreate(BaseModel): + title: str + content: Optional[str] = "" + +@app.get("/health") +def health(): + return {"status": "ok"} + +@app.get("/notes") +def get_notes(): + conn = get_db() + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute("SELECT * FROM notes ORDER BY created_at DESC") + notes = list(cur.fetchall()) + cur.close() + conn.close() + return notes + +@app.post("/notes") +def create_note(note: NoteCreate): + conn = get_db() + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + "INSERT INTO notes (title, content) VALUES (%s, %s) RETURNING *", + (note.title, note.content), + ) + new_note = dict(cur.fetchone()) + conn.commit() + cur.close() + conn.close() + return new_note + +@app.delete("/notes/{note_id}") +def delete_note(note_id: int): + conn = get_db() + cur = conn.cursor() + cur.execute("DELETE FROM notes WHERE id = %s", (note_id,)) + if cur.rowcount == 0: + cur.close() + conn.close() + raise HTTPException(status_code=404, detail="Note not found") + conn.commit() + cur.close() + conn.close() + return {"deleted": note_id} diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..1c4fa8a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.111.0 +uvicorn==0.29.0 +psycopg2-binary==2.9.9 +pydantic==2.7.1 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..94e094b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: "3.8" + +services: + db: + image: postgres:15 + environment: + POSTGRES_DB: notesdb + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + + backend: + build: ./backend + ports: + - "8000:8000" + environment: + DB_HOST: db + DB_PORT: 5432 + DB_NAME: notesdb + DB_USER: postgres + DB_PASSWORD: postgres + depends_on: + - db + + frontend: + build: ./frontend + ports: + - "8501:8501" + environment: + BACKEND_URL: http://backend:8000 + depends_on: + - backend + +volumes: + pgdata: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..ac8fd86 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8501 + +CMD ["streamlit", "run", "app.py", "--server.address", "0.0.0.0", "--server.port", "8501", "--server.headless", "true"] diff --git a/frontend/app.py b/frontend/app.py new file mode 100644 index 0000000..4104412 --- /dev/null +++ b/frontend/app.py @@ -0,0 +1,58 @@ +import streamlit as st +import requests +import os + +BACKEND_URL = os.environ.get("BACKEND_URL", "http://localhost:8000") + +st.set_page_config(page_title="Notes", page_icon="📝") +st.title("📝 Notes") + +# Add note +with st.form("add_note", clear_on_submit=True): + st.subheader("New Note") + title = st.text_input("Title") + content = st.text_area("Content", height=100) + submitted = st.form_submit_button("Add Note", use_container_width=True) + + if submitted: + if not title.strip(): + st.error("Title is required.") + else: + try: + r = requests.post( + f"{BACKEND_URL}/notes", + json={"title": title, "content": content}, + timeout=5, + ) + if r.status_code == 200: + st.success("Note added!") + st.rerun() + else: + st.error(f"Backend error: {r.text}") + except Exception as e: + st.error(f"Could not reach backend: {e}") + +st.divider() +st.subheader("All Notes") + +try: + r = requests.get(f"{BACKEND_URL}/notes", timeout=5) + notes = r.json() + + if not notes: + st.info("No notes yet. Add one above.") + else: + for note in notes: + with st.container(border=True): + col1, col2 = st.columns([5, 1]) + with col1: + st.markdown(f"**{note['title']}**") + if note.get("content"): + st.write(note["content"]) + st.caption(note["created_at"]) + with col2: + if st.button("Delete", key=f"del_{note['id']}"): + requests.delete(f"{BACKEND_URL}/notes/{note['id']}", timeout=5) + st.rerun() +except Exception as e: + st.error(f"Could not reach backend: {e}") diff --git a/frontend/requirements.txt b/frontend/requirements.txt new file mode 100644 index 0000000..b229e9a --- /dev/null +++ b/frontend/requirements.txt @@ -0,0 +1,2 @@ +streamlit==1.35.0 +requests==2.32.3