Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
Alba María Álvarez
/
front_recipes
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
8b7dbb5c
authored
Sep 10, 2025
by
Alba María Álvarez
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Ultimos cambios
parent
451d56f2
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
334 additions
and
47 deletions
src/services/recipe.js
src/services/user.js
src/stores/recipeStore.js
src/views/AISearch.vue
src/views/CreateRecipe.vue
src/views/RecipeDetail.vue
src/services/recipe.js
View file @
8b7dbb5c
import
RecipeDetail
from
'@/views/RecipeDetail.vue'
;
import
api
from
'./api'
;
export
const
recipeService
=
{
...
...
@@ -8,19 +7,43 @@ export const recipeService = {
if
(
size
!=
null
)
params
.
append
(
'size'
,
size
);
if
(
sortDirection
!=
null
)
params
.
append
(
'sortDirection'
,
sortDirection
);
const
response
=
await
api
.
get
(
`/recipe?
${
params
.
toString
()}
`
);
const
response
=
await
api
.
get
(
`/recipe?
${
params
.
toString
()}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
madeFavorite
:
async
(
id
)
=>
{
const
response
=
await
api
.
patch
(
`/recipe/
${
id
}
/favorite`
);
const
response
=
await
api
.
patch
(
`/recipe/
${
id
}
/favorite`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
removeFavorite
:
async
(
id
)
=>
{
const
response
=
await
api
.
delete
(
`/recipe/
${
id
}
/favorite`
);
const
response
=
await
api
.
delete
(
`/recipe/
${
id
}
/favorite`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
readDetail
:
async
(
id
)
=>
{
const
response
=
await
api
.
get
(
`/recipe/
${
id
}
`
);
const
response
=
await
api
.
get
(
`/recipe/
${
id
}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
create
:
async
(
recipeData
)
=>
{
...
...
@@ -28,7 +51,13 @@ export const recipeService = {
return
response
.
data
;
},
delete
:
async
(
id
)
=>
{
const
response
=
await
api
.
delete
(
`/recipe/
${
id
}
`
);
const
response
=
await
api
.
delete
(
`/recipe/
${
id
}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
update
:
async
(
recipeId
,
newData
)
=>
{
...
...
@@ -41,7 +70,13 @@ export const recipeService = {
if
(
page
)
params
.
append
(
'page'
,
page
);
if
(
size
)
params
.
append
(
'size'
,
size
);
const
response
=
await
api
.
get
(
`/recipe/search?
${
params
.
toString
()}
`
)
const
response
=
await
api
.
get
(
`/recipe/search?
${
params
.
toString
()}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
readFavorites
:
async
(
page
,
size
,
sortDirection
)
=>
{
...
...
@@ -50,7 +85,49 @@ export const recipeService = {
if
(
size
!=
null
)
params
.
append
(
'size'
,
size
);
if
(
sortDirection
!=
null
)
params
.
append
(
'sortDirection'
,
sortDirection
);
const
response
=
await
api
.
get
(
`/recipe/favorites?
${
params
.
toString
()}
`
);
const
response
=
await
api
.
get
(
`/recipe/favorites?
${
params
.
toString
()}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
searchAI
:
async
(
query
)
=>
{
const
response
=
await
api
.
post
(
`/recipe/search?ingredients=
${
query
}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
searchMoreAI
:
async
(
sessionId
)
=>
{
const
response
=
await
api
.
post
(
'/recipe/search/more'
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
,
"X-Session-ID"
:
sessionId
}
}
)
return
response
.
data
;
},
getDetailAI
:
async
(
sessionId
,
index
)
=>
{
const
response
=
await
api
.
post
(
`/recipe/detail/
${
index
}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
,
"X-Session-ID"
:
sessionId
}
}
)
return
response
.
data
;
}
}
\ No newline at end of file
src/services/user.js
View file @
8b7dbb5c
...
...
@@ -2,7 +2,13 @@ import api from './api';
export
const
userService
=
{
readUser
:
async
()
=>
{
const
response
=
await
api
.
get
(
'/user/me'
);
const
response
=
await
api
.
get
(
'/user/me'
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
readAll
:
async
(
page
,
size
,
sortBy
,
sortDirection
)
=>
{
...
...
@@ -16,31 +22,73 @@ export const userService = {
}
// Petición al endpoint con los parámetros
const
response
=
await
api
.
get
(
`/user?
${
params
.
toString
()}
`
);
const
response
=
await
api
.
get
(
`/user?
${
params
.
toString
()}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
deactivate
:
async
(
id
)
=>
{
const
response
=
await
api
.
patch
(
`/user/
${
id
}
/deactivate`
);
const
response
=
await
api
.
patch
(
`/user/
${
id
}
/deactivate`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
activate
:
async
(
id
)
=>
{
const
response
=
await
api
.
patch
(
`/user/
${
id
}
/activate`
);
const
response
=
await
api
.
patch
(
`/user/
${
id
}
/activate`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
deactivateMe
:
async
()
=>
{
const
response
=
await
api
.
patch
(
`/user/deactivate`
);
const
response
=
await
api
.
patch
(
`/user/deactivate`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
update
:
async
(
updatedUser
)
=>
{
const
response
=
await
api
.
put
(
'/user'
,
updatedUser
);
const
response
=
await
api
.
put
(
'/user'
,
updatedUser
,
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
changePassword
:
async
(
passwords
)
=>
{
const
response
=
await
api
.
patch
(
`user/password`
,
passwords
);
const
response
=
await
api
.
patch
(
`user/password`
,
passwords
,
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
changeRole
:
async
(
id
,
role
)
=>
{
const
response
=
await
api
.
patch
(
`/user/
${
id
}
/role`
,
role
);
const
response
=
await
api
.
patch
(
`/user/
${
id
}
/role`
,
role
,
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
},
search
:
async
(
query
,
page
,
size
)
=>
{
...
...
@@ -49,7 +97,13 @@ export const userService = {
if
(
page
)
params
.
append
(
'page'
,
page
);
if
(
size
)
params
.
append
(
'size'
,
size
);
const
response
=
await
api
.
get
(
`/user/search?
${
params
.
toString
()}
`
)
const
response
=
await
api
.
get
(
`/user/search?
${
params
.
toString
()}
`
,
{},
{
headers
:
{
"Content-Type"
:
"application/json"
}
}
);
return
response
.
data
;
}
}
\ No newline at end of file
src/stores/recipeStore.js
View file @
8b7dbb5c
...
...
@@ -9,7 +9,13 @@ export const useRecipeStore = defineStore('recipe', {
pageSize
:
10
,
totalElements
:
0
,
totalPages
:
0
,
sortDirection
:
'desc'
sortDirection
:
'desc'
,
// Estados búsqueda por IA
aiRecipes
:
[],
aiSessionId
:
null
,
loading
:
false
,
aiRecipe
:
null
,
loadingDetail
:
false
}),
actions
:
{
async
readAll
(
page
,
size
,
sortDirection
)
{
...
...
@@ -111,6 +117,63 @@ export const useRecipeStore = defineStore('recipe', {
this
.
totalPages
=
0
;
throw
error
;
}
},
async
searchAI
(
query
)
{
this
.
loading
=
true
;
// Limpiamos los resultados de la búsqueda anterior
this
.
aiRecipes
=
[];
this
.
aiSessionId
=
null
;
try
{
const
{
recipes
,
sessionId
}
=
await
recipeService
.
searchAI
(
query
);
if
(
recipes
)
{
this
.
aiRecipes
=
recipes
;
this
.
aiSessionId
=
sessionId
;
}
else
{
this
.
aiRecipes
=
[];
this
.
aiSessionId
=
null
;
}
}
catch
(
error
)
{
console
.
error
(
'Error al hacer la búsqueda por IA:'
,
error
);
this
.
aiRecipes
=
[];
this
.
aiSessionId
=
null
;
throw
error
;
}
finally
{
this
.
loading
=
false
;
}
},
async
searchMoreAI
(
sessionId
)
{
this
.
loading
=
true
;
try
{
const
response
=
await
recipeService
.
searchMoreAI
(
sessionId
);
if
(
!
response
||
response
.
length
===
0
)
{
return
;
}
this
.
aiRecipes
.
push
(...
response
);
}
catch
(
error
)
{
console
.
error
(
'Error al cargar más recetas'
,
error
);
throw
error
;
}
finally
{
this
.
loading
=
false
;
}
},
async
getDetailAI
(
sessionId
,
index
)
{
this
.
loadingDetail
=
true
;
this
.
aiRecipe
=
null
;
try
{
const
response
=
await
recipeService
.
getDetailAI
(
sessionId
,
index
);
this
.
aiRecipe
=
response
;
}
catch
(
error
)
{
console
.
error
(
'Error al obtener el detalle de la receta:'
,
error
);
this
.
aiRecipe
=
null
;
throw
error
;
}
finally
{
this
.
loadingDetail
=
false
;
}
}
}
});
\ No newline at end of file
src/views/AISearch.vue
View file @
8b7dbb5c
This diff is collapsed.
Click to expand it.
src/views/CreateRecipe.vue
View file @
8b7dbb5c
...
...
@@ -42,7 +42,7 @@
</div>
<div
v-for=
"ingredient in ingredients"
:key=
"ingredient.id"
class=
"row align-items-center mb-2"
>
<div
class=
"col-3"
>
<input
type=
"
number
"
class=
"form-control"
v-model=
"ingredient.quantity"
placeholder=
"Ej: 2"
>
<input
type=
"
text
"
class=
"form-control"
v-model=
"ingredient.quantity"
placeholder=
"Ej: 2"
>
</div>
<div
class=
"col-3"
>
<input
type=
"text"
class=
"form-control"
v-model=
"ingredient.unitOfMeasure"
placeholder=
"Ej: tazas"
>
...
...
src/views/RecipeDetail.vue
View file @
8b7dbb5c
...
...
@@ -53,7 +53,7 @@
<div
v-else
class=
"mb-4 mt-3 text-center"
>
<label
for=
"recipePicture"
class=
"form-label fw-bold"
>
Cambiar imagen
</label>
<input
id=
"recipePicture"
type=
"file"
class=
"form-control mx-auto"
style=
"max-width: 300px"
@
change=
"handlePictureChange"
/>
<input
id=
"recipePicture"
type=
"file"
class=
"form-control mx-auto"
@
change=
"handlePictureChange"
/>
</div>
<!-- Descripción -->
...
...
@@ -68,25 +68,37 @@
<hr
class=
"my-4"
/>
<!-- Ingredientes -->
<div
class=
"row"
>
<!-- Ingredientes -->
<div
class=
"col-lg-6"
>
<div
class=
"mb-4"
>
<h5
class=
"fw-bold"
>
Ingredientes
</h5>
<ul
v-if=
"mode === 'view'"
class=
"list-unstyled"
>
<li
v-for=
"(ing, index) in processedIngredients"
:key=
"index"
class=
"mb-1"
>
<i
class=
"bi bi-check-circle-fill me-2 text-success"
></i>
{{
ing
.
quantity
}}
{{
ing
.
unit
}}
{{
ing
.
connective
}}
{{
ing
.
name
}}
<h5
class=
"section-title"
>
<i
class=
"bi bi-basket me-2"
></i>
Ingredientes
</h5>
<!-- Vista -->
<ul
v-if=
"mode === 'view'"
class=
"ingredients-list"
>
<li
v-for=
"(ing, index) in processedIngredients"
:key=
"index"
class=
"ingredient-item"
>
<span
class=
"ingredient-name"
>
{{
ing
.
name
}}
</span>
<span
class=
"ingredient-amount"
>
{{
ing
.
quantity
}}
{{
ing
.
unit
}}
{{
ing
.
connective
}}
</span>
</li>
</ul>
<!-- Edición -->
<div
v-else
class=
"ingredient-form-section"
>
<div
v-for=
"(ing, index) in editableRecipe.ingredients"
:key=
"index"
class=
"input-group mb-2"
>
<input
type=
"number"
v-model=
"ing.quantity"
class=
"form-control"
placeholder=
"Cantidad"
>
<input
type=
"text"
v-model=
"ing.unitOfMeasure"
class=
"form-control"
placeholder=
"Unidad"
>
<input
type=
"text"
v-model=
"ing.name"
class=
"form-control"
placeholder=
"Ingrediente"
>
<button
@
click=
"editableRecipe.ingredients.splice(index, 1)"
class=
"btn btn-outline-danger"
type=
"button"
><i
class=
"bi bi-trash"
></i></button>
<div
v-for=
"(ing, index) in editableRecipe.ingredients"
:key=
"index"
class=
"ingredient-form mb-2"
>
<input
type=
"text"
v-model=
"ing.name"
class=
"form-control mb-2"
placeholder=
"Ingrediente"
>
<div
class=
"d-flex gap-2 mt-2"
>
<input
type=
"text"
v-model=
"ing.quantity"
class=
"form-control"
placeholder=
"Cantidad"
>
<input
type=
"text"
v-model=
"ing.unitOfMeasure"
class=
"form-control"
placeholder=
"Unidad"
>
<button
@
click=
"editableRecipe.ingredients.splice(index, 1)"
class=
"btn btn-outline-danger"
type=
"button"
>
<i
class=
"bi bi-trash"
></i>
</button>
</div>
</div>
<button
@
click=
"editableRecipe.ingredients.push(
{ quantity: '', unit: '',
ingredient
: '' })" class="btn btn-outline-primary-custom mt-2 w-100">
<button
@
click=
"editableRecipe.ingredients.push(
{ quantity: '', unit: '',
name
: '' })" class="btn btn-outline-primary-custom mt-2 w-100">
<i
class=
"bi bi-plus-circle"
></i>
Añadir Ingrediente
</button>
</div>
...
...
@@ -96,21 +108,28 @@
<!-- Pasos -->
<div
class=
"col-lg-6"
>
<div
class=
"mb-4"
>
<h5
class=
"fw-bold"
>
Pasos de Preparación
</h5>
<ol
v-if=
"mode === 'view'"
>
<li
v-for=
"(step, index) in recipe.steps"
:key=
"index"
class=
"mb-2"
>
{{
step
.
description
}}
<h5
class=
"section-title"
>
<i
class=
"bi bi-list-ol me-2"
></i>
Pasos de preparación
</h5>
<!-- Vista -->
<ol
v-if=
"mode === 'view'"
class=
"steps-list"
>
<li
v-for=
"(step, index) in recipe.steps"
:key=
"index"
class=
"step-item"
>
<span
class=
"step-number"
>
{{
index
+
1
}}
</span>
<span
class=
"step-text"
>
{{
step
.
description
}}
</span>
</li>
</ol>
<!-- Edición -->
<div
v-else
class=
"steps-form-section"
>
<div
v-for=
"(step, index) in editableRecipe.steps"
:key=
"index"
class=
"
input-group mb-2
"
>
<
span
class=
"input-group-text"
>
{{
index
+
1
}}
</span
>
<textarea
v-model=
"step.description"
class=
"form-control"
rows=
"3"
>
</textarea
>
<
button
@
click=
"editableRecipe.steps.splice(index, 1)"
class=
"btn btn-outline-danger"
type=
"button"
><i
class=
"bi bi-trash"
></i></button
>
<div
v-for=
"(step, index) in editableRecipe.steps"
:key=
"index"
class=
"
step-form
"
>
<
div
class=
"d-flex gap-2 mb-2"
>
<span
class=
"input-group-text"
>
{{
index
+
1
}}
</span>
<textarea
v-model=
"step.description"
class=
"form-control"
rows=
"2"
></textarea>
<button
@
click=
"editableRecipe.steps.splice(index, 1)"
class=
"btn btn-outline-danger"
type=
"button"
>
<i
class=
"bi bi-trash"
></i
>
</button
>
<
/div
>
</div>
<button
@
click=
"editableRecipe.steps.push(
{ description: '' })" class="btn btn-outline-primary-custom mt-2 w-100">
<i
class=
"bi bi-plus-circle"
></i>
Añadir Paso
...
...
@@ -120,6 +139,7 @@
</div>
</div>
</div>
</div>
</div>
</div>
...
...
@@ -175,12 +195,12 @@ const processedIngredients = computed(() => {
return
recipe
.
value
.
ingredients
.
map
(
ing
=>
{
const
unit
=
ing
.
unitOfMeasure
?
ing
.
unitOfMeasure
.
toLowerCase
()
:
''
;
const
name
=
ing
.
name
?
ing
.
name
.
toLowerCase
()
:
''
;
const
connective
=
ing
.
unitOfMeasure
?
'de'
:
''
;
//
const connective = ing.unitOfMeasure ? 'de' : '';
return
{
...
ing
,
unit
,
name
,
connective
name
//
connective
};
});
});
...
...
@@ -333,4 +353,73 @@ h5 {
color
:
white
;
font-size
:
14px
;
}
/* Títulos de sección */
.section-title
{
font-weight
:
700
;
color
:
#793E6C
;
border-bottom
:
2px
solid
#eee
;
padding-bottom
:
4px
;
margin-bottom
:
16px
;
display
:
flex
;
align-items
:
center
;
}
/* INGREDIENTES vista */
.ingredients-list
{
list-style
:
none
;
padding
:
0
;
margin
:
0
;
}
.ingredient-item
{
display
:
flex
;
justify-content
:
space-between
;
padding
:
6px
10px
;
border-bottom
:
1px
solid
#eee
;
}
.ingredient-name
{
font-weight
:
500
;
}
.ingredient-amount
{
color
:
#555
;
font-size
:
0.9rem
;
}
/* PASOS vista */
.steps-list
{
list-style
:
none
;
padding
:
0
;
margin
:
0
;
}
.step-item
{
display
:
flex
;
align-items
:
flex-start
;
margin-bottom
:
12px
;
background
:
#f5f5f5
;
padding
:
10px
14px
;
border-radius
:
8px
;
}
.step-number
{
flex-shrink
:
0
;
width
:
28px
;
height
:
28px
;
border-radius
:
50%
;
background
:
#793e6c
;
color
:
white
;
font-weight
:
bold
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
margin-right
:
12px
;
}
.step-text
{
flex
:
1
;
}
</
style
>
\ No newline at end of file
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