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