first commit
This commit is contained in:
44
README.md
Normal file
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Generátor katalogu
|
||||||
|
|
||||||
|
Generuje PDF katalog z YAML nebo CSV dat přes HTML/CSS šablonu.
|
||||||
|
|
||||||
|
## Instalace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # Windows: .venv\\Scripts\\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
> Poznámka: WeasyPrint může na některých systémech vyžadovat systémové knihovny. Pokud by instalace zlobila, lze později přepnout renderer na Playwright/Chromium.
|
||||||
|
|
||||||
|
## Použití
|
||||||
|
|
||||||
|
Z YAML:
|
||||||
|
```bash
|
||||||
|
python generate_catalog.py --input catalog.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Z CSV:
|
||||||
|
```bash
|
||||||
|
python generate_catalog.py --input catalog.csv --title "BistroUšky" --subtitle "CAKE AND PATISSERIE"
|
||||||
|
```
|
||||||
|
|
||||||
|
Výstup je standardně `output/katalog.pdf`.
|
||||||
|
|
||||||
|
## Struktura dat
|
||||||
|
|
||||||
|
- `brand`: název, podtitulek, logo, pozadí, ikony
|
||||||
|
- `settings`: rozložení, počet sloupců, výstupní soubor
|
||||||
|
- `categories`: seznam kategorií
|
||||||
|
- `products`: produkty v kategoriích
|
||||||
|
|
||||||
|
Každý produkt má minimálně:
|
||||||
|
- `name`
|
||||||
|
- `price`
|
||||||
|
- `image`
|
||||||
|
|
||||||
|
Volitelně:
|
||||||
|
- `note`
|
||||||
|
- `tags`
|
||||||
|
- `show_allergen_icon`
|
||||||
81
README_FRONTEND.md
Normal file
81
README_FRONTEND.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Webový frontend pro generátor katalogu
|
||||||
|
|
||||||
|
## Instalace na Debianu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt update
|
||||||
|
apt install -y \
|
||||||
|
python3 python3-venv python3-pip \
|
||||||
|
libpango-1.0-0 libpangoft2-1.0-0 libcairo2 \
|
||||||
|
libgdk-pixbuf-2.0-0 libffi8 shared-mime-info fonts-dejavu-core
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spuštění
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/catalog_generator
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Pak otevři:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://192.168.50.112:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formát CSV
|
||||||
|
|
||||||
|
Povinné sloupce:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
category,name,price,image
|
||||||
|
Klasické zákusky,Věneček,"85,-",venecek.jpg
|
||||||
|
Dortíky,Red Velvet,"145,-",red-velvet.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Volitelné sloupce:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
note,tags,show_allergen_icon
|
||||||
|
```
|
||||||
|
|
||||||
|
Ve sloupci `image` může být jen název souboru (`venecek.jpg`) nebo cesta `images/venecek.jpg`.
|
||||||
|
|
||||||
|
## Nahrávání fotek
|
||||||
|
|
||||||
|
Frontend podporuje:
|
||||||
|
|
||||||
|
- více samostatných obrázků najednou,
|
||||||
|
- jeden ZIP archiv s obrázky.
|
||||||
|
|
||||||
|
Podporované formáty: JPG, JPEG, PNG, WebP.
|
||||||
|
|
||||||
|
## Systemd služba
|
||||||
|
|
||||||
|
Soubor `/etc/systemd/system/catalog-generator.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Catalog Generator Web App
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/opt/catalog_generator
|
||||||
|
ExecStart=/opt/catalog_generator/venv/bin/python /opt/catalog_generator/app.py
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Aktivace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now catalog-generator
|
||||||
|
systemctl status catalog-generator
|
||||||
|
```
|
||||||
BIN
__pycache__/generate_catalog.cpython-313.pyc
Normal file
BIN
__pycache__/generate_catalog.cpython-313.pyc
Normal file
Binary file not shown.
96
app.py
Normal file
96
app.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, send_file, abort
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
UPLOAD_DIR = BASE_DIR / "uploads"
|
||||||
|
OUTPUT_DIR = BASE_DIR / "web_output"
|
||||||
|
|
||||||
|
UPLOAD_DIR.mkdir(exist_ok=True)
|
||||||
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def index():
|
||||||
|
return render_template("upload.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/generate")
|
||||||
|
def generate():
|
||||||
|
job_id = uuid.uuid4().hex
|
||||||
|
job_dir = UPLOAD_DIR / job_id
|
||||||
|
images_dir = job_dir / "images"
|
||||||
|
job_dir.mkdir(parents=True)
|
||||||
|
images_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
csv_file = request.files.get("csv_file")
|
||||||
|
if not csv_file or not csv_file.filename:
|
||||||
|
return "Chybí CSV soubor", 400
|
||||||
|
|
||||||
|
csv_path = job_dir / "catalog.csv"
|
||||||
|
csv_file.save(csv_path)
|
||||||
|
|
||||||
|
for image in request.files.getlist("images"):
|
||||||
|
if image and image.filename:
|
||||||
|
image.save(images_dir / Path(image.filename).name)
|
||||||
|
|
||||||
|
zip_file = request.files.get("images_zip")
|
||||||
|
if zip_file and zip_file.filename:
|
||||||
|
zip_path = job_dir / "images.zip"
|
||||||
|
zip_file.save(zip_path)
|
||||||
|
with zipfile.ZipFile(zip_path, "r") as z:
|
||||||
|
z.extractall(images_dir)
|
||||||
|
|
||||||
|
output_pdf = OUTPUT_DIR / f"catalog_{job_id}.pdf"
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"python",
|
||||||
|
str(BASE_DIR / "generate_catalog.py"),
|
||||||
|
"-i",
|
||||||
|
str(csv_path),
|
||||||
|
"--output",
|
||||||
|
str(output_pdf),
|
||||||
|
"--images-dir",
|
||||||
|
str(images_dir),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=BASE_DIR,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return (
|
||||||
|
"<h2>Generování selhalo</h2>"
|
||||||
|
"<h3>STDOUT</h3><pre>" + result.stdout + "</pre>"
|
||||||
|
"<h3>STDERR</h3><pre>" + result.stderr + "</pre>"
|
||||||
|
), 500
|
||||||
|
|
||||||
|
if not output_pdf.exists():
|
||||||
|
return "PDF se nevytvořilo.", 500
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<h1>Katalog je hotový</h1>
|
||||||
|
<p><a href="/download/{job_id}">Stáhnout PDF</a></p>
|
||||||
|
<p><a href="/">Vygenerovat další</a></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/download/<job_id>")
|
||||||
|
def download(job_id):
|
||||||
|
pdf_path = OUTPUT_DIR / f"catalog_{job_id}.pdf"
|
||||||
|
if not pdf_path.exists():
|
||||||
|
abort(404)
|
||||||
|
return send_file(pdf_path, as_attachment=True, download_name="catalog.pdf")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=8080)
|
||||||
BIN
assets/allergen.png
Normal file
BIN
assets/allergen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/background.png
Executable file
BIN
assets/background.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 277 KiB |
4
catalog.csv
Normal file
4
catalog.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
category,name,price,image,show_allergen_icon,note
|
||||||
|
Klasické zákusky,Likérová špička,"103,-",images/likerova_spicka.jpg,true,
|
||||||
|
Klasické zákusky,Laskonka - mini,"45,-",images/laskonka_mini.jpg,true,
|
||||||
|
Dortíky,Red Velvet,"145,-",images/red_velvet.jpg,true,
|
||||||
|
35
catalog.yaml
Normal file
35
catalog.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
brand:
|
||||||
|
title: "BistroUšky"
|
||||||
|
subtitle: "CAKE AND PATISSERIE"
|
||||||
|
cover_logo: "static/logo.png" # volitelné
|
||||||
|
background: "static/background.jpg" # volitelné, použije se na každé stránce
|
||||||
|
allergen_icon: "static/allergen_icon.png" # volitelné
|
||||||
|
|
||||||
|
settings:
|
||||||
|
page_size: "landscape-a4" # landscape-a4 / portrait-a4
|
||||||
|
products_per_page: 8 # při 4 sloupcích a 2 řádcích
|
||||||
|
columns: 4
|
||||||
|
show_prices: true
|
||||||
|
currency_suffix: ""
|
||||||
|
output_file: "output/katalog.pdf"
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- name: "Klasické zákusky"
|
||||||
|
products:
|
||||||
|
- name: "Likérová špička"
|
||||||
|
price: "103,-"
|
||||||
|
image: "images/likerova_spicka.jpg"
|
||||||
|
tags: []
|
||||||
|
note: ""
|
||||||
|
show_allergen_icon: true
|
||||||
|
- name: "Laskonka - mini"
|
||||||
|
price: "45,-"
|
||||||
|
image: "images/laskonka_mini.jpg"
|
||||||
|
show_allergen_icon: true
|
||||||
|
|
||||||
|
- name: "Dortíky"
|
||||||
|
products:
|
||||||
|
- name: "Red Velvet"
|
||||||
|
price: "145,-"
|
||||||
|
image: "images/red_velvet.jpg"
|
||||||
|
show_allergen_icon: true
|
||||||
184
generate_catalog.py
Normal file
184
generate_catalog.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from weasyprint import HTML
|
||||||
|
|
||||||
|
|
||||||
|
def load_csv(csv_path):
|
||||||
|
categories = {}
|
||||||
|
|
||||||
|
with open(csv_path, newline="", encoding="utf-8-sig") as f:
|
||||||
|
sample = f.read(4096)
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
dialect = csv.Sniffer().sniff(sample, delimiters=",;")
|
||||||
|
reader = csv.DictReader(f, dialect=dialect)
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
normalized = {
|
||||||
|
(key or "").strip().lower().replace(" ", "_"): (value or "").strip()
|
||||||
|
for key, value in row.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
category = (
|
||||||
|
normalized.get("category")
|
||||||
|
or normalized.get("catagory")
|
||||||
|
or normalized.get("kategorie")
|
||||||
|
or "Ostatní"
|
||||||
|
)
|
||||||
|
|
||||||
|
name = (
|
||||||
|
normalized.get("name")
|
||||||
|
or normalized.get("nazev")
|
||||||
|
or normalized.get("název")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
price = (
|
||||||
|
normalized.get("price")
|
||||||
|
or normalized.get("cena")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
image = (
|
||||||
|
normalized.get("image")
|
||||||
|
or normalized.get("foto")
|
||||||
|
or normalized.get("fotka")
|
||||||
|
or normalized.get("obrazek")
|
||||||
|
or normalized.get("obrázek")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
show_allergen_icon = (
|
||||||
|
normalized.get("show_alergen_icon")
|
||||||
|
or normalized.get("show_allergen_icon")
|
||||||
|
or normalized.get("alergen_icon")
|
||||||
|
or normalized.get("allergen_icon")
|
||||||
|
or ""
|
||||||
|
).lower() in ("1", "true", "yes", "ano", "y")
|
||||||
|
|
||||||
|
note = (
|
||||||
|
normalized.get("note")
|
||||||
|
or normalized.get("description")
|
||||||
|
or normalized.get("poznamka")
|
||||||
|
or normalized.get("poznámka")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
if category not in categories:
|
||||||
|
categories[category] = []
|
||||||
|
|
||||||
|
categories[category].append({
|
||||||
|
"name": name,
|
||||||
|
"price": price,
|
||||||
|
"image": image,
|
||||||
|
"show_allergen_icon": show_allergen_icon,
|
||||||
|
"description": note,
|
||||||
|
})
|
||||||
|
|
||||||
|
return categories
|
||||||
|
|
||||||
|
|
||||||
|
def build_catalog_data(title, subtitle, categories):
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"subtitle": subtitle,
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"products": products,
|
||||||
|
}
|
||||||
|
for name, products in categories.items()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_html(data, template_dir, images_dir):
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(template_dir)
|
||||||
|
)
|
||||||
|
|
||||||
|
template = env.get_template("catalog.html.j2")
|
||||||
|
return template.render(
|
||||||
|
catalog=data,
|
||||||
|
brand=data,
|
||||||
|
settings={
|
||||||
|
"columns": 4,
|
||||||
|
"page_size": "1024px 768px",
|
||||||
|
"margin": "0",
|
||||||
|
},
|
||||||
|
images_dir=images_dir.as_posix(),
|
||||||
|
assets_dir=(Path(__file__).resolve().parent / "assets").as_posix(),
|
||||||
|
logo_exists=(Path(__file__).resolve().parent / "assets" / "logo.png").exists(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_pdf(html_content, output_path):
|
||||||
|
HTML(string=html_content).write_pdf(output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--input",
|
||||||
|
required=True,
|
||||||
|
help="CSV input file"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
default="output/catalog.pdf",
|
||||||
|
help="Output PDF path"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--images-dir",
|
||||||
|
default="images",
|
||||||
|
help="Directory with product images"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--title",
|
||||||
|
default="Bistro Ušky",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--subtitle",
|
||||||
|
default="Cake & Patisserie",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
base_dir = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
csv_path = Path(args.input)
|
||||||
|
output_path = Path(args.output)
|
||||||
|
images_dir = Path(args.images_dir)
|
||||||
|
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
categories = load_csv(csv_path)
|
||||||
|
|
||||||
|
catalog_data = build_catalog_data(
|
||||||
|
args.title,
|
||||||
|
args.subtitle,
|
||||||
|
categories,
|
||||||
|
)
|
||||||
|
|
||||||
|
html = render_html(
|
||||||
|
catalog_data,
|
||||||
|
base_dir / "templates",
|
||||||
|
images_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
generate_pdf(html, output_path)
|
||||||
|
|
||||||
|
print(f"PDF generated: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
236
output/katalog.html
Normal file
236
output/katalog.html
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>BistroUšky</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4 landscape;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "DejaVu Sans", Arial, sans-serif;
|
||||||
|
color: #383633;
|
||||||
|
background: #f4efe5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
width: 297mm;
|
||||||
|
height: 210mm;
|
||||||
|
position: relative;
|
||||||
|
page-break-after: always;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 20mm 19mm 18mm 19mm;
|
||||||
|
background-color: #f4efe5;
|
||||||
|
|
||||||
|
background-image: linear-gradient(rgba(246,241,231,.82), rgba(246,241,231,.82)), url("static/background.jpg");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::before,
|
||||||
|
.page::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 19mm;
|
||||||
|
right: 19mm;
|
||||||
|
border-top: 1px solid rgba(56,54,51,.65);
|
||||||
|
}
|
||||||
|
.page::before { top: 14mm; }
|
||||||
|
.page::after { bottom: 14mm; }
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title {
|
||||||
|
font-size: 34pt;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-subtitle {
|
||||||
|
font-size: 16pt;
|
||||||
|
letter-spacing: .35em;
|
||||||
|
margin-top: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-logo {
|
||||||
|
margin-top: 26mm;
|
||||||
|
max-width: 48mm;
|
||||||
|
max-height: 48mm;
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.category-title {
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin: 6mm 0 14mm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
column-gap: 9mm;
|
||||||
|
row-gap: 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product { break-inside: avoid; }
|
||||||
|
|
||||||
|
.photo-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 48mm;
|
||||||
|
border-radius: 6mm;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255,255,255,.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo.placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #777;
|
||||||
|
border: 1px dashed #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
margin-top: 2.5mm;
|
||||||
|
border-top: 1px solid rgba(56,54,51,.7);
|
||||||
|
border-bottom: 1px solid rgba(56,54,51,.45);
|
||||||
|
min-height: 9mm;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .28em;
|
||||||
|
line-height: 1.35;
|
||||||
|
padding: 1.5mm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-icon {
|
||||||
|
width: 6.8mm;
|
||||||
|
height: 6.8mm;
|
||||||
|
object-fit: contain;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
opacity: .85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11pt;
|
||||||
|
letter-spacing: .18em;
|
||||||
|
margin-top: 2mm;
|
||||||
|
min-height: 6mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 7pt;
|
||||||
|
margin-top: 1mm;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section class="page cover">
|
||||||
|
<div>
|
||||||
|
<h1 class="cover-title">BistroUšky</h1>
|
||||||
|
<div class="cover-subtitle">CAKE AND PATISSERIE</div>
|
||||||
|
<img class="cover-logo" src="static/logo.png" alt="logo">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="page">
|
||||||
|
<h1 class="category-title">
|
||||||
|
Klasické zákusky
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<article class="product">
|
||||||
|
<div class="photo-wrap">
|
||||||
|
|
||||||
|
<img class="photo" src="images/likerova_spicka.jpg" alt="Likérová špička">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="name-row">
|
||||||
|
<div class="product-name">Likérová špička</div>
|
||||||
|
|
||||||
|
<img class="allergen-icon" src="static/allergen_icon.png" alt="ikona">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="price">103,-</div>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="product">
|
||||||
|
<div class="photo-wrap">
|
||||||
|
|
||||||
|
<img class="photo" src="images/laskonka_mini.jpg" alt="Laskonka - mini">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="name-row">
|
||||||
|
<div class="product-name">Laskonka - mini</div>
|
||||||
|
|
||||||
|
<img class="allergen-icon" src="static/allergen_icon.png" alt="ikona">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="price">45,-</div>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page">
|
||||||
|
<h1 class="category-title">
|
||||||
|
Dortíky
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<article class="product">
|
||||||
|
<div class="photo-wrap">
|
||||||
|
|
||||||
|
<img class="photo" src="images/red_velvet.jpg" alt="Red Velvet">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="name-row">
|
||||||
|
<div class="product-name">Red Velvet</div>
|
||||||
|
|
||||||
|
<img class="allergen-icon" src="static/allergen_icon.png" alt="ikona">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="price">145,-</div>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
output/katalog.pdf
Normal file
BIN
output/katalog.pdf
Normal file
Binary file not shown.
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
jinja2>=3.1.0
|
||||||
|
pyyaml>=6.0.0
|
||||||
|
weasyprint>=62.0
|
||||||
|
pillow>=10.0.0
|
||||||
|
flask>=3.0.0
|
||||||
|
werkzeug>=3.0.0
|
||||||
BIN
static/background.png
Executable file
BIN
static/background.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
BIN
static/logo.png
Normal file
BIN
static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 277 KiB |
187
templates/catalog.html.j2
Normal file
187
templates/catalog.html.j2
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 1024px 768px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "DejaVu Sans", Arial, sans-serif;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
width: 1024px;
|
||||||
|
height: 768px;
|
||||||
|
position: relative;
|
||||||
|
padding: 68px 68px 64px 68px;
|
||||||
|
page-break-after: always;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255,255,255,.58), rgba(255,255,255,.58)),
|
||||||
|
url("file://{{ assets_dir }}/background.png");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::before,
|
||||||
|
.page::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 66px;
|
||||||
|
right: 66px;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(45,45,45,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::before { top: 67px; }
|
||||||
|
.page::after { bottom: 68px; }
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-top: 140px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-subtitle {
|
||||||
|
font-size: 23px;
|
||||||
|
letter-spacing: 9px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-logo {
|
||||||
|
margin-top: 130px;
|
||||||
|
width: 190px;
|
||||||
|
opacity: .82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin: 28px 0 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
column-gap: 31px;
|
||||||
|
row-gap: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product {
|
||||||
|
width: 205px;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 205px;
|
||||||
|
height: 169px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name-row {
|
||||||
|
margin-top: 9px;
|
||||||
|
border-top: 1px solid rgba(50,50,50,.7);
|
||||||
|
border-bottom: 1px solid rgba(50,50,50,.7);
|
||||||
|
min-height: 31px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.25;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen {
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
opacity: .85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 205px;
|
||||||
|
height: 169px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(230,230,230,.75);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<section class="page cover">
|
||||||
|
<div class="cover-title">{{ catalog.title }}</div>
|
||||||
|
<div class="cover-subtitle">{{ catalog.subtitle|upper }}</div>
|
||||||
|
|
||||||
|
{% if logo_exists %}
|
||||||
|
<img class="cover-logo" src="file://{{ assets_dir }}/logo.png">
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% for category in catalog.categories %}
|
||||||
|
<section class="page">
|
||||||
|
<h1 class="category-title">{{ category.name }}</h1>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
{% for product in category.products %}
|
||||||
|
<div class="product">
|
||||||
|
{% if product.image %}
|
||||||
|
<img class="product-image" src="file://{{ images_dir }}/{{ product.image }}">
|
||||||
|
{% else %}
|
||||||
|
<div class="placeholder">bez fotky</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="product-name-row">
|
||||||
|
<div class="product-name">{{ product.name }}</div>
|
||||||
|
{% if product.show_allergen_icon %}
|
||||||
|
<img class="allergen" src="file://{{ assets_dir }}/allergen.png">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="price">{{ product.price }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
templates/upload.html
Normal file
64
templates/upload.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Generátor katalogu</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: #f7f3ee;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
background: white;
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 8px 30px rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
margin-top: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-top: 28px;
|
||||||
|
padding: 14px 22px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="box">
|
||||||
|
<h1>Generátor katalogu</h1>
|
||||||
|
<p class="hint">Nahraj CSV a fotky produktů. Názvy fotek musí odpovídat sloupci <code>image</code> v CSV.</p>
|
||||||
|
|
||||||
|
<form action="/generate" method="post" enctype="multipart/form-data">
|
||||||
|
<label>CSV soubor</label>
|
||||||
|
<input type="file" name="csv_file" accept=".csv" required>
|
||||||
|
|
||||||
|
<label>Fotky produktů</label>
|
||||||
|
<input type="file" name="images" accept="image/*" multiple>
|
||||||
|
|
||||||
|
<label>ZIP s fotkami</label>
|
||||||
|
<input type="file" name="images_zip" accept=".zip">
|
||||||
|
|
||||||
|
<button type="submit">Vygenerovat katalog PDF</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
uploads/.DS_Store
vendored
Normal file
BIN
uploads/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
web_jobs/.DS_Store
vendored
Normal file
BIN
web_jobs/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
web_jobs/dda19e80c065/.DS_Store
vendored
Normal file
BIN
web_jobs/dda19e80c065/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
web_output/.DS_Store
vendored
Normal file
BIN
web_output/.DS_Store
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user