[32788f42] feat(list-generator): create React app and FastAPI backend for PDF list generation

This commit is contained in:
2026-03-18 19:20:59 +00:00
parent 16cd760dac
commit 21c8ff66fd
30 changed files with 4525 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info fonts-liberation fontconfig && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

View File

@@ -0,0 +1,19 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import generate
app = FastAPI(title="List Generator API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(generate.router, prefix="/api")
@app.get("/health")
def health_check():
return {"status": "ok"}

View File

@@ -0,0 +1,46 @@
from fastapi import APIRouter, File, UploadFile, Form, HTTPException
from fastapi.responses import FileResponse
from typing import Optional
import os
import shutil
import tempfile
from ..services.pdf_generator import generate_school_pdf
router = APIRouter()
@router.post("/generate-list")
async def generate_list(
institution: str = Form(...),
date_info: str = Form(...),
list_type: str = Form("k"),
students_file: UploadFile = File(...),
families_file: Optional[UploadFile] = File(None)
):
try:
with tempfile.TemporaryDirectory() as temp_dir:
students_path = os.path.join(temp_dir, "students.csv")
with open(students_path, "wb") as buffer:
shutil.copyfileobj(students_file.file, buffer)
families_path = None
if families_file:
families_path = os.path.join(temp_dir, "families.csv")
with open(families_path, "wb") as buffer:
shutil.copyfileobj(families_file.file, buffer)
pdf_path = generate_school_pdf(
institution=institution,
date_info=date_info,
list_type=list_type,
students_csv_path=students_path,
families_csv_path=families_path,
output_dir=temp_dir
)
if not pdf_path or not os.path.exists(pdf_path):
raise HTTPException(status_code=500, detail="PDF generation failed")
with open(pdf_path, "rb") as f:
pdf_bytes = f.read()
final_output_path = os.path.join("/tmp", os.path.basename(pdf_path))
with open(final_output_path, "wb") as f:
f.write(pdf_bytes)
return FileResponse(path=final_output_path, filename=os.path.basename(pdf_path), media_type="application/pdf")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,45 @@
import pandas as pd
import os
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
import datetime
def generate_school_pdf(institution: str, date_info: str, list_type: str, students_csv_path: str, families_csv_path: str = None, output_dir: str = "/tmp") -> str:
# Read CSV data
df = pd.read_csv(students_csv_path, sep=";")
# Clean column names
df.columns = df.columns.str.strip()
# Group by class
grouped = df.groupby("Klasse")
class_data = []
for class_name, group in grouped:
students = group.to_dict("records")
class_data.append({"name": class_name, "students": students})
# Prepare summary data
class_counts = [{"name": c, "count": len(g)} for c, g in grouped]
total_students = sum(c["count"] for c in class_counts)
# Setup Jinja2 environment
template_dir = os.path.join(os.path.dirname(__file__), "..", "templates")
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template("school_list.html")
# Render HTML
current_time = datetime.datetime.now().strftime("%d.%m.%Y %H:%M Uhr")
html_out = template.render(
institution=institution,
date_info=date_info,
class_counts=class_counts,
total_students=total_students,
class_data=class_data,
current_time=current_time
)
# Generate PDF
output_filename = f"Listen_{institution.replace( , _)}_{list_type}_{datetime.datetime.now().strftime(%Y-%m-%d_%H-%M)}.pdf"
output_path = os.path.join(output_dir, output_filename)
HTML(string=html_out).write_pdf(output_path)
return output_path

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><style>
@page { size: A4 portrait; margin: 20mm; }
body { font-family: sans-serif; font-size: 11pt; }
.header { margin-bottom: 20px; }
.institution-name { font-weight: bold; font-size: 14pt; }
.date-info { font-size: 12pt; }
.summary { margin-top: 30px; }
.summary h2 { font-size: 12pt; font-weight: normal; margin-bottom: 10px; }
.summary-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
.summary-table td { padding: 4px 0; }
.summary-total { margin-top: 10px; border-top: 1px solid black; padding-top: 10px; font-weight: bold; }
.class-section { page-break-before: always; }
.student-table { width: 100%; border-collapse: collapse; margin-top: 30px; }
.student-table th { text-align: left; border-bottom: 1px solid black; padding-bottom: 5px; font-weight: normal; }
.student-table td { padding: 5px 0; }
.class-summary { margin-top: 30px; font-weight: bold; }
.class-note { margin-top: 20px; font-size: 10pt; }
.footer { position: fixed; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; font-size: 10pt; }
.footer-left { text-align: left; }
.footer-right { text-align: right; white-space: pre-wrap; }
</style></head><body>
<div class="header"><div class="institution-name">{{ institution }}</div><div class="date-info">{{ date_info }}</div></div>
<div class="summary"><h2>Übersicht der Anmeldungen:</h2><table class="summary-table">
{% for count in class_counts %}
<tr><td style="width: 50%;">Klasse {{ count.name }}</td><td>{{ count.count }} Anmeldungen</td></tr>
{% endfor %}
</table><div class="summary-total">Gesamt: {{ total_students }} Anmeldungen</div></div>
{% for class_info in class_data %}
<div class="class-section">
<div class="header"><div class="institution-name">{{ institution }}</div><div class="date-info">{{ date_info }}</div></div>
<table class="student-table"><thead><tr><th style="width: 40%">Nachname</th><th style="width: 40%">Vorname</th><th style="width: 20%">Klasse</th></tr></thead><tbody>
{% for student in class_info.students %}
<tr><td>{{ student.Nachname }}</td><td>{{ student.Vorname }}</td><td>{{ student.Klasse }}</td></tr>
{% endfor %}
</tbody></table>
<div class="class-summary">{{ class_info.students|length }} angemeldete Kinder</div>
<div class="class-note">Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden<br>Schüler an die Anmeldung erinnern.</div>
</div>
{% endfor %}
<div class="footer"><div class="footer-left">Stand {{ current_time }}</div><div class="footer-right">Kinderfotos Erding\nGartenstr. 10 85445 Oberding\nwww.kinderfotos-erding.de\n08122-8470867</div></div>
</body></html>

View File

@@ -0,0 +1,6 @@
fastapi[all]==0.111.0
uvicorn==0.30.1
pandas==2.2.2
python-multipart==0.0.9
weasyprint==62.1
jinja2==3.1.4

View File

@@ -0,0 +1,3 @@
import pandas as pd
df = pd.DataFrame({'Nachname': ['Müller', 'Schmidt'], 'Vorname': ['Max', 'Anna'], 'Klasse': ['1a', '1b']})
df.to_csv('test.csv', sep=';', index=False)

View File

@@ -0,0 +1,26 @@
version: "3.8"
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
volumes:
- ./backend:/app
- data-volume:/app/data
ports:
- "8008:8000"
environment:
- HOST=0.0.0.0
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3008:80"
depends_on:
- backend
volumes:
data-volume:

24
list-generator/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,12 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

3299
list-generator/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"autoprefixer": "^10.4.27",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "^8.0.0"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -0,0 +1,83 @@
import { useState } from "react";
import "./App.css";
function App() {
const [institution, setInstitution] = useState("");
const [dateInfo, setDateInfo] = useState("");
const [listType, setListType] = useState("k");
const [studentsFile, setStudentsFile] = useState<File | null>(null);
const [familiesFile, setFamiliesFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleGenerate = async (e: React.FormEvent) => {
e.preventDefault();
if (!studentsFile) return alert("Bitte Namensliste (CSV) hochladen!");
setIsLoading(true);
const formData = new FormData();
formData.append("institution", institution);
formData.append("date_info", dateInfo);
formData.append("list_type", listType);
formData.append("students_file", studentsFile);
if (familiesFile) formData.append("families_file", familiesFile);
try {
const API_URL = "";
const response = await fetch(`${API_URL}/api/generate-list`, {
method: "POST",
body: formData,
});
if (!response.ok) throw new Error("Fehler bei der Generierung");
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `Listen_${institution.replace(/\s+/g, "_")}_${listType}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
} catch (err) {
alert("Es gab einen Fehler: " + err);
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-xl mx-auto">
<div className="bg-white py-8 px-6 shadow rounded-lg sm:px-10">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">Listentool</h1>
<p className="mt-1 text-sm text-gray-500">Generiere Fotoauftragslisten aus CSV Dateien.</p>
</div>
<form className="space-y-6" onSubmit={handleGenerate}>
<div>
<label className="block text-sm font-medium text-gray-700">Einrichtung / Event</label>
<input type="text" required value={institution} onChange={(e) => setInstitution(e.target.value)} placeholder="Grundschule Klettham, Erding" className="mt-1 block w-full border-gray-300 rounded-md shadow-sm border p-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Datum (Anzeige auf PDF)</label>
<input type="text" required value={dateInfo} onChange={(e) => setDateInfo(e.target.value)} placeholder="07.+09.07.2025" className="mt-1 block w-full border-gray-300 border p-2 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Namensliste (CSV)</label>
<input type="file" required accept=".csv" onChange={(e) => setStudentsFile(e.target.files?.[0] || null)} className="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
</div>
<button type="submit" disabled={isLoading} className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50">
{isLoading ? "Generiere..." : "PDF Generieren & Herunterladen"}
</button>
</form>
</div>
</div>
</div>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

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

View File

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

521
list-generator/sample.txt Normal file
View File

@@ -0,0 +1,521 @@
Grundschule Klettham, Erding
07.+09.07.2025
Übersicht der Anmeldungen:
Klasse 1a 24 Anmeldungen
Klasse 1b 18 Anmeldungen
Klasse 1c 19 Anmeldungen
Klasse 2a 18 Anmeldungen
Klasse 2b 17 Anmeldungen
Klasse 2c 22 Anmeldungen
Klasse 3a 13 Anmeldungen
Klasse 3b 20 Anmeldungen
Klasse 3c 22 Anmeldungen
Klasse 4a 18 Anmeldungen
Klasse 4b 16 Anmeldungen
Klasse 4c 19 Anmeldungen
--------------------
Gesamt: 226 Anmeldungen
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Anker Margarethe 1a
Bauer Christian 1a
Ben Ahmed Hayet 1a
Carăba Iannis David 1a
Daradics Lorant Ármin 1a
Feraru-Bucataru Emily 1a
Gabrielmichael Lydia 1a
Gashi Learta 1a
Halilović Aziz 1a
Igbinosa Zoe 1a
Incebacak Ishak Emir 1a
Jakesevic Stefany 1a
Jost Danna 1a
Juszczyk Franciszek 1a
Korotaj Gael 1a
Mustafa Alimi Djejlan Alimi 1a
Nafissi Rosha 1a
Penkov Emily 1a
Rosenboom Felix 1a
Saadeddin Maram 1a
Staufer Leopold 1a
Tanz Jasmin 1a
Veliqi Luan 1a
Winter Felix 1a
24 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Ahmetovic Adnan 1b
Bablick Josephine 1b
Donauer Moritz 1b
Eddine Yaakob 1b
Eddiness as Yaakob 1b
Eken Emir 1b
Ianis Simion 1b
Ismair Franziska 1b
Jankovsky Xaver 1b
Kiermaier Alexia 1b
Kiermaier Paulina 1b
Kühne Felix 1b
Maierhofer Luis 1b
Reisinger Lukas 1b
Riester Constantin 1b
Spohnholtz Julian 1b
von Eynatten Hanna 1b
Zecheru Marius - Nicolae 1b
18 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Akyildiz Linus 1c
Arkhypov Herman 1c
Bauer Alma 1c
Bozova Ebrar 1c
Chouaibi Adam 1c
Gazic Leyla 1c
Gemsjäger Lea 1c
Goia Sophia Cristina 1c
Hutzler Leon 1c
Kaiser Leni 1c
Kirchner Matilda 1c
Mujakovic Bilal 1c
Pojsl Leni 1c
Schell Leonard 1c
Schäfer Johannes 1c
Sijamhodzic Ahmed 1c
Vogl Noah 1c
Weiße Mina 1c
Yilmaz Melek 1c
19 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Alimi Elif 2a
Andrei Diana 2a
Bereczki David 2a
Braune Nele 2a
Darici Sare 2a
Eshetu Sosnadawit Tadelo 2a
Humpok Nila Norina 2a
Kainz Emily Monique 2a
Kamal Mubashir 2a
Khankan Hamza 2a
Korkut Lara Eva 2a
Kos Ana 2a
Lajqi Suela 2a
Redzepi Arsa 2a
Rosenboom James 2a
Sljivar Hana 2a
Toader Olivia 2a
Wodarczyk Florian 2a
18 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Baris Anton 2b
Bauer Moritz 2b
Borkovic Toma 2b
Cinoglu Eymen 2b
Geßner Julian 2b
Gutwirth Isabella 2b
Huemer Lucia 2b
Javorovac Mahir 2b
Kinolli Valentin 2b
Leeb Luise 2b
Machura Mia 2b
Mathar Sebastian 2b
Scharrer Kayra 2b
Stark Alexander 2b
van Niekerk Gavin 2b
Walter Nicolas 2b
Weber Antonia 2b
17 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Adam Lorena 2c
Bargmann Sebastian 2c
Derr Xaver 2c
Döll Mia Sophie 2c
Falke Justus 2c
Forsch Gloria 2c
Garagasheva Meriem 2c
Gui Milan Matei 2c
Jell Leo 2c
Kaess Maximilian 2c
Leiner Franziska 2c
Meisel Paulina Luise 2c
Meisel Valentin Luka 2c
Peipe Livia 2c
Roth Matthias 2c
Schroff Sarah 2c
Schwinghammer Felix 2c
Seidl Emma 2c
Sordel Niko 2c
Soylak Yahya 2c
Wiesmüller Raphael 2c
ÖZDEMIR Rüzgar Ali 2c
22 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Aladag Mirza 3a
Alibabic Lejla 3a
Eckelt-Marins Kevin 3a
Gengatharan Thiyaan 3a
Graf Jonas 3a
Igbinosa Zion 3a
Jehl Luca 3a
Kaleem Fatimah 3a
Kpegouni Mashoud 3a
Ritondale Giada 3a
Sattler Jakob 3a
Sijamhodzic Ahmed 3a
Veliqi Lorena 3a
13 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Atas Tümer 3b
Aydogan Beren 3b
Bablick Valentina 3b
Ballesto Mario 3b
Borkovic Marko 3b
Demircan Ebru 3b
Derr Emma 3b
Ferstl Jakob 3b
Groneberg Lena 3b
Huber Eva 3b
Irl Luisa 3b
Jankovsky Anton 3b
Kretzschmar Lily 3b
Matei Arjan Elson 3b
Nnaji Great 3b
Rebecca Candeloro 3b
Reichwein Quirin 3b
Schroff Samuel 3b
Staufer Konstantin 3b
Stojanovska Melani 3b
20 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Ahmetovic Sadik 3c
Ascher Anton 3c
Atol Garren 3c
Babos Zoey 3c
Brandt Marik 3c
Civaoglu Ekrem 3c
Cruz dos Santos Mina 3c
Demiröz Alya Ada 3c
Donauer Emil 3c
Eken Eslem 3c
Ismair Veronika 3c
Karamanoglu Baran 3c
Keliel Malik 3c
Licina Daris 3c
Rodigast Mats 3c
Schmidt Daniel 3c
Stark Julia 3c
Steiner Tim 3c
Tende Rhema 3c
Tepis Lucas 3c
Vogl Rafael 3c
Wanninger Milena Frieda 3c
22 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Alimi Lejla 4a
Andrei Eduard 4a
Balzer Katharina 4a
Cadraki Noel 4a
Dudukovic Bojan 4a
Gashi Amina 4a
Hutzler Levin 4a
Junghänel Vincent 4a
Kabashi Loris 4a
Konak Yusuf Can 4a
Kuqani Yll 4a
Nuhagic Dajla 4a
Rath Shikhar 4a
Sabunchi Betül 4a
schelhorn Lorijan 4a
Stanic Mila 4a
Stemmer Luna 4a
Wodarczyk Magdalena 4a
18 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Baris John 4b
Bauer Jonathan 4b
Buljubasic Omar 4b
Camur Ela 4b
Groneberg Clara 4b
Keskin Noah 4b
Konrad Maria 4b
Konrad Sophia 4b
Kraeusel Felix 4b
Lechner Isabella 4b
Lupascu Nicoleta andreea 4b
Molnár Júlia 4b
Rama Retian 4b
Rodigast Mila 4b
Steiner Lena 4b
Thieme Lena 4b
16 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867
Grundschule Klettham, Erding
07.+09.07.2025
Nachname Vorname Klasse
Gaßner Emma 4c
Gelzinger Maximilian 4c
Halasz Felix 4c
Hilal memedi Hilal 4c
Huber Laura 4c
Kiermaier Juliana 4c
Kinolli Elian 4c
Kohls Carolin 4c
Kohls Erik 4c
Kohls Lennard 4c
Kravtsova Maiia 4c
Lechner Isabella 4c
Matvii Kobakhidze 4c
Moreira Wolf Leonardo 4c
Raysa Simion 4c
Reis Sophie 4c
Riester Marie 4c
Romaisae Agharbi 4c
Schubert Lukas 4c
19 angemeldete Kinder
Dies ist die Liste der bereits angemeldeten Schüler. Bitte die noch fehlenden
Schüler an die Anmeldung erinnern.
Stand 04.07.2025 06:24 Uhr
Kinderfotos Erding
Gartenstr. 10 85445 Oberding
www.kinderfotos-erding.de
08122-8470867