Add: add base frontend & docker compose

This commit is contained in:
Kirill Samoylenkov 2025-11-19 23:04:28 +05:00
parent 4d0fe108ff
commit 381cad66e6
7 changed files with 1908 additions and 0 deletions

12
frontend/Dockerfile Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

18
frontend/package.json Normal file
View 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
View 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
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 />);