Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
Dishly
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
2
Issues
2
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
O Bar da Lora
Dishly
Commits
ccf0bf5c
Commit
ccf0bf5c
authored
Jun 11, 2026
by
Natã Frederico Sangalletti
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Versão Final
parent
2c2ea596
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
957 additions
and
392 deletions
+957
-392
app.py
app.py
+185
-38
style.css
static/style.css
+486
-200
base.html
templates/base.html
+51
-0
cadastro.html
templates/cadastro.html
+36
-34
editar.html
templates/editar.html
+49
-40
index.html
templates/index.html
+42
-68
perfil.html
templates/perfil.html
+54
-12
receita.html
templates/receita.html
+54
-0
No files found.
app.py
View file @
ccf0bf5c
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
)
static/style.css
View file @
ccf0bf5c
...
...
@@ -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
8
0px
;
padding
:
20px
16px
10
0px
;
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.9
5
);
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
:
30
px
;
align-items
:
flex-start
;
padding
:
40px
24
px
;
}
.
editar
-container
{
.
form
-container
{
width
:
100%
;
max-width
:
5
0
0px
;
max-width
:
5
6
0px
;
}
.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
:
1
8
px
;
gap
:
1
6
px
;
}
.
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
:
1
fr
1
fr
;
gap
:
12px
;
}
.form-actions
{
display
:
flex
;
gap
:
12px
;
margin-top
:
10
px
;
margin-top
:
8
px
;
}
.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
:
#ddd
ddd
;
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
,
1
fr
));
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
:
1
fr
;
}
.form-actions
{
flex-direction
:
column
;
}
.detail__content
{
padding
:
20px
;
}
.detail__header
h1
{
font-size
:
24px
;
}
}
templates/base.html
0 → 100644
View file @
ccf0bf5c
<!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>
©
2026 Dishly. Todos os direitos reservados.
</p>
</footer>
{% endblock %}
</body>
</html>
templates/cadastro.html
View file @
ccf0bf5c
<!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') }}"
>
←
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 R
eceita
</label>
<input
id=
"nome"
name=
"nome"
type=
"text"
placeholder=
"Ex: Lasanha
"
required
>
<label
for=
"nome"
>
Nome da r
eceita
</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 %}
templates/editar.html
View file @
ccf0bf5c
<!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) }}"
>
←
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 %}
templates/index.html
View file @
ccf0bf5c
<!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
</butto
n>
<
span
class=
"card__tempo"
>
{{ receita.tempo }}
</spa
n>
</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>
©
2026 Dishly. Todos os direitos reservados.
</p>
</footer>
</body>
</html>
</a>
{% endblock %}
templates/perfil.html
View file @
ccf0bf5c
<!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 %}
templates/receita.html
0 → 100644
View file @
ccf0bf5c
{% 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 %}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment