initial webapp draft
This commit is contained in:
commit
4f2723b767
22 changed files with 713 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
.env
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /cost-assistant
|
||||
|
||||
# Copy requirements first (for caching)
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the project files
|
||||
COPY . .
|
||||
|
||||
# Create upload folder
|
||||
RUN mkdir -p data/uploads
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run FastAPI with Uvicorn
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
11
README.md
Normal file
11
README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# papercut-lite-printshop
|
||||
|
||||
Minimal Flask-based PDF print-cost calculator with optional ink-coverage adjustment.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
Then open http://localhost:8000 and upload PDFs. Configure rate via env var RATE_PER_M2 (default 4.0).
|
||||
BIN
__pycache__/main.cpython-311.pyc
Normal file
BIN
__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
87
cost_calculator.py
Normal file
87
cost_calculator.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# cost_calculator.py
|
||||
import os
|
||||
from PyPDF2 import PdfReader
|
||||
from pdf2image import convert_from_path
|
||||
import numpy as np
|
||||
|
||||
UPLOAD_FOLDER = "data/uploads"
|
||||
ALLOWED_EXTENSIONS = {"pdf"}
|
||||
|
||||
|
||||
def allowed_file(filename: str) -> bool:
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
def points_to_meters(points: float) -> float:
|
||||
return (points / 72.0) * 0.0254
|
||||
|
||||
|
||||
def get_rate_black() -> float:
|
||||
return float(os.environ.get("RATE_PER_M2_BLACK", "4.0"))
|
||||
|
||||
|
||||
def get_rate_color() -> float:
|
||||
return float(os.environ.get("RATE_PER_M2_COLOR", "5.0"))
|
||||
|
||||
|
||||
def analyze_pdf(path, compute_ink=True):
|
||||
reader = PdfReader(path)
|
||||
pages_info = []
|
||||
total_area_black = total_area_color = 0.0
|
||||
total_cost_black = total_cost_color = 0.0
|
||||
|
||||
for i, page in enumerate(reader.pages):
|
||||
box = page.mediabox
|
||||
width_m = points_to_meters(float(box.width))
|
||||
height_m = points_to_meters(float(box.height))
|
||||
area = width_m * height_m
|
||||
|
||||
ink_pct = None
|
||||
is_color = False
|
||||
|
||||
if compute_ink:
|
||||
try:
|
||||
images = convert_from_path(path, first_page=i + 1, last_page=i + 1, dpi=150)
|
||||
img = images[0].convert("RGB")
|
||||
arr = np.array(img)
|
||||
|
||||
# ink pixels: any channel < 240
|
||||
ink_mask = np.any(arr < 240, axis=2)
|
||||
ink_pct = float(np.count_nonzero(ink_mask)) / (arr.shape[0] * arr.shape[1]) * 100.0
|
||||
|
||||
# simple color detection: if RGB channels differ significantly
|
||||
avg_rgb = arr.mean(axis=(0, 1))
|
||||
if np.ptp(avg_rgb) > 30:
|
||||
is_color = True
|
||||
except Exception as e:
|
||||
print(f"Page {i+1} ink/color calc failed: {e}")
|
||||
ink_pct = None
|
||||
|
||||
if is_color:
|
||||
rate = get_rate_color()
|
||||
total_area_color += area
|
||||
total_cost_color += area * rate
|
||||
else:
|
||||
rate = get_rate_black()
|
||||
total_area_black += area
|
||||
total_cost_black += area * rate
|
||||
|
||||
pages_info.append({
|
||||
"page": i + 1,
|
||||
"width_m": width_m,
|
||||
"height_m": height_m,
|
||||
"area_m2": area,
|
||||
"ink_pct": ink_pct,
|
||||
"is_color": is_color,
|
||||
"cost": round(area * rate, 2)
|
||||
})
|
||||
|
||||
return {
|
||||
"filename": os.path.basename(path),
|
||||
"pages": pages_info,
|
||||
"total_area_black": total_area_black,
|
||||
"total_area_color": total_area_color,
|
||||
"total_cost_black": round(total_cost_black, 2),
|
||||
"total_cost_color": round(total_cost_color, 2),
|
||||
"grand_total": round(total_cost_black + total_cost_color, 2)
|
||||
}
|
||||
BIN
data/uploads/Nachweis Arbeitsverhältnis Socius.pdf
Normal file
BIN
data/uploads/Nachweis Arbeitsverhältnis Socius.pdf
Normal file
Binary file not shown.
BIN
data/uploads/Poster AD (A3).pdf
Normal file
BIN
data/uploads/Poster AD (A3).pdf
Normal file
Binary file not shown.
BIN
data/uploads/Screen AD.pdf
Normal file
BIN
data/uploads/Screen AD.pdf
Normal file
Binary file not shown.
BIN
data/uploads/Socius_Arbeitsvertrag.pdf
Normal file
BIN
data/uploads/Socius_Arbeitsvertrag.pdf
Normal file
Binary file not shown.
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
container_name: cost-dashboard
|
||||
ports:
|
||||
- "8000:8000"
|
||||
working_dir: /cost-assistant
|
||||
volumes:
|
||||
- .:/cost-assistant # live reload for code/templates/static
|
||||
- ./data/uploads:/cost-assistant/data/uploads # persistent uploads
|
||||
env_file:
|
||||
- .env # RATE_PER_M2_BLACK and RATE_PER_M2_COLOR
|
||||
command: python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
62
main.py
Normal file
62
main.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# main.py
|
||||
import os
|
||||
import shutil
|
||||
from fastapi import FastAPI, UploadFile, File, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from cost_calculator import allowed_file, analyze_pdf, get_rate_black, get_rate_color, UPLOAD_FOLDER
|
||||
|
||||
app = FastAPI()
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def welcome(request: Request):
|
||||
return templates.TemplateResponse("landing.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/about", response_class=HTMLResponse)
|
||||
async def about(request: Request):
|
||||
return templates.TemplateResponse("about.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/cost", response_class=HTMLResponse)
|
||||
async def cost_dashboard(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"cost-calculator.html",
|
||||
{
|
||||
"request": request,
|
||||
"rate_black": get_rate_black(),
|
||||
"rate_color": get_rate_color()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/upload")
|
||||
async def upload_file(request: Request, file: UploadFile = File(...)):
|
||||
if not allowed_file(file.filename):
|
||||
return templates.TemplateResponse(
|
||||
"cost-calculator.html",
|
||||
{"request": request, "error": "Unsupported file type. Only PDF allowed."},
|
||||
)
|
||||
|
||||
path = os.path.join(UPLOAD_FOLDER, file.filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
result = analyze_pdf(path)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"result.html",
|
||||
{
|
||||
"request": request,
|
||||
"result": result,
|
||||
"rate_black": get_rate_black(),
|
||||
"rate_color": get_rate_color()
|
||||
},
|
||||
)
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fastapi
|
||||
uvicorn[standard]
|
||||
jinja2
|
||||
python-multipart
|
||||
pypdf2
|
||||
pdf2image
|
||||
Pillow
|
||||
numpy
|
||||
98
static/css/style.css
Normal file
98
static/css/style.css
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
@font-face {
|
||||
font-family: "SISTEMAS";
|
||||
src: url("/static/fonts/SISTEMAS FONT BT.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "BauPro";
|
||||
src: url("/static/fonts/BauPro.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "SISTEMAS", sans-serif;
|
||||
}
|
||||
|
||||
body, p, input, button, label, a {
|
||||
font-family: "BauPro", sans-serif;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #F9F9F9;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #001F4D; /* navy blue */
|
||||
color: #FFD600; /* yellow text */
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "SISTEMAS", sans-serif;
|
||||
color: #001F4D; /* navy blue */
|
||||
}
|
||||
|
||||
a.link-card {
|
||||
display: block;
|
||||
padding: 1.2em 1em;
|
||||
background-color: #E6007E; /* magenta */
|
||||
color: #FFD600; /* yellow text */
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
a.link-card:hover {
|
||||
background-color: #FFD600; /* yellow hover */
|
||||
color: #001F4D; /* navy text on hover */
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.7em;
|
||||
background-color: #E6007E; /* magenta */
|
||||
color: #FFD600; /* yellow text */
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #FFD600; /* yellow hover */
|
||||
color: #001F4D; /* navy text */
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
background-color: #001f3f; /* navy */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
BIN
static/fonts/BauPro.ttf
Normal file
BIN
static/fonts/BauPro.ttf
Normal file
Binary file not shown.
BIN
static/fonts/SISTEMAS FONT BT.ttf
Normal file
BIN
static/fonts/SISTEMAS FONT BT.ttf
Normal file
Binary file not shown.
BIN
static/images/logo.png
Normal file
BIN
static/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
103
templates/about.html
Normal file
103
templates/about.html
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<!-- templates/about.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Über uns – Studio EinsZwoVier</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
/* Override colors for this page */
|
||||
header {
|
||||
background-color: #001F4D; /* navy */
|
||||
color: #FFD600; /* yellow */
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #001F4D; /* navy headers */
|
||||
}
|
||||
a {
|
||||
color: #E6007E; /* magenta links */
|
||||
}
|
||||
a:hover {
|
||||
color: #FFD600;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 3em auto;
|
||||
background: #fff;
|
||||
padding: 2em;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 4em;
|
||||
color: #555;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Studio EinsZwoVier Makerspace</h1>
|
||||
<p>Am Gabriele-von-Bülow-Gymnasium</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<section>
|
||||
<h2>Über den Maker Space</h2>
|
||||
<p>Seit Dezember 2024 trägt unser Maker Space den Namen <strong>studio einszwovier</strong>. Er ist ein innovativer, digitaler Lernraum, der Kreativität, Technik und Bildungsgerechtigkeit verbindet. Hier wird „Making“ erlebbar: Lernende gestalten ihren Lernprozess aktiv, entdecken individuelle Stärken und erleben durch Selbstwirksamkeit besondere Motivation.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Ausstattung</h2>
|
||||
<ul>
|
||||
<li><strong>3D-Drucker:</strong> Für Modelle und Prototypen.</li>
|
||||
<li><strong>Lasercutter:</strong> Präzises Schneiden und Gravieren von Materialien.</li>
|
||||
<li><strong>Microcontroller:</strong> Elektronik und Programmierung.</li>
|
||||
<li><strong>Holzbearbeitung:</strong> Handwerkliche Projekte.</li>
|
||||
<li><strong>Textildruckgeräte:</strong> Kreative Designs auf Stoffen.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Betreuungsteam</h2>
|
||||
<p>Betreut wird der Maker Space von <strong>Aron Petau</strong> und <strong>Friedrich Weber</strong>. Sie sind montags bis mittwochs von 11:00 bis 15:00 Uhr vor Ort. Einfach vorbeischauen, Ideen vorstellen und loslegen!</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Öffnungszeiten</h2>
|
||||
<p>Dienstag bis Donnerstag: 11:00 – 16:00 Uhr<br>
|
||||
Raum 124, Gabriele-von-Bülow-Gymnasium</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Kontakt</h2>
|
||||
<p>E-Mail: <a href="mailto:einszwovier@gvb-gymnasium.de">einszwovier@gvb-gymnasium.de</a></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Aktuelle Kurse</h2>
|
||||
<ul>
|
||||
<li><strong>Löten und Leuchten:</strong> Herstellung von Nachttischleuchten mit 3D-Design und Löttechnik.</li>
|
||||
<li><strong>Die Vogelvilla:</strong> Bau von Vogelhäusern mit Lasercutter und Holzbearbeitung.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Standort</h2>
|
||||
<p>Gabriele-von-Bülow-Gymnasium<br>
|
||||
Tile-Brügge-Weg 63, 13509 Berlin (Tegel)<br>
|
||||
Telefon: 030 21 00 52 460<br>
|
||||
E-Mail: <a href="mailto:info@gvb-gymnasium.de">info@gvb-gymnasium.de</a></p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
© 2025 Studio EinsZwoVier – Gabriele-von-Bülow-Gymnasium
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
24
templates/base.html
Normal file
24
templates/base.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Studio Einszwovier{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="/" class="logo-link">
|
||||
<img src="/static/images/logo.png" alt="Studio Einszwovier Logo" class="logo">
|
||||
</a>
|
||||
<nav>
|
||||
<a href="/">Home</a> |
|
||||
<a href="/about">About</a> |
|
||||
<a href="/cost">Cost Calculator</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
81
templates/cost-calculator.html
Normal file
81
templates/cost-calculator.html
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!-- templates/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Print Cost Calculator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 700px;
|
||||
margin: 3em auto;
|
||||
background: #fff;
|
||||
padding: 2em;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #444;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
input[type="file"] {
|
||||
padding: 0.5em;
|
||||
}
|
||||
button {
|
||||
padding: 0.7em;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.rate-info {
|
||||
text-align: center;
|
||||
color: #555;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Print Cost Calculator</h1>
|
||||
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="/upload" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="file" accept="application/pdf" required />
|
||||
<button type="submit">Upload & Calculate</button>
|
||||
</form>
|
||||
|
||||
<p class="rate-info">
|
||||
Rates are fixed via environment variables:<br>
|
||||
B&W: {{ rate_black if rate_black else 'RATE_PER_M2_BLACK' }} € / m²,
|
||||
Color: {{ rate_color if rate_color else 'RATE_PER_M2_COLOR' }} € / m²
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
122
templates/landing.html
Normal file
122
templates/landing.html
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!-- templates/landing.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Studio Einszwovier</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
/* Body & layout */
|
||||
body {
|
||||
font-family: 'BauPro', sans-serif;
|
||||
background-color: #f0f4f8;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #001f3f;
|
||||
/* navy text */
|
||||
}
|
||||
|
||||
/* Header with logo */
|
||||
header {
|
||||
width: 100%;
|
||||
background-color: #001f3f;
|
||||
/* navy */
|
||||
color: #fff;
|
||||
padding: 1.5em 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0.5em 0 0;
|
||||
font-family: 'SISTEMAS', sans-serif;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
header p {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
color: #ff00ff;
|
||||
/* magenta accent for subtitle */
|
||||
}
|
||||
|
||||
.logo-link img {
|
||||
height: 80px;
|
||||
margin-bottom: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Main container with links */
|
||||
.container {
|
||||
max-width: 700px;
|
||||
width: 90%;
|
||||
margin: 3em auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
/* Link cards */
|
||||
a.link-card {
|
||||
display: block;
|
||||
padding: 1.2em 1em;
|
||||
background-color: #fff;
|
||||
color: #001f3f;
|
||||
/* navy text */
|
||||
text-decoration: none;
|
||||
border-left: 6px solid #ff00ff;
|
||||
/* magenta accent */
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
a.link-card:hover {
|
||||
background-color: #ff0;
|
||||
/* yellow hover */
|
||||
color: #001f3f;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 4em;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<a href="/" class="logo-link">
|
||||
<img src="/static/images/logo.png" alt="Studio Einszwovier Logo">
|
||||
</a>
|
||||
<h1>Studio Einszwovier</h1>
|
||||
<p>Welcome to the school makerspace portal at the GvB</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<a class="link-card" href="/about">About us</a>
|
||||
<a class="link-card" href="/cost">Print Cost Calculator</a>
|
||||
<a class="link-card" href="mailto:einszwovier@gvb-gymnasium.de">Contact: einszwovier@gvb-gymnasium.de</a>
|
||||
<a class="link-card" href="https://gvb-gymnasium.de">Log in to iServ</a>
|
||||
<a class="link-card" href="https://www.gvb-berlin.de">School website</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
© Studio Einszwovier @ GvB Berlin
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
74
templates/result.html
Normal file
74
templates/result.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!-- templates/result.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Print Cost Result</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2em; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 1em; }
|
||||
th, td { border: 1px solid #ccc; padding: 0.5em; text-align: center; }
|
||||
th { background-color: #f2f2f2; }
|
||||
.color { background-color: #ffdede; }
|
||||
.black { background-color: #e0e0ff; }
|
||||
.totals { font-weight: bold; }
|
||||
.container { max-width: 900px; margin: auto; }
|
||||
h1, h2 { text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Print Cost for {{ result.filename }}</h1>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Page</th>
|
||||
<th>Width (m)</th>
|
||||
<th>Height (m)</th>
|
||||
<th>Area (m²)</th>
|
||||
<th>Ink %</th>
|
||||
<th>Type</th>
|
||||
<th>Cost (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for page in result.pages %}
|
||||
<tr class="{{ 'color' if page.is_color else 'black' }}">
|
||||
<td>{{ page.page }}</td>
|
||||
<td>{{ "%.3f"|format(page.width_m) }}</td>
|
||||
<td>{{ "%.3f"|format(page.height_m) }}</td>
|
||||
<td>{{ "%.4f"|format(page.area_m2) }}</td>
|
||||
<td>{{ "%.1f"|format(page.ink_pct) if page.ink_pct is not none else '-' }}</td>
|
||||
<td>{{ 'Color' if page.is_color else 'B&W' }}</td>
|
||||
<td>{{ "%.2f"|format(page.cost) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="totals">
|
||||
<td colspan="3">Total Black Pages</td>
|
||||
<td>{{ "%.4f"|format(result.total_area_black) }}</td>
|
||||
<td colspan="2"></td>
|
||||
<td>{{ "%.2f"|format(result.total_cost_black) }}</td>
|
||||
</tr>
|
||||
<tr class="totals">
|
||||
<td colspan="3">Total Color Pages</td>
|
||||
<td>{{ "%.4f"|format(result.total_area_color) }}</td>
|
||||
<td colspan="2"></td>
|
||||
<td>{{ "%.2f"|format(result.total_cost_color) }}</td>
|
||||
</tr>
|
||||
<tr class="totals">
|
||||
<td colspan="6">Grand Total</td>
|
||||
<td>{{ "%.2f"|format(result.grand_total) }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<p style="text-align:center; margin-top:2em;">
|
||||
<a href="/">Upload another PDF</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue