Add: add base frontend & docker compose
This commit is contained in:
parent
4d0fe108ff
commit
381cad66e6
7 changed files with 1908 additions and 0 deletions
30
docker-compose.yaml
Normal file
30
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
container_name: ${POSTGRES_HOST}
|
||||
ports:
|
||||
- 127.0.0.1:${POSTGRES_PORT}:${POSTGRES_PORT}
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASS}
|
||||
POSTGRES_DB: ${POSTGRES_NAME}
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
restart: always
|
||||
ports:
|
||||
- 8000:8000
|
||||
depends_on:
|
||||
- db
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
command: ["npm", "run", "dev", "--", "--host"]
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- 3000:5173
|
||||
12
frontend/Dockerfile
Normal file
12
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
FROM node:20
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package.json
|
||||
COPY package-lock.json package-lock.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
ENTRYPOINT []
|
||||
5
frontend/index.html
Normal file
5
frontend/index.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Demo</title></head>
|
||||
<body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body>
|
||||
</html>
|
||||
1746
frontend/package-lock.json
generated
Normal file
1746
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
frontend/package.json
Normal file
18
frontend/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 4173"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
92
frontend/src/App.jsx
Normal file
92
frontend/src/App.jsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "http://backend:8000";
|
||||
|
||||
export default function App(){
|
||||
const [items, setItems] = useState([]);
|
||||
const [title, setTitle] = useState("");
|
||||
const [desc, setDesc] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(()=> {
|
||||
let mounted = true;
|
||||
setLoading(true);
|
||||
fetch(`${API_BASE}/api/items`)
|
||||
.then(async res => {
|
||||
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(data => { if (mounted) setItems(data); })
|
||||
.catch(err => { if (mounted) setError(err.message); })
|
||||
.finally(()=> { if (mounted) setLoading(false); });
|
||||
return ()=> { mounted = false; };
|
||||
}, []);
|
||||
|
||||
async function submit(e){
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (!title.trim()) { setError("Title required"); return; }
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch(`${API_BASE}/api/items`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: title.trim(), description: desc || null })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Server ${res.status}: ${text}`);
|
||||
}
|
||||
const item = await res.json();
|
||||
setItems(prev => [item, ...prev]);
|
||||
setTitle(""); setDesc("");
|
||||
} catch (err) {
|
||||
setError(err.message ?? String(err));
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{maxWidth:800, margin:"2rem auto", fontFamily:"Arial"}}>
|
||||
<h1>Demo Items</h1>
|
||||
|
||||
<form onSubmit={submit} style={{marginBottom:20}}>
|
||||
<input
|
||||
value={title}
|
||||
onChange={e=>setTitle(e.target.value)}
|
||||
placeholder="Title"
|
||||
required
|
||||
style={{width:"100%", padding:8, marginBottom:8}}
|
||||
/>
|
||||
<textarea
|
||||
value={desc}
|
||||
onChange={e=>setDesc(e.target.value)}
|
||||
placeholder="Description"
|
||||
style={{width:"100%", padding:8, marginBottom:8}}
|
||||
/>
|
||||
<button type="submit" disabled={loading} style={{padding:"8px 12px"}}>
|
||||
{loading ? "Saving..." : "Add"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error && <div style={{color:"red", marginBottom:12}}>Error: {error}</div>}
|
||||
|
||||
{loading && items.length === 0 ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<ul>
|
||||
{items.map(it=>(
|
||||
<li key={it.id} style={{marginBottom:10}}>
|
||||
<strong>{it.title}</strong>
|
||||
{it.description && <div>{it.description}</div>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
frontend/src/main.jsx
Normal file
5
frontend/src/main.jsx
Normal 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 />);
|
||||
Loading…
Add table
Add a link
Reference in a new issue