Versão Final

parent 2c2ea596
import os
from flask import Flask, redirect, render_template, request, url_for
from flask import Flask, flash, redirect, render_template, request, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func, text
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dishly-dev-key")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join(
app.instance_path, "database.db"
)
......@@ -11,7 +12,9 @@ app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
# Modelo da Tabela
CATEGORIAS = ["Todos", "Doces", "Salgados", "Bebidas"]
class Receita(db.Model):
id = db.Column(db.Integer, primary_key=True)
nome = db.Column(db.String(100), nullable=False)
......@@ -20,71 +23,215 @@ class Receita(db.Model):
descricao = db.Column(db.Text, nullable=False)
ingredientes = db.Column(db.Text, nullable=False)
preparo = db.Column(db.Text, nullable=False)
categoria = db.Column(db.String(50), nullable=False, default="Salgados")
def _ensure_categoria_column():
with db.engine.connect() as conn:
cols = conn.execute(text("PRAGMA table_info(receita)")).fetchall()
if cols and not any(col[1] == "categoria" for col in cols):
conn.execute(
text("ALTER TABLE receita ADD COLUMN categoria VARCHAR(50) DEFAULT 'Salgados'")
)
conn.commit()
def _parse_receita_form():
nome = (request.form.get("nome") or "").strip()
porcoes = (request.form.get("porcoes") or "").strip()
tempo = (request.form.get("tempo") or "").strip()
descricao = (request.form.get("descricao") or "").strip()
ingredientes = (request.form.get("ingredientes") or "").strip()
preparo = (request.form.get("preparo") or "").strip()
categoria = (request.form.get("categoria") or "Salgados").strip()
if categoria not in CATEGORIAS[1:]:
categoria = "Salgados"
erros = []
if not nome:
erros.append("Informe o nome da receita.")
if not porcoes:
erros.append("Informe o número de porções.")
if not tempo:
erros.append("Informe o tempo de preparo.")
if not descricao:
erros.append("Informe uma descrição.")
if not ingredientes:
erros.append("Informe os ingredientes.")
if not preparo:
erros.append("Informe o modo de preparo.")
return {
"nome": nome,
"porcoes": porcoes,
"tempo": tempo,
"descricao": descricao,
"ingredientes": ingredientes,
"preparo": preparo,
"categoria": categoria,
"erros": erros,
}
def _filtrar_receitas():
busca = (request.args.get("q") or "").strip()
categoria = (request.args.get("categoria") or "Todos").strip()
query = Receita.query
if categoria in CATEGORIAS[1:]:
query = query.filter(Receita.categoria == categoria)
if busca:
termo = f"%{busca.lower()}%"
query = query.filter(
db.or_(
func.lower(Receita.nome).like(termo),
func.lower(Receita.descricao).like(termo),
func.lower(Receita.ingredientes).like(termo),
)
)
return query.order_by(Receita.id.desc()).all(), busca, categoria
@app.route("/")
def index():
receitas = Receita.query.all()
return render_template("index.html", receitas=receitas)
receitas, busca, categoria = _filtrar_receitas()
return render_template(
"index.html",
receitas=receitas,
busca=busca,
categoria_ativa=categoria,
categorias=CATEGORIAS,
)
@app.route("/receita/<int:id>")
def receita(id):
receita_obj = Receita.query.get_or_404(id)
return render_template("receita.html", receita=receita_obj)
@app.route("/cadastro", methods=["GET", "POST"])
def cadastro():
if request.method == "POST":
nome = request.form.get("nome")
porcoes = request.form.get("porcoes")
tempo = request.form.get("tempo")
descricao = request.form.get("descricao")
ingredientes = request.form.get("ingredientes")
preparo = request.form.get("preparo")
dados = {
"nome": "",
"porcoes": "",
"tempo": "",
"descricao": "",
"ingredientes": "",
"preparo": "",
"categoria": "Salgados",
}
print(f"DADOS RECEBIDOS: {nome}, {porcoes}, {tempo}, {descricao}, {ingredientes}, {preparo}")
if request.method == "POST":
dados = _parse_receita_form()
if dados["erros"]:
for erro in dados["erros"]:
flash(erro, "erro")
return render_template(
"cadastro.html", dados=dados, categorias=CATEGORIAS[1:]
)
nova = Receita(
nome=nome,
porcoes=porcoes,
tempo=tempo,
descricao=descricao,
ingredientes=ingredientes,
preparo=preparo,
nome=dados["nome"],
porcoes=dados["porcoes"],
tempo=dados["tempo"],
descricao=dados["descricao"],
ingredientes=dados["ingredientes"],
preparo=dados["preparo"],
categoria=dados["categoria"],
)
db.session.add(nova)
db.session.commit()
return redirect(url_for("index"))
flash("Receita cadastrada com sucesso!", "sucesso")
return redirect(url_for("receita", id=nova.id))
return render_template("cadastro.html", dados=dados, categorias=CATEGORIAS[1:])
return render_template("cadastro.html")
@app.route("/editar/<int:id>", methods=["GET", "POST"])
def editar(id):
receita = Receita.query.get_or_404(id)
receita_obj = Receita.query.get_or_404(id)
if request.method == "POST":
receita.nome = request.form.get("nome")
receita.porcoes = request.form.get("porcoes")
receita.tempo = request.form.get("tempo")
receita.descricao = request.form.get("descricao")
receita.ingredientes = request.form.get("ingredientes")
receita.preparo = request.form.get("preparo")
dados = _parse_receita_form()
if dados["erros"]:
for erro in dados["erros"]:
flash(erro, "erro")
return render_template(
"editar.html",
receita=receita_obj,
dados=dados,
categorias=CATEGORIAS[1:],
)
receita_obj.nome = dados["nome"]
receita_obj.porcoes = dados["porcoes"]
receita_obj.tempo = dados["tempo"]
receita_obj.descricao = dados["descricao"]
receita_obj.ingredientes = dados["ingredientes"]
receita_obj.preparo = dados["preparo"]
receita_obj.categoria = dados["categoria"]
db.session.commit()
return redirect(url_for("index"))
flash("Receita atualizada com sucesso!", "sucesso")
return redirect(url_for("receita", id=receita_obj.id))
dados = {
"nome": receita_obj.nome,
"porcoes": receita_obj.porcoes,
"tempo": receita_obj.tempo,
"descricao": receita_obj.descricao,
"ingredientes": receita_obj.ingredientes,
"preparo": receita_obj.preparo,
"categoria": receita_obj.categoria or "Salgados",
}
return render_template(
"editar.html",
receita=receita_obj,
dados=dados,
categorias=CATEGORIAS[1:],
)
return render_template("editar.html", receita=receita)
@app.route("/excluir/<int:id>")
@app.route("/excluir/<int:id>", methods=["POST"])
def excluir(id):
receita = Receita.query.get_or_404(id)
db.session.delete(receita)
receita_obj = Receita.query.get_or_404(id)
nome = receita_obj.nome
db.session.delete(receita_obj)
db.session.commit()
flash(f'Receita "{nome}" excluída.', "sucesso")
return redirect(url_for("index"))
@app.route("/perfil")
def acessoPerfil():
return render_template("perfil.html")
@app.route("/perfil")
def perfil():
total = Receita.query.count()
por_categoria = (
db.session.query(Receita.categoria, func.count(Receita.id))
.group_by(Receita.categoria)
.all()
)
recentes = Receita.query.order_by(Receita.id.desc()).limit(5).all()
return render_template(
"perfil.html",
total=total,
por_categoria=por_categoria,
recentes=recentes,
)
if __name__ == "__main__":
def init_db():
os.makedirs(app.instance_path, exist_ok=True)
with app.app_context():
db.create_all()
_ensure_categoria_column()
init_db()
if __name__ == "__main__":
app.run(debug=True)
......@@ -6,25 +6,62 @@
padding: 0;
}
:root {
--brand: #e60023;
--brand-dark: #ad081b;
--brand-light: #ff1a3c;
--bg: #f9f9f9;
--surface: #fff;
--border: #e8e8e8;
--text: #111;
--muted: #767676;
--radius: 16px;
}
body {
font-family: "Segoe UI", system-ui, sans-serif;
background-color: #f9f9f9;
color: #111;
background-color: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* Acessibilidade de foco geral */
:focus-visible {
outline: 2px solid #e60023;
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* ── Flash messages ── */
.flash-container {
padding: 12px 24px 0;
}
.flash {
padding: 12px 16px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
.flash--sucesso {
background: #e6f7ed;
color: #1a7f37;
border: 1px solid #b7ebc9;
}
.flash--erro {
background: #fde8ea;
color: #c41e3a;
border: 1px solid #f5c2c7;
}
/* ── Header ── */
.header {
position: sticky;
top: 0;
z-index: 100;
background: #fff;
border-bottom: 1px solid #e8e8e8;
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex;
align-items: center;
......@@ -34,11 +71,30 @@ body {
.header__logo {
font-size: 22px;
font-weight: 700;
color: #e60023;
color: var(--brand);
text-decoration: none;
flex-shrink: 0;
}
.header__user {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: #efefef;
color: #555;
text-decoration: none;
flex-shrink: 0;
transition: background 0.2s, color 0.2s;
}
.header__user:hover {
background: var(--brand);
color: #fff;
}
.header__search {
flex: 1;
display: flex;
......@@ -52,7 +108,7 @@ body {
.header__search svg {
flex-shrink: 0;
color: #767676;
color: var(--muted);
}
.header__search input {
......@@ -62,27 +118,22 @@ body {
font-size: 15px;
width: 100%;
padding: 10px 0;
color: #111;
color: var(--text);
}
.header__search input::placeholder {
color: #767676;
color: var(--muted);
}
.header__search-btn {
display: none;
/* busca acontece inline */
}
/* ── Categorias (Nav) ── */
/* ── Nav categorias ── */
.nav {
background: #fff;
background: var(--surface);
padding: 8px 24px 12px;
display: flex;
gap: 8px;
overflow-x: auto;
scrollbar-width: none;
border-bottom: 1px solid #e8e8e8;
border-bottom: 1px solid var(--border);
}
.nav::-webkit-scrollbar {
......@@ -97,41 +148,34 @@ body {
padding: 8px 16px;
border-radius: 20px;
background: #efefef;
color: #111;
transition:
background 0.2s ease,
color 0.2s ease;
color: var(--text);
transition: background 0.2s, color 0.2s;
}
.nav a:hover {
background: #e60023;
background: var(--brand);
color: #fff;
}
.nav a.active {
background: #111;
background: var(--text);
color: #fff;
}
/* ── Feed (grid masonry) ── */
/* ── Feed ── */
.feed {
padding: 20px 16px 80px;
padding: 20px 16px 100px;
columns: 5 180px;
/* cria a grade tipo Pinterest */
column-gap: 12px;
}
/* ── Cartão ── */
.card {
break-inside: avoid;
margin-bottom: 12px;
border-radius: 16px;
border-radius: var(--radius);
overflow: hidden;
background: #fff;
cursor: pointer;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
background: var(--surface);
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}
......@@ -140,47 +184,55 @@ body {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* Placeholder de imagem com altura aleatória */
.card__link {
display: block;
text-decoration: none;
color: inherit;
}
.card__img {
width: 100%;
background: #e0e0e0;
background: linear-gradient(135deg, #ececec, #d8d8d8);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.card:nth-child(3n + 1) .card__img {
height: 200px;
}
.card:nth-child(3n + 2) .card__img {
height: 280px;
}
.card:nth-child(3n) .card__img {
height: 240px;
}
.card__img--0 { height: 200px; }
.card__img--1 { height: 280px; }
.card__img--2 { height: 240px; }
.card__img svg {
opacity: 0.35;
}
/* overlay de ações aparece no hover */
.card__categoria {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.92);
color: var(--text);
font-size: 11px;
font-weight: 700;
padding: 4px 10px;
border-radius: 12px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.card__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 16px;
background: rgba(0, 0, 0, 0.2);
/* Leve escurecimento opcional */
inset: 0;
border-radius: var(--radius);
background: rgba(0, 0, 0, 0.25);
opacity: 0;
transition: opacity 0.2s ease;
transition: opacity 0.2s;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 12px;
pointer-events: none;
}
.card:hover .card__overlay,
......@@ -188,51 +240,36 @@ body {
opacity: 1;
}
.card__overlay-top {
.card__overlay-bottom {
display: flex;
justify-content: flex-end;
pointer-events: auto;
}
.btn-salvar {
background: #e60023;
.card__tempo {
background: rgba(0, 0, 0, 0.6);
color: #fff;
border: none;
border-radius: 20px;
padding: 8px 16px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-salvar:hover {
background: #ad081b;
}
.card__overlay-bottom {
display: flex;
gap: 6px;
align-items: center;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 12px;
}
.btn-editar {
text-decoration: none;
background: rgba(255, 255, 255, 0.9);
color: #111;
background: rgba(255, 255, 255, 0.95);
color: var(--text);
border-radius: 20px;
padding: 6px 14px;
font-size: 13px;
font-weight: 600;
margin-left: auto;
/* Melhor que 10rem fixo, adapta a qualquer largura */
transition: background 0.2s ease;
transition: background 0.2s;
}
.btn-editar:hover {
background: #fff;
}
/* Informações do cartão */
.card__info {
padding: 10px 12px 14px;
}
......@@ -240,11 +277,35 @@ body {
.card__title {
font-size: 14px;
font-weight: 600;
color: #111;
line-height: 1.4;
}
/* ── FAB (botão +) ── */
.card__meta {
font-size: 12px;
color: var(--muted);
margin-top: 4px;
}
/* ── Empty state ── */
.empty-state {
column-span: all;
text-align: center;
padding: 60px 24px;
max-width: 420px;
margin: 0 auto;
}
.empty-state h2 {
font-size: 22px;
margin: 16px 0 8px;
}
.empty-state p {
color: var(--muted);
margin-bottom: 20px;
}
/* ── FAB ── */
.fab {
position: fixed;
bottom: 32px;
......@@ -252,152 +313,138 @@ body {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #ff1a3c, #e60023);
color: #ffffff;
border: none;
cursor: pointer;
background: linear-gradient(135deg, var(--brand-light), var(--brand));
color: #fff;
text-decoration: none;
z-index: 1000;
/* Centralização moderna */
display: inline-flex;
align-items: center;
justify-content: center;
/* Sombra suave e realista (estilo Material Design moderno) */
box-shadow: 0 8px 24px rgba(230, 0, 35, 0.25),
0 4px 12px rgba(0, 0, 0, 0.15);
/* Transição robusta usando 'will-change' para alta performance */
will-change: transform, box-shadow;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.3s ease,
filter 0.3s ease;
box-shadow: 0 8px 24px rgba(230, 0, 35, 0.25), 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s, filter 0.3s;
}
/* Efeito de passar o mouse (Hover) */
.fab:hover {
transform: translateY(-4px) scale(1.05);
filter: brightness(1.1);
box-shadow: 0 12px 32px rgba(230, 0, 35, 0.4),
0 6px 16px rgba(0, 0, 0, 0.2);
box-shadow: 0 12px 32px rgba(230, 0, 35, 0.4), 0 6px 16px rgba(0, 0, 0, 0.2);
}
/* Efeito de clique (Active) - Essencial para o mobile! */
.fab:active {
transform: translateY(-1px) scale(0.98);
box-shadow: 0 4px 12px rgba(230, 0, 35, 0.3);
transition: transform 0.1s ease;
}
/* Garante que o ícone dentro dele (seja SVG ou Font Icon) fique perfeito */
.fab i, .fab svg {
font-size: 24px;
width: 24px;
height: 24px;
transition: transform 0.3s ease;
}
/* Bônus: Gira o ícone de leve no hover se você quiser um charme extra */
.fab:hover i, .fab:hover svg {
transform: rotate(15deg);
}
/* ── Footer ── */
footer {
background: #fff;
border-top: 1px solid #e8e8e8;
background: var(--surface);
border-top: 1px solid var(--border);
text-align: center;
padding: 16px;
font-size: 13px;
color: #767676;
position: fixed;
bottom: 0;
width: 100%;
z-index: 10;
/* Garante que o footer fique acima do feed */
color: var(--muted);
}
/* ── Responsividade ── */
@media (max-width: 600px) {
.feed {
columns: 2 140px;
}
.header__logo {
font-size: 18px;
}
.fab {
bottom: 70px;
/* Ajuste fino para mobile caso o footer seja maior */
right: 16px;
}
/* ── Form pages ── */
.form-page {
background: #f5f5f5;
}
/* ===========================
Página Editar Receita
=========================== */
.editar-page {
.form-page__wrap {
min-height: 100vh;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
padding: 30px;
align-items: flex-start;
padding: 40px 24px;
}
.editar-container {
.form-container {
width: 100%;
max-width: 500px;
max-width: 560px;
}
.editar-form {
background: #fff;
padding: 35px;
.form-back {
display: inline-block;
color: var(--muted);
text-decoration: none;
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
transition: color 0.2s;
}
.form-back:hover {
color: var(--brand);
}
.form-card {
background: var(--surface);
padding: 36px;
border-radius: 24px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
gap: 18px;
gap: 16px;
}
.editar-form h1 {
.form-card h1 {
font-size: 28px;
text-align: center;
color: #111;
margin-bottom: 10px;
}
.editar-form label {
.form-subtitle {
text-align: center;
color: var(--muted);
font-size: 14px;
margin-top: -8px;
margin-bottom: 8px;
}
.form-card label {
font-size: 14px;
font-weight: 600;
color: #555;
}
.editar-form input {
.form-card input,
.form-card select,
.form-card textarea {
width: 100%;
padding: 14px 18px;
border: 2px solid #efefef;
border-radius: 14px;
background: #fafafa;
font-size: 15px;
transition: 0.2s;
font-family: inherit;
transition: border-color 0.2s, background 0.2s;
}
.editar-form input:focus {
border-color: #e60023;
.form-card textarea {
min-height: 100px;
resize: vertical;
}
.form-card input:focus,
.form-card select:focus,
.form-card textarea:focus {
border-color: var(--brand);
background: #fff;
outline: none;
}
.editar-botoes {
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 10px;
margin-top: 8px;
}
.editar-botoes button {
.form-actions button,
.form-delete button {
flex: 1;
padding: 12px;
border: none;
......@@ -405,37 +452,67 @@ footer {
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: 0.25s;
transition: background 0.25s, transform 0.25s;
}
/* Salvar */
.form-delete {
margin-top: 16px;
}
.btn-principal {
background: #e60023;
color: white;
background: var(--brand);
color: #fff;
}
.btn-principal:hover {
background: #b6001b;
background: var(--brand-dark);
transform: translateY(-2px);
}
/* Limpar */
.btn-principal--inline {
display: inline-block;
padding: 10px 20px;
border-radius: 30px;
text-decoration: none;
font-size: 14px;
font-weight: 700;
}
.btn-secundario {
background: #efefef;
color: #111;
color: var(--text);
}
.btn-secundario:hover {
background: #dddddd;
background: #ddd;
}
/* Excluir */
.btn-secundario--inline {
display: inline-block;
padding: 10px 20px;
border-radius: 30px;
text-decoration: none;
font-size: 14px;
font-weight: 700;
background: #efefef;
color: var(--text);
}
.btn-secundario--inline:hover {
background: #ddd;
}
.btn-excluir {
background: #111;
color: white;
width: 100%;
padding: 12px;
border: none;
border-radius: 30px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
background: var(--text);
color: #fff;
transition: background 0.25s, transform 0.25s;
}
.btn-excluir:hover {
......@@ -443,39 +520,248 @@ footer {
transform: translateY(-2px);
}
/* Responsivo */
/* ── Detail page ── */
.detail-page footer {
margin-top: 40px;
}
.detail {
max-width: 800px;
margin: 0 auto;
padding: 0 16px 40px;
}
@media (max-width: 600px) {
.detail__hero-img {
height: 260px;
background: linear-gradient(135deg, #ececec, #d0d0d0);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
}
.editar-form {
padding: 25px;
}
.detail__content {
background: var(--surface);
border-radius: var(--radius);
padding: 32px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
.editar-botoes {
.detail__badge {
display: inline-block;
background: #efefef;
font-size: 12px;
font-weight: 700;
padding: 4px 12px;
border-radius: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 12px;
}
.detail__header h1 {
font-size: 32px;
margin-bottom: 8px;
}
.detail__desc {
color: #444;
line-height: 1.6;
margin-bottom: 16px;
}
.detail__meta {
display: flex;
gap: 16px;
color: var(--muted);
font-size: 14px;
font-weight: 600;
margin-bottom: 20px;
}
.detail__actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.detail__section {
margin-top: 28px;
padding-top: 24px;
border-top: 1px solid var(--border);
}
.detail__section h2 {
font-size: 18px;
margin-bottom: 14px;
}
.detail__list {
list-style: none;
display: flex;
flex-direction: column;
}
gap: 8px;
}
.editar-botoes button {
width: 100%;
}
.detail__list li {
padding: 10px 14px;
background: #fafafa;
border-radius: 10px;
font-size: 15px;
}
.detail__steps {
padding-left: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.detail__steps li {
line-height: 1.6;
padding-left: 4px;
}
.editar-form textarea {
width: 100%;
min-height: 120px;
/* ── Profile page ── */
.profile {
max-width: 720px;
margin: 0 auto;
padding: 32px 24px 60px;
}
.profile__header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 32px;
}
.profile__avatar {
width: 72px;
height: 72px;
border-radius: 50%;
background: linear-gradient(135deg, var(--brand-light), var(--brand));
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.profile__header h1 {
font-size: 26px;
}
.profile__header p {
color: var(--muted);
font-size: 14px;
}
.profile__stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 32px;
}
.stat-card {
background: var(--surface);
border-radius: var(--radius);
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.stat-card strong {
display: block;
font-size: 28px;
color: var(--brand);
}
.stat-card span {
font-size: 13px;
color: var(--muted);
}
.profile__recent h2 {
font-size: 18px;
margin-bottom: 14px;
}
.profile__list {
list-style: none;
background: var(--surface);
border-radius: var(--radius);
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.profile__list li + li {
border-top: 1px solid var(--border);
}
.profile__list a {
display: flex;
flex-direction: column;
gap: 2px;
padding: 14px 18px;
border: 2px solid #efefef;
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: background 0.2s;
}
.profile__list a:hover {
background: #fafafa;
resize: vertical;
}
.profile__list-name {
font-weight: 600;
font-size: 15px;
font-family: inherit;
transition: 0.2s;
}
.editar-form textarea:focus {
border-color: #e60023;
background: #fff;
outline: none;
.profile__list-meta {
font-size: 13px;
color: var(--muted);
}
.profile__empty {
color: var(--muted);
margin-bottom: 16px;
}
/* ── Responsividade ── */
@media (max-width: 600px) {
.feed {
columns: 2 140px;
}
.header__logo {
font-size: 18px;
}
.fab {
bottom: 24px;
right: 16px;
}
.form-card {
padding: 24px;
}
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.detail__content {
padding: 20px;
}
.detail__header h1 {
font-size: 24px;
}
}
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
<title>{% block title %}Dishly{% endblock %}</title>
</head>
<body class="{% block body_class %}{% endblock %}">
{% block header %}
<header class="header">
<a class="header__user" href="{{ url_for('perfil') }}" title="Perfil">
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
</svg>
</a>
<a class="header__logo" href="{{ url_for('index') }}">Dishly</a>
{% block search %}
<form class="header__search" action="{{ url_for('index') }}" method="get" role="search">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input type="search" name="q" value="{{ busca|default('') }}" placeholder="Buscar receitas..." aria-label="Buscar receitas" />
{% if categoria_ativa|default('Todos') != 'Todos' %}
<input type="hidden" name="categoria" value="{{ categoria_ativa }}" />
{% endif %}
</form>
{% endblock %}
</header>
{% endblock %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-container" role="status">
{% for categoria, mensagem in messages %}
<p class="flash flash--{{ categoria }}">{{ mensagem }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
{% block footer %}
<footer>
<p>&copy; 2026 Dishly. Todos os direitos reservados.</p>
</footer>
{% endblock %}
</body>
</html>
<!doctype html>
<html lang="pt-BR">
{% extends "base.html" %}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
<title>Cadastro de Receita</title>
</head>
{% block title %}Nova Receita — Dishly{% endblock %}
{% block body_class %}form-page{% endblock %}
<body class="editar-page">
{% block header %}{% endblock %}
{% block footer %}{% endblock %}
<div class="editar-container">
<form class="editar-form" action="/cadastro" method="POST">
{% block content %}
<div class="form-page__wrap">
<div class="form-container">
<a class="form-back" href="{{ url_for('index') }}">&larr; Voltar ao feed</a>
<form class="form-card" action="{{ url_for('cadastro') }}" method="POST">
<h1>Nova Receita</h1>
<p class="form-subtitle">Compartilhe sua receita com a comunidade Dishly.</p>
<label for="nome">Nome da Receita</label>
<input id="nome" name="nome" type="text" placeholder="Ex: Lasanha" required>
<label for="nome">Nome da receita</label>
<input id="nome" name="nome" type="text" placeholder="Ex: Lasanha bolonhesa" value="{{ dados.nome }}" required>
<div class="form-row">
<div>
<label for="porcoes">Porções</label>
<input id="porcoes" name="porcoes" type="text" placeholder="Ex: 6 porções">
<input id="porcoes" name="porcoes" type="text" placeholder="Ex: 6 porções" value="{{ dados.porcoes }}" required>
</div>
<div>
<label for="tempo">Tempo de preparo</label>
<input id="tempo" name="tempo" type="text" placeholder="Ex: 45 minutos">
<input id="tempo" name="tempo" type="text" placeholder="Ex: 45 minutos" value="{{ dados.tempo }}" required>
</div>
</div>
<label for="categoria">Categoria</label>
<select id="categoria" name="categoria" required>
{% for cat in categorias %}
<option value="{{ cat }}" {% if dados.categoria == cat %}selected{% endif %}>{{ cat }}</option>
{% endfor %}
</select>
<label for="descricao">Descrição</label>
<input id="descricao" name="descricao" type="text" placeholder="Breve descrição da receita">
<textarea id="descricao" name="descricao" placeholder="Breve descrição da receita" required>{{ dados.descricao }}</textarea>
<label for="ingredientes">Ingredientes</label>
<input id="ingredientes" name="ingredientes" type="text" placeholder="Liste os ingredientes">
<textarea id="ingredientes" name="ingredientes" placeholder="Um ingrediente por linha" required>{{ dados.ingredientes }}</textarea>
<label for="preparo">Modo de preparo</label>
<input id="preparo" name="preparo" type="text" placeholder="Descreva o preparo">
<div class="editar-botoes">
<button class="btn-secundario" type="reset">
Limpar
</button>
<textarea id="preparo" name="preparo" placeholder="Descreva o passo a passo" required>{{ dados.preparo }}</textarea>
<button class="btn-principal" type="submit">
Salvar
</button>
<div class="form-actions">
<button class="btn-secundario" type="reset">Limpar</button>
<button class="btn-principal" type="submit">Salvar receita</button>
</div>
</form>
</div>
</body>
</html>
\ No newline at end of file
</div>
{% endblock %}
<!DOCTYPE html>
<html lang="pt-BR">
{% extends "base.html" %}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
<title>Editar Receita</title>
</head>
{% block title %}Editar Receita — Dishly{% endblock %}
{% block body_class %}form-page{% endblock %}
<body class="editar-page">
{% block header %}{% endblock %}
{% block footer %}{% endblock %}
<div class="editar-container">
<form class="editar-form" action="/editar/{{ receita.id }}" method="POST">
{% block content %}
<div class="form-page__wrap">
<div class="form-container">
<a class="form-back" href="{{ url_for('receita', id=receita.id) }}">&larr; Voltar à receita</a>
<form class="form-card" action="{{ url_for('editar', id=receita.id) }}" method="POST">
<h1>Editar Receita</h1>
<p class="form-subtitle">Atualize os detalhes de {{ receita.nome }}.</p>
<label for="nome">Nome da Receita</label>
<input
id="nome"
name="nome"
type="text"
value="{{ receita.nome }}"
required
>
<div class="editar-botoes">
<button class="btn-secundario" type="reset">
Limpar
</button>
<button class="btn-principal" type="submit">
Salvar
</button>
<button
class="btn-excluir"
type="button"
onclick="location.href='/excluir/{{ receita.id }}'">
Excluir
</button>
</div>
<label for="nome">Nome da receita</label>
<input id="nome" name="nome" type="text" value="{{ dados.nome }}" required>
</form>
<div class="form-row">
<div>
<label for="porcoes">Porções</label>
<input id="porcoes" name="porcoes" type="text" value="{{ dados.porcoes }}" required>
</div>
<div>
<label for="tempo">Tempo de preparo</label>
<input id="tempo" name="tempo" type="text" value="{{ dados.tempo }}" required>
</div>
</div>
<label for="categoria">Categoria</label>
<select id="categoria" name="categoria" required>
{% for cat in categorias %}
<option value="{{ cat }}" {% if dados.categoria == cat %}selected{% endif %}>{{ cat }}</option>
{% endfor %}
</select>
<label for="descricao">Descrição</label>
<textarea id="descricao" name="descricao" required>{{ dados.descricao }}</textarea>
</body>
<label for="ingredientes">Ingredientes</label>
<textarea id="ingredientes" name="ingredientes" required>{{ dados.ingredientes }}</textarea>
</html>
\ No newline at end of file
<label for="preparo">Modo de preparo</label>
<textarea id="preparo" name="preparo" required>{{ dados.preparo }}</textarea>
<div class="form-actions">
<button class="btn-principal" type="submit">Salvar alterações</button>
</div>
</form>
<form class="form-delete" action="{{ url_for('excluir', id=receita.id) }}" method="POST"
onsubmit="return confirm('Tem certeza que deseja excluir esta receita?');">
<button class="btn-excluir" type="submit">Excluir receita</button>
</form>
</div>
</div>
{% endblock %}
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/static/style.css" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<title>Dishly — Feed</title>
</head>
{% extends "base.html" %}
<body>
<!-- Cabeçalho com logo e busca -->
<header class="header">
<a class="header_user" href="/templates/perfil"
><img src="{{ url_for('static', filename='img/user-solid.png') }}" alt="Perfil" width="30px"></a>
<a class="header__logo" href="/">Dishly</a>
<div class="header__search">
<!-- ícone de lupa -->
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input type="text" placeholder="Buscar receitas..." />
</div>
</header>
{% block title %}Dishly — Feed{% endblock %}
<!-- Navegação de categorias -->
<nav class="nav">
<a href="" class="active">Todos</a>
<a href="">Doces</a>
<a href="">Salgados</a>
<a href="">Bebidas</a>
</nav>
{% block content %}
<nav class="nav" aria-label="Categorias">
{% for cat in categorias %}
<a href="{{ url_for('index', categoria=cat, q=busca) }}"
class="{% if categoria_ativa == cat %}active{% endif %}">{{ cat }}</a>
{% endfor %}
</nav>
<!-- Grade de receitas estilo Pinterest -->
<main class="feed">
<main class="feed">
{% if receitas %}
{% for receita in receitas %}
<article class="card">
<!-- Placeholder de imagem; substitua por <img src="{{ receita.imagem }}"> quando tiver imagens -->
<div class="card__img">
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke="#999"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<a class="card__link" href="{{ url_for('receita', id=receita.id) }}">
<div class="card__img card__img--{{ loop.index0 % 3 }}">
<span class="card__categoria">{{ receita.categoria or 'Salgados' }}</span>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
<!-- Overlay com ações que aparecem no hover -->
<div class="card__overlay">
<div class="card__overlay-top">
<button class="btn-salvar">Salvar</button>
<span class="card__tempo">{{ receita.tempo }}</span>
</div>
<div class="card__overlay-bottom">
<a class="btn-editar" href="/editar/{{ receita.id }}">Editar</a>
<a class="btn-editar" href="{{ url_for('editar', id=receita.id) }}" onclick="event.stopPropagation()">Editar</a>
</div>
</div>
<!-- Nome da receita -->
<div class="card__info">
<p class="card__title">{{ receita.nome }}</p>
<p class="card__meta">{{ receita.porcoes }}</p>
</div>
</a>
</article>
{% endfor %}
</main>
{% else %}
<div class="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#ccc" stroke-width="1.5" aria-hidden="true">
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 0 1-8 0"/>
</svg>
<h2>Nenhuma receita encontrada</h2>
<p>
{% if busca or categoria_ativa != 'Todos' %}
Tente outra busca ou categoria.
{% else %}
Comece cadastrando sua primeira receita!
{% endif %}
</p>
<a class="btn-principal btn-principal--inline" href="{{ url_for('cadastro') }}">Nova receita</a>
</div>
{% endif %}
</main>
<!-- Botão flutuante para cadastrar nova receita -->
<a class="fab" href="/cadastro" title="Nova receita">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<a class="fab" href="{{ url_for('cadastro') }}" title="Nova receita" aria-label="Nova receita">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</a>
<footer>
<p>&copy; 2026 Dishly. Todos os direitos reservados.</p>
</footer>
</body>
</html>
</a>
{% endblock %}
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Perfil</title>
</head>
<body>
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Perfil — Dishly{% endblock %}
{% block body_class %}profile-page{% endblock %}
{% block search %}{% endblock %}
{% block content %}
<main class="profile">
<div class="profile__header">
<div class="profile__avatar">
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
</svg>
</div>
<div>
<h1>Meu perfil</h1>
<p>Suas receitas no Dishly</p>
</div>
</div>
<div class="profile__stats">
<article class="stat-card">
<strong>{{ total }}</strong>
<span>Receitas cadastradas</span>
</article>
{% for cat, qtd in por_categoria %}
<article class="stat-card">
<strong>{{ qtd }}</strong>
<span>{{ cat }}</span>
</article>
{% endfor %}
</div>
<section class="profile__recent">
<h2>Receitas recentes</h2>
{% if recentes %}
<ul class="profile__list">
{% for receita in recentes %}
<li>
<a href="{{ url_for('receita', id=receita.id) }}">
<span class="profile__list-name">{{ receita.nome }}</span>
<span class="profile__list-meta">{{ receita.categoria }} · {{ receita.tempo }}</span>
</a>
</li>
{% endfor %}
</ul>
{% else %}
<p class="profile__empty">Você ainda não cadastrou nenhuma receita.</p>
<a class="btn-principal btn-principal--inline" href="{{ url_for('cadastro') }}">Cadastrar receita</a>
{% endif %}
</section>
</main>
{% endblock %}
{% extends "base.html" %}
{% block title %}{{ receita.nome }} — Dishly{% endblock %}
{% block body_class %}detail-page{% endblock %}
{% block search %}{% endblock %}
{% block content %}
<main class="detail">
<div class="detail__hero">
<div class="detail__hero-img">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="#bbb" stroke-width="1.2" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
</div>
<div class="detail__content">
<div class="detail__header">
<span class="detail__badge">{{ receita.categoria or 'Salgados' }}</span>
<h1>{{ receita.nome }}</h1>
<p class="detail__desc">{{ receita.descricao }}</p>
<div class="detail__meta">
<span>{{ receita.porcoes }}</span>
<span>{{ receita.tempo }}</span>
</div>
<div class="detail__actions">
<a class="btn-principal btn-principal--inline" href="{{ url_for('editar', id=receita.id) }}">Editar</a>
<a class="btn-secundario btn-secundario--inline" href="{{ url_for('index') }}">Voltar ao feed</a>
</div>
</div>
<section class="detail__section">
<h2>Ingredientes</h2>
<ul class="detail__list">
{% for linha in receita.ingredientes.splitlines() if linha.strip() %}
<li>{{ linha.strip() }}</li>
{% endfor %}
</ul>
</section>
<section class="detail__section">
<h2>Modo de preparo</h2>
<ol class="detail__steps">
{% for passo in receita.preparo.splitlines() if passo.strip() %}
<li>{{ passo.strip() }}</li>
{% endfor %}
</ol>
</section>
</div>
</main>
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment