v0.2 - 19 features: comparator, allergies, gamification, shopping list, achievements, stats, profile, share, bottom nav
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
85
docs/generate-pdfs.py
Normal file
85
docs/generate-pdfs.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess, base64, os, sys
|
||||
from pathlib import Path
|
||||
|
||||
os.chdir(Path(__file__).parent)
|
||||
|
||||
# Load logo
|
||||
logo_path = Path("../frontend/public/icons/icon-192.png")
|
||||
logo_b64 = base64.b64encode(logo_path.read_bytes()).decode()
|
||||
|
||||
# Load template
|
||||
template = Path("pdf-template.html").read_text()
|
||||
|
||||
from datetime import datetime
|
||||
date_str = datetime.now().strftime("%d/%m/%Y")
|
||||
|
||||
docs = {
|
||||
"MANUAL-PRODUTO": "Manual do Produto",
|
||||
"MANUAL-VENDAS": "Manual de Vendas",
|
||||
"MANUAL-TECNICO": "Manual Técnico",
|
||||
"ARQUITETURA-TECNICA": "Arquitetura Técnica",
|
||||
}
|
||||
|
||||
for doc, title in docs.items():
|
||||
md_file = f"{doc}.md"
|
||||
pdf_file = f"{doc}.pdf"
|
||||
|
||||
if not os.path.exists(md_file):
|
||||
print(f" ⚠️ {md_file} not found, skipping")
|
||||
continue
|
||||
|
||||
print(f"Generating {pdf_file}...")
|
||||
|
||||
# Convert MD to HTML
|
||||
result = subprocess.run(
|
||||
["pandoc", md_file, "--from", "markdown", "--to", "html"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
body_html = result.stdout
|
||||
|
||||
# Build cover
|
||||
cover = f'''<div class="cover">
|
||||
<img src="data:image/png;base64,{logo_b64}" class="cover-logo" />
|
||||
<h1>ALETHEIA</h1>
|
||||
<div class="subtitle">SCANNER NUTRICIONAL COM IA</div>
|
||||
<div class="doc-title">{title}</div>
|
||||
<div class="version">Versão 1.0 — {date_str}</div>
|
||||
<div class="tagline">"A verdade sobre o que você come"</div>
|
||||
</div>
|
||||
<div class="page-header">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<img src="data:image/png;base64,{logo_b64}" />
|
||||
<span class="brand">ALETHEIA</span>
|
||||
</div>
|
||||
<span class="doc-type">{title}</span>
|
||||
</div>'''
|
||||
|
||||
full_body = cover + "\n" + body_html
|
||||
full_html = template.replace("$body$", full_body)
|
||||
|
||||
tmp_html = f"/tmp/{doc}-full.html"
|
||||
Path(tmp_html).write_text(full_html, encoding="utf-8")
|
||||
|
||||
# Generate PDF
|
||||
result = subprocess.run([
|
||||
"wkhtmltopdf",
|
||||
"--page-size", "A4",
|
||||
"--margin-top", "20mm",
|
||||
"--margin-bottom", "20mm",
|
||||
"--margin-left", "20mm",
|
||||
"--margin-right", "20mm",
|
||||
"--enable-local-file-access",
|
||||
"--print-media-type",
|
||||
"--encoding", "utf-8",
|
||||
"--quiet",
|
||||
tmp_html, pdf_file
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if os.path.exists(pdf_file):
|
||||
size = os.path.getsize(pdf_file)
|
||||
print(f" ✅ {pdf_file} ({size/1024:.0f}KB)")
|
||||
else:
|
||||
print(f" ❌ Failed: {result.stderr[:200]}")
|
||||
|
||||
print("\nDone! 🎉")
|
||||
66
docs/generate-pdfs.sh
Executable file
66
docs/generate-pdfs.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
LOGO_B64=$(base64 -w0 ../frontend/public/icons/icon-192.png)
|
||||
TEMPLATE="pdf-template.html"
|
||||
DATE=$(date +"%d/%m/%Y")
|
||||
|
||||
declare -A TITLES=(
|
||||
["MANUAL-PRODUTO"]="Manual do Produto"
|
||||
["MANUAL-VENDAS"]="Manual de Vendas"
|
||||
["MANUAL-TECNICO"]="Manual Técnico"
|
||||
["ARQUITETURA-TECNICA"]="Arquitetura Técnica"
|
||||
)
|
||||
|
||||
for doc in MANUAL-PRODUTO MANUAL-VENDAS MANUAL-TECNICO ARQUITETURA-TECNICA; do
|
||||
TITLE="${TITLES[$doc]}"
|
||||
echo "Generating $doc.pdf..."
|
||||
|
||||
# Create cover + content HTML
|
||||
COVER="<div class=\"cover\">
|
||||
<img src=\"data:image/png;base64,$LOGO_B64\" class=\"cover-logo\" />
|
||||
<h1>ALETHEIA</h1>
|
||||
<div class=\"subtitle\">SCANNER NUTRICIONAL COM IA</div>
|
||||
<div class=\"doc-title\">$TITLE</div>
|
||||
<div class=\"version\">Versão 1.0 — $DATE</div>
|
||||
<div class=\"tagline\">\"A verdade sobre o que você come\"</div>
|
||||
</div>
|
||||
<div class=\"page-header\">
|
||||
<div style=\"display:flex;align-items:center;gap:10px\">
|
||||
<img src=\"data:image/png;base64,$LOGO_B64\" />
|
||||
<span class=\"brand\">ALETHEIA</span>
|
||||
</div>
|
||||
<span class=\"doc-type\">$TITLE</span>
|
||||
</div>"
|
||||
|
||||
# Convert MD to HTML body
|
||||
BODY=$(pandoc "$doc.md" --from markdown --to html 2>/dev/null)
|
||||
|
||||
# Build full HTML
|
||||
FULL_HTML=$(cat "$TEMPLATE" | sed "s|\\\$body\\\$|$COVER\n$BODY|")
|
||||
|
||||
# Write temp HTML
|
||||
echo "$FULL_HTML" > "/tmp/${doc}-full.html"
|
||||
|
||||
# Generate PDF with wkhtmltopdf
|
||||
wkhtmltopdf \
|
||||
--page-size A4 \
|
||||
--margin-top 20mm \
|
||||
--margin-bottom 20mm \
|
||||
--margin-left 20mm \
|
||||
--margin-right 20mm \
|
||||
--enable-local-file-access \
|
||||
--print-media-type \
|
||||
--encoding utf-8 \
|
||||
--quiet \
|
||||
"/tmp/${doc}-full.html" "$doc.pdf" 2>/dev/null
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
SIZE=$(du -h "$doc.pdf" | cut -f1)
|
||||
echo " ✅ $doc.pdf ($SIZE)"
|
||||
else
|
||||
echo " ❌ Failed $doc.pdf"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
247
docs/pdf-template.html
Normal file
247
docs/pdf-template.html
Normal file
@@ -0,0 +1,247 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
:root {
|
||||
--primary: #00D4AA;
|
||||
--accent: #7C3AED;
|
||||
--dark: #0A0E17;
|
||||
--dark2: #111827;
|
||||
--gray: #9CA3AF;
|
||||
--white: #F9FAFB;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
color: #1F2937;
|
||||
line-height: 1.7;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
/* COVER PAGE */
|
||||
.cover {
|
||||
page-break-after: always;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0A0E17 0%, #111827 40%, #0A0E17 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(0,212,170,0.15) 0%, transparent 70%);
|
||||
top: 10%;
|
||||
right: -10%;
|
||||
}
|
||||
|
||||
.cover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(124,58,237,0.1) 0%, transparent 70%);
|
||||
bottom: 10%;
|
||||
left: -5%;
|
||||
}
|
||||
|
||||
.cover-logo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 24px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 0 60px rgba(0,212,170,0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cover h1 {
|
||||
font-size: 48pt;
|
||||
font-weight: 800;
|
||||
letter-spacing: 8px;
|
||||
background: linear-gradient(135deg, #00D4AA 0%, #7C3AED 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cover .subtitle {
|
||||
font-size: 14pt;
|
||||
color: #9CA3AF;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 50px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cover .doc-title {
|
||||
font-size: 22pt;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
padding: 15px 40px;
|
||||
border: 2px solid rgba(0,212,170,0.4);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(0,212,170,0.05);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cover .version {
|
||||
margin-top: 40px;
|
||||
font-size: 10pt;
|
||||
color: #6B7280;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cover .tagline {
|
||||
font-size: 11pt;
|
||||
color: #00D4AA;
|
||||
font-style: italic;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 0;
|
||||
border-bottom: 2px solid #00D4AA;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-header img {
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.page-header .brand {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
color: #0A0E17;
|
||||
}
|
||||
|
||||
.page-header .doc-type {
|
||||
font-size: 8pt;
|
||||
color: #6B7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* CONTENT */
|
||||
h1 { font-size: 24pt; font-weight: 800; color: #0A0E17; margin: 30px 0 15px; }
|
||||
h2 { font-size: 16pt; font-weight: 700; color: #0A0E17; margin: 25px 0 12px; border-left: 4px solid #00D4AA; padding-left: 12px; }
|
||||
h3 { font-size: 13pt; font-weight: 600; color: #374151; margin: 20px 0 8px; }
|
||||
|
||||
p { margin-bottom: 10px; color: #374151; }
|
||||
|
||||
ul, ol { margin: 10px 0 10px 25px; color: #374151; }
|
||||
li { margin-bottom: 5px; }
|
||||
|
||||
strong { color: #0A0E17; }
|
||||
|
||||
code {
|
||||
background: #F3F4F6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 9pt;
|
||||
color: #7C3AED;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0A0E17;
|
||||
color: #00D4AA;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 9pt;
|
||||
margin: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #00D4AA;
|
||||
background: rgba(0,212,170,0.05);
|
||||
padding: 12px 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #0A0E17;
|
||||
color: #00D4AA;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
tr:nth-child(even) { background: #F9FAFB; }
|
||||
|
||||
/* FOOTER */
|
||||
.page-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 40px;
|
||||
font-size: 8pt;
|
||||
color: #9CA3AF;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
/* HIGHLIGHT BOXES */
|
||||
.info-box {
|
||||
background: linear-gradient(135deg, rgba(0,212,170,0.08), rgba(124,58,237,0.05));
|
||||
border: 1px solid rgba(0,212,170,0.2);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.cover { height: 100vh; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
$body$
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user