v0.2 - 19 features: comparator, allergies, gamification, shopping list, achievements, stats, profile, share, bottom nav

This commit is contained in:
2026-02-10 18:52:42 -03:00
parent e8f4788a33
commit ecdd7546d3
33 changed files with 2105 additions and 309 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

85
docs/generate-pdfs.py Normal file
View 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
View 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
View 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>