fix docker, add base routing, header + drawer

This commit is contained in:
Marcus Almert
2025-10-28 17:33:39 +01:00
parent dbf54f59ef
commit b0a42de301
18 changed files with 1461 additions and 214 deletions

View File

@@ -2,15 +2,8 @@ FROM python:3.11-slim
WORKDIR /app
# Dependencies installieren
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Source-Code kopieren
COPY src/ ./src
# Port öffnen
EXPOSE 8000
# FastAPI starten (Uvicorn)
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
# Quellcode mounten (Hot Reload)
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

View File

@@ -4,20 +4,20 @@ services:
backend:
build:
context: ./backend
container_name: fastapi-backend
dockerfile: ./backend/Dockerfile
ports:
- "8000:8000"
volumes:
- ./backend/src:/app/src:Z
environment:
- PYTHONUNBUFFERED=1
command: uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
- ./backend:/app
frontend:
build:
context: ./frontend
container_name: react-frontend
ports:
- "3000:80"
depends_on:
- backend
- "5173:5173"
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- CHOKIDAR_USEPOLLING=true
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@@ -1,24 +1,13 @@
# --- Build Stage ---
FROM node:20 AS build
# --- frontend/Dockerfile.dev ---
FROM node:20-alpine
WORKDIR /app
# 1. Nur package.json zuerst kopieren → Cache für Dependencies
COPY package*.json ./
RUN npm install
# 2. Abhängigkeiten installieren
RUN npm ci
# Source wird gemountet – kein COPY nötig
EXPOSE 5173
# 3. Restlichen Code kopieren
COPY . .
# 4. Sicherstellen, dass vite ausführbar ist (Windows-Berechtigungsfix)
RUN chmod +x node_modules/.bin/vite
# 5. Frontend bauen
RUN npm run build
# --- Serve Stage ---
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]
# Kein ENTRYPOINT, direkt CMD
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

File diff suppressed because it is too large Load Diff

View File

@@ -11,18 +11,26 @@
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.4",
"@mui/material": "^7.3.4",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.4"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"vite": "^7.1.7"
"typescript": "^5.9.3",
"vite": "^7.1.12"
}
}

View File

@@ -4,39 +4,3 @@
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,13 +0,0 @@
import "./App.css";
function App() {
return (
<>
<div>
<h1>MitM Webserver</h1>
</div>
</>
);
}
export default App;

15
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { Routes, Route } from "react-router-dom";
import "./App.css";
import Header from "./header/Header";
import Home from "./pages/Home";
export default function App() {
return (
<>
<Header />
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</>
);
}

View File

@@ -0,0 +1,93 @@
import React from "react";
import {
AppBar,
Toolbar,
Typography,
IconButton,
Drawer,
List,
ListItem,
ListItemButton,
ListItemText,
Box,
useTheme,
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import { useNavigate, useLocation, To } from "react-router-dom";
const pages = [{ name: "Home", path: "/" }];
export default function Header() {
const [drawerOpen, setDrawerOpen] = React.useState(false);
const theme = useTheme();
const navigate = useNavigate();
const location = useLocation();
const toggleDrawer = (open: boolean) => () => setDrawerOpen(open);
const handleNavigate = (path: To) => {
navigate(path);
setDrawerOpen(false);
};
const drawerList = (
<Box
sx={{
width: 250,
height: "100%",
bgcolor: theme.palette.background.default,
color: theme.palette.text.primary,
}}
role="presentation"
onKeyDown={toggleDrawer(false)}
>
<Typography
variant="h6"
sx={{
textAlign: "center",
my: 2,
fontWeight: "bold",
color: theme.palette.primary.main,
}}
>
MitM Webserver
</Typography>
<List>
{pages.map(({ name, path }) => (
<ListItem key={name} disablePadding>
<ListItemButton
onClick={() => handleNavigate(path)}
selected={location.pathname === path}
>
<ListItemText primary={name} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
);
return (
<>
<AppBar position="fixed" color="primary" elevation={2}>
<Toolbar>
<IconButton color="inherit" edge="start" onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
component="div"
sx={{ flexGrow: 1, fontWeight: "bold", cursor: "pointer" }}
onClick={() => handleNavigate("/")}
>
MitM Webserver
</Typography>
</Toolbar>
</AppBar>
<Toolbar /> {/* spacing */}
<Drawer anchor="left" open={drawerOpen} onClose={toggleDrawer(false)}>
{drawerList}
</Drawer>
</>
);
}

View File

@@ -1,68 +1,89 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
/* === Reset & base === */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
font-synthesis: none;
text-rendering: optimizeLegibility;
/* Root background and typography (matches theme background + font) */
body {
font-family: "Inter", "Roboto", "Helvetica", "Arial", sans-serif;
background-color: #f9f9f9; /* matches theme.palette.background.default */
color: #262626; /* matches theme.palette.text.primary */
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Anchor defaults */
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
color: #1976d2; /* primary main */
text-decoration: none;
transition: color 0.2s ease-in-out;
}
a:hover {
color: #535bf2;
color: #004ba0; /* primary dark */
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
/* === Scrollbar styling (WebKit browsers) === */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
::-webkit-scrollbar-track {
background: #f0f0f0;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
::-webkit-scrollbar-thumb {
background: #bfbfbf;
border-radius: 5px;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
::-webkit-scrollbar-thumb:hover {
background: #737373;
}
/* === Utility classes === */
/* Center container with padding */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
/* Add subtle fade animation for route transitions */
.fade-enter {
opacity: 0;
transform: translateY(10px);
}
.fade-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
}
/* === Code blocks / preformatted text === */
pre,
code {
background-color: #f0f0f0;
color: #262626;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", monospace;
}
/* === App container fix for fixed header === */
main {
padding-top: 64px; /* matches MUI AppBar height */
}

View File

@@ -1,10 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

21
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.js";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { BrowserRouter } from "react-router-dom";
import theme from "./theme";
const rootElement = document.getElementById("root");
if (rootElement) {
createRoot(rootElement).render(
<StrictMode>
<ThemeProvider theme={theme}>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>
</StrictMode>
);
}

View File

@@ -0,0 +1,14 @@
import { Container, Typography, Button } from "@mui/material";
export default function Home() {
return (
<Container style={{ textAlign: "center", marginTop: "4rem" }}>
<Typography variant="h4" gutterBottom>
🚀 Hello MUI + React!
</Typography>
<Button variant="contained" color="primary">
Klick mich!
</Button>
</Container>
);
}

111
frontend/src/theme.tsx Normal file
View File

@@ -0,0 +1,111 @@
import { createTheme } from "@mui/material/styles";
const theme = createTheme({
palette: {
mode: "light",
// === Primary colors (blue shades) ===
primary: {
main: "#1976d2", // medium blue
light: "#63a4ff", // light blue
dark: "#004ba0", // deep blue
contrastText: "#ffffff",
},
// === Secondary colors (gray shades) ===
secondary: {
main: "#737373", // neutral medium gray
light: "#bfbfbf", // lighter gray
dark: "#404040", // darker gray
contrastText: "#ffffff",
},
// === Background colors ===
background: {
default: "#f9f9f9", // soft gray-white for page background
paper: "#ffffff", // white for cards, modals, etc.
},
// === Text colors ===
text: {
primary: "#262626", // almost black
secondary: "#737373", // muted gray
},
// === Divider ===
divider: "#d9d9d9",
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
h1: { fontWeight: 700, fontSize: "2.5rem", color: "#262626" },
h2: { fontWeight: 600, fontSize: "2rem", color: "#262626" },
h3: { fontWeight: 600, fontSize: "1.75rem", color: "#262626" },
body1: { fontSize: "1rem", lineHeight: 1.6, color: "#262626" },
button: {
textTransform: "none",
fontWeight: 500,
color: "#ffffff",
},
},
shape: {
borderRadius: 10,
},
components: {
MuiAppBar: {
styleOverrides: {
root: {
backgroundColor: "#1976d2", // primary main
color: "#ffffff",
},
},
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
textTransform: "none",
fontWeight: 500,
"&:hover": {
backgroundColor: "#004ba0", // darker blue
},
},
},
},
MuiPaper: {
styleOverrides: {
root: {
borderRadius: 0,
backgroundColor: "#ffffff",
color: "#262626",
boxShadow:
"0px 2px 8px rgba(0, 0, 0, 0.05), 0px 1px 2px rgba(0, 0, 0, 0.1)",
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: "#f0f0f0", // subtle gray drawer background
color: "#262626",
},
},
},
MuiListItemButton: {
styleOverrides: {
root: {
"&.Mui-selected": {
backgroundColor: "#e0e0e0",
"&:hover": {
backgroundColor: "#d0d0d0",
},
},
},
},
},
},
});
export default theme;

19
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

11
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
watch: {
usePolling: true,
interval: 300, // alle 300ms nach Änderungen suchen
},
});