Ultimos cambios

parent 451d56f2
import RecipeDetail from '@/views/RecipeDetail.vue';
import api from './api'; import api from './api';
export const recipeService = { export const recipeService = {
...@@ -8,19 +7,43 @@ export const recipeService = { ...@@ -8,19 +7,43 @@ export const recipeService = {
if (size != null) params.append('size', size); if (size != null) params.append('size', size);
if (sortDirection != null) params.append('sortDirection', sortDirection); 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; return response.data;
}, },
madeFavorite: async (id) => { 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; return response.data;
}, },
removeFavorite: async (id) => { 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; return response.data;
}, },
readDetail: async (id) => { 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; return response.data;
}, },
create: async (recipeData) => { create: async (recipeData) => {
...@@ -28,7 +51,13 @@ export const recipeService = { ...@@ -28,7 +51,13 @@ export const recipeService = {
return response.data; return response.data;
}, },
delete: async (id) => { 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; return response.data;
}, },
update: async (recipeId, newData) => { update: async (recipeId, newData) => {
...@@ -41,7 +70,13 @@ export const recipeService = { ...@@ -41,7 +70,13 @@ export const recipeService = {
if (page) params.append('page', page); if (page) params.append('page', page);
if (size) params.append('size', size); 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; return response.data;
}, },
readFavorites: async (page, size, sortDirection) => { readFavorites: async (page, size, sortDirection) => {
...@@ -50,7 +85,49 @@ export const recipeService = { ...@@ -50,7 +85,49 @@ export const recipeService = {
if (size != null) params.append('size', size); if (size != null) params.append('size', size);
if (sortDirection != null) params.append('sortDirection', sortDirection); 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; return response.data;
} }
} }
\ No newline at end of file
...@@ -2,7 +2,13 @@ import api from './api'; ...@@ -2,7 +2,13 @@ import api from './api';
export const userService = { export const userService = {
readUser: async () => { readUser: async () => {
const response = await api.get('/user/me'); const response = await api.get('/user/me', {},
{
headers: {
"Content-Type": "application/json"
}
}
);
return response.data; return response.data;
}, },
readAll: async (page, size, sortBy, sortDirection) => { readAll: async (page, size, sortBy, sortDirection) => {
...@@ -16,31 +22,73 @@ export const userService = { ...@@ -16,31 +22,73 @@ export const userService = {
} }
// Petición al endpoint con los parámetros // 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; return response.data;
}, },
deactivate: async (id) => { 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; return response.data;
}, },
activate: async (id) => { 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; return response.data;
}, },
deactivateMe: async () => { deactivateMe: async () => {
const response = await api.patch(`/user/deactivate`); const response = await api.patch(`/user/deactivate`, {},
{
headers: {
"Content-Type": "application/json"
}
}
);
return response.data; return response.data;
}, },
update: async (updatedUser) => { 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; return response.data;
}, },
changePassword: async (passwords) => { 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; return response.data;
}, },
changeRole: async (id, role) => { 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; return response.data;
}, },
search: async (query, page, size) => { search: async (query, page, size) => {
...@@ -49,7 +97,13 @@ export const userService = { ...@@ -49,7 +97,13 @@ export const userService = {
if (page) params.append('page', page); if (page) params.append('page', page);
if (size) params.append('size', size); 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; return response.data;
} }
} }
\ No newline at end of file
...@@ -9,7 +9,13 @@ export const useRecipeStore = defineStore('recipe', { ...@@ -9,7 +9,13 @@ export const useRecipeStore = defineStore('recipe', {
pageSize: 10, pageSize: 10,
totalElements: 0, totalElements: 0,
totalPages: 0, totalPages: 0,
sortDirection: 'desc' sortDirection: 'desc',
// Estados búsqueda por IA
aiRecipes: [],
aiSessionId: null,
loading: false,
aiRecipe: null,
loadingDetail: false
}), }),
actions: { actions: {
async readAll(page, size, sortDirection) { async readAll(page, size, sortDirection) {
...@@ -111,6 +117,63 @@ export const useRecipeStore = defineStore('recipe', { ...@@ -111,6 +117,63 @@ export const useRecipeStore = defineStore('recipe', {
this.totalPages = 0; this.totalPages = 0;
throw error; 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
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<section class="row justify-content-center mb-5"> <section class="row justify-content-center mb-5">
<div class="col-md-8"> <div class="col-md-8">
<form @submit.prevent="searchRecipes" class="d-flex search-box-custom"> <form @submit.prevent="handleAISearch" class="d-flex search-box-custom">
<input <input
type="text" type="text"
class="form-control me-2" class="form-control me-2"
...@@ -24,37 +24,224 @@ ...@@ -24,37 +24,224 @@
</div> </div>
</section> </section>
<section v-if="results.length" class="recipes-container"> <!-- Estado de carga de la IA -->
<div class="row mb-4" v-for="recipe in results" :key="recipe.id"> <section v-if="loading && aiRecipes.length === 0" class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p>Buscando recetas...</p>
</section>
<!-- Mostrar las recetas -->
<section v-if="aiRecipes.length" class="recipes-container">
<div class="row mb-4" v-for="(recipe, index) in aiRecipes" :key="recipe.id">
<div class="card h-100 recipe-card-custom"> <div class="card h-100 recipe-card-custom">
<div class="card-body"> <div class="card-body d-flex justify-content-between align-items-center">
<h5 class="card-title">{{ recipe.name }}</h5> <div>
<p class="card-text">{{ recipe.description }}</p> <h5 class="card-title">{{ recipe.name }}</h5>
<div class="d-flex justify-content-end"> <p class="card-text">{{ recipe.description }}</p>
<button @click="openModal(recipe)" class="btn btn-primary-custom mt-auto">Ver más</button>
</div> </div>
<button @click="openModal(index)" class="btn btn-primary-custom mt-auto">Ver más</button>
</div> </div>
</div> </div>
</div> </div>
</section>
<div class="text-center mt-4"> <section v-if="loading && aiRecipes.length" class="text-center mt-4">
<button @click="loadMore" class="btn btn-secondary-custom"> <div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p>Cargando más recetas...</p>
</section>
<section v-if="!loading && aiSessionId" class="text-center mt-4">
<button @click="handleLoadMore" class="btn btn-secondary-custom">
Ver más recetas Ver más recetas
</button> </button>
</section>
<section v-else-if="errorMsg" class="text-center">
<div class="alert alert-danger">
{{ errorMsg }}
</div> </div>
</section> </section>
<!-- No devuelve resultados -->
<section v-else-if="!loading && searchCompleted" class="text-center">
<div class="alert alert-info">
No se encontraron recetas para tu búsqueda.
</div>
</section>
<!-- Modal para el detalle de la receta seleccionada -->
<div class="modal fade" id="recipeModal" tabindex="-1" aria-labelledby="recipeModalLabel" aria-hidden="true"> <div class="modal fade" id="recipeModal" tabindex="-1" aria-labelledby="recipeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg"> <div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content modal-custom"> <div class="modal-content modal-custom" v-if="!loadingDetail && aiRecipe">
<div class="modal-header">
<h3 class="modal-title fw-bold w-100 text-center" id="recipeModalLabel">{{ aiRecipe.name }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body">
<div>
<!-- Descripción -->
<div class="text-center w-75 mx-auto mb-3">
<p class="recipe-description">{{ aiRecipe.description }}</p>
</div>
<!-- Ingredientes -->
<h5 class="fw-bold mt-4">Ingredientes</h5>
<ul v-if="aiRecipe.ingredients && aiRecipe.ingredients.length" class="ingredients-list">
<li v-for="(ingredient, i) in aiRecipe.ingredients" :key="i" class="ingredient-item">
<span class="ingredient-name">{{ ingredient.name }}</span>
<span class="ingredient-amount">
{{ ingredient.quantity }} {{ ingredient.unitOfMeasure }}
</span>
</li>
</ul>
<p v-else>No hay ingredientes especificados.</p>
<!-- Pasos -->
<h5 class="fw-bold mt-4">Pasos de preparación</h5>
<ol v-if="aiRecipe.steps && aiRecipe.steps.length" class="steps-list">
<li v-for="(step, i) in aiRecipe.steps" :key="i" class="step-item">
<span class="step-number">{{ i + 1 }}</span>
<span class="step-text">{{ step.description }}</span>
</li>
</ol>
<p v-else>No hay pasos especificados.</p>
</div>
</div>
<div class="modal-footer d-flex justify-content-center">
<button @click="importRecipe" class="btn btn-import">
<i class="bi bi-save"></i> Importar Receta
</button>
</div>
</div>
<div v-else-if="loadingDetail" class="modal-content modal-custom">
<div class="modal-body text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p>Cargando detalles de la receta...</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup>
import { useRecipeStore } from '@/stores/recipeStore';
import { Modal } from 'bootstrap';
import * as bootstrap from 'bootstrap';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
const recipeStore = useRecipeStore();
const router = useRouter();
const query = ref('');
const searchCompleted = ref(false);
// Propiedades computadas para acceder al estado del store
const aiRecipes = computed(() => recipeStore.aiRecipes);
const aiSessionId = computed(() => recipeStore.aiSessionId);
const loading = computed(() => recipeStore.loading);
const aiRecipe = computed(() => recipeStore.aiRecipe);
const loadingDetail = computed(() => recipeStore.loadingDetail);
const errorMsg = ref('');
// Función para hacer la primera búsqueda
async function handleAISearch() {
if(!query.value.trim()) {
return;
}
searchCompleted.value = true;
try {
await recipeStore.searchAI(query.value);
} catch (error) {
console.error('Error al hacer la búsqueda por IA:', error);
if (error.response.status == 429 || error.response.status == 500) {
errorMsg.value = 'Hemos alcanzado temporalmente nuestro límite de uso de la IA. Vuelve a intentarlo en otro momento.'
}
}
};
// Función para cargar más recetas (mismo query)
async function handleLoadMore() {
try {
await recipeStore.searchMoreAI(aiSessionId.value);
} catch (error) {
console.error('Error al cargar más recetas:', error);
}
}
// Función para mostrar el modal con la receta seleccionada
async function openModal(index) {
recipeStore.aiRecipe = null;
// Se muestra el modal
const modalElement = document.getElementById('recipeModal');
const bsModal = new bootstrap.Modal(modalElement);
bsModal.show();
try {
await recipeStore.getDetailAI(aiSessionId.value, index);
} catch (error) {
console.error('Error al obtener el detalle de la receta:', error);
bsModal.hide();
}
};
// Función para importar la receta
async function importRecipe() {
try {
const aiRecipeData = aiRecipe.value;
if (!aiRecipeData) {
console.error('No hay receta para importar.');
return;
}
// Formato para subir archivos
const formData = new FormData();
formData.append('name', aiRecipeData.name);
formData.append('description', aiRecipeData.description);
aiRecipeData.ingredients.forEach((ingredient, index) => {
formData.append(`ingredients[${index}].name`, ingredient.name);
formData.append(`ingredients[${index}].quantity`, ingredient.quantity);
formData.append(`ingredients[${index}].unitOfMeasure`, ingredient.unitOfMeasure);
});
aiRecipeData.steps.forEach((step, index) => {
formData.append(`steps[${index}].number`, step.number);
formData.append(`steps[${index}].description`, step.description);
});
const newRecipeId = await recipeStore.create(formData);
const modalElement = document.getElementById('recipeModal');
const bsModal = Modal.getInstance(modalElement);
if (bsModal) bsModal.hide();
router.push(`/recipes/detail/${newRecipeId}`);
} catch (error) {
console.error('Error al importar la receta:', error);
alert('Ocurrió un error al importar la receta. Inténtalo de nuevo.');
}
};
</script>
<style scoped> <style scoped>
.text-center-custom { .text-center-custom, .modal-title {
color: #2C0C21; color: #2C0C21;
} }
...@@ -63,13 +250,13 @@ ...@@ -63,13 +250,13 @@
box-shadow: 0 0 0 0.25rem rgba(121, 62, 108, 0.25); box-shadow: 0 0 0 0.25rem rgba(121, 62, 108, 0.25);
} }
.btn-search-ai { .btn-search-ai, .btn-import {
background-color: #793E6C; background-color: #793E6C;
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
} }
.btn-search-ai:hover { .btn-search-ai:hover, .btn-import:hover {
background-color: #5e3054; background-color: #5e3054;
color: #fff; color: #fff;
} }
...@@ -118,58 +305,73 @@ ...@@ -118,58 +305,73 @@
background-color: #f1e0ee; background-color: #f1e0ee;
border-color: #d1c8cd; border-color: #d1c8cd;
} }
</style>
<script> /* Estilo ingredientes */
import { Modal } from 'bootstrap'; .modal-body h5 {
background: #f5f5f5;
padding: 6px 12px;
border-radius: 6px;
margin-bottom: 12px;
}
export default { .ingredients-list {
data() { list-style: none;
return { padding: 0;
query: '', margin: 0;
results: [], }
page: 0,
selectedRecipe: null .ingredient-item {
}; display: flex;
}, justify-content: space-between;
methods: { padding: 6px 10px;
async searchRecipes() { border-bottom: 1px solid #eee;
this.page = 0; max-width: 60%;
this.results = []; }
await this.fetchRecipes();
}, .ingredient-name {
async loadMore() { font-weight: 500;
this.page++; }
await this.fetchRecipes();
}, .ingredient-amount {
async fetchRecipes() { color: #555;
// Simula la llamada a la API font-size: 0.95rem;
const newRecipes = this.simulateApiCall(this.query, this.page); }
this.results.push(...newRecipes);
}, /* Estilo pasos */
simulateApiCall(query, page) { .steps-list {
// Esta es una función de ejemplo que simula los resultados list-style: none;
console.log(`Buscando "${query}" - Página ${page}`); padding: 0;
const baseId = page * 4; margin: 0;
return [ }
{ id: baseId + 1,
name: 'Receta de Ejemplo 1', .step-item {
description: 'Auto-layout for flexbox grid columns also means you can set the width of one column and have the sibling columns automatically resize around it. You may use predefined grid classes.'}, display: flex;
{ id: baseId + 2, align-items: flex-start;
name: 'Receta de Ejemplo 2', margin-bottom: 12px;
description: 'Una descripción corta de la receta...'}, background: light;
{ id: baseId + 3, padding: 10px 14px;
name: 'Receta de Ejemplo 3', border-radius: 8px;
description: 'Una descripción corta de la receta...'} box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
]; }
},
openModal(recipe) { .step-number {
this.selectedRecipe = recipe; flex-shrink: 0;
// Aquí puedes cargar el contenido completo de la receta, ya sea desde la API o si ya lo tienes width: 28px;
const modalElement = document.getElementById('recipeModal'); height: 28px;
const bsModal = new Modal(modalElement); border-radius: 50%;
bsModal.show(); background: #793e6c;
} color: white;
} font-weight: bold;
}; font-size: 0.9rem;
</script> display: flex;
\ No newline at end of file align-items: center;
justify-content: center;
margin-right: 12px;
}
.step-text {
flex: 1;
color: #333;
line-height: 1.4;
}
</style>
\ No newline at end of file
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
</div> </div>
<div v-for="ingredient in ingredients" :key="ingredient.id" class="row align-items-center mb-2"> <div v-for="ingredient in ingredients" :key="ingredient.id" class="row align-items-center mb-2">
<div class="col-3"> <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>
<div class="col-3"> <div class="col-3">
<input type="text" class="form-control" v-model="ingredient.unitOfMeasure" placeholder="Ej: tazas"> <input type="text" class="form-control" v-model="ingredient.unitOfMeasure" placeholder="Ej: tazas">
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<div v-else class="mb-4 mt-3 text-center"> <div v-else class="mb-4 mt-3 text-center">
<label for="recipePicture" class="form-label fw-bold">Cambiar imagen</label> <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> </div>
<!-- Descripción --> <!-- Descripción -->
...@@ -68,25 +68,37 @@ ...@@ -68,25 +68,37 @@
<hr class="my-4" /> <hr class="my-4" />
<!-- Ingredientes -->
<div class="row"> <div class="row">
<!-- Ingredientes -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="mb-4"> <div class="mb-4">
<h5 class="fw-bold">Ingredientes</h5> <h5 class="section-title">
<ul v-if="mode === 'view'" class="list-unstyled"> <i class="bi bi-basket me-2"></i> Ingredientes
<li v-for="(ing, index) in processedIngredients" :key="index" class="mb-1"> </h5>
<i class="bi bi-check-circle-fill me-2 text-success"></i>
{{ ing.quantity }} {{ ing.unit }} {{ ing.connective }} {{ ing.name }} <!-- 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> </li>
</ul> </ul>
<!-- Edición -->
<div v-else class="ingredient-form-section"> <div v-else class="ingredient-form-section">
<div v-for="(ing, index) in editableRecipe.ingredients" :key="index" class="input-group mb-2"> <div v-for="(ing, index) in editableRecipe.ingredients" :key="index" class="ingredient-form mb-2">
<input type="number" v-model="ing.quantity" class="form-control" placeholder="Cantidad"> <input type="text" v-model="ing.name" class="form-control mb-2" placeholder="Ingrediente">
<input type="text" v-model="ing.unitOfMeasure" class="form-control" placeholder="Unidad"> <div class="d-flex gap-2 mt-2">
<input type="text" v-model="ing.name" class="form-control" placeholder="Ingrediente"> <input type="text" v-model="ing.quantity" class="form-control" placeholder="Cantidad">
<button @click="editableRecipe.ingredients.splice(index, 1)" class="btn btn-outline-danger" type="button"><i class="bi bi-trash"></i></button> <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> </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 <i class="bi bi-plus-circle"></i> Añadir Ingrediente
</button> </button>
</div> </div>
...@@ -96,21 +108,28 @@ ...@@ -96,21 +108,28 @@
<!-- Pasos --> <!-- Pasos -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="mb-4"> <div class="mb-4">
<h5 class="fw-bold">Pasos de Preparación</h5> <h5 class="section-title">
<ol v-if="mode === 'view'"> <i class="bi bi-list-ol me-2"></i> Pasos de preparación
<li v-for="(step, index) in recipe.steps" :key="index" class="mb-2"> </h5>
{{ step.description }}
<!-- 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> </li>
</ol> </ol>
<!-- Edición -->
<div v-else class="steps-form-section"> <div v-else class="steps-form-section">
<div v-for="(step, index) in editableRecipe.steps" :key="index" class="input-group mb-2"> <div v-for="(step, index) in editableRecipe.steps" :key="index" class="step-form">
<span class="input-group-text">{{ index + 1 }}</span> <div class="d-flex gap-2 mb-2">
<textarea <span class="input-group-text">{{ index + 1 }}</span>
v-model="step.description" <textarea v-model="step.description" class="form-control" rows="2"></textarea>
class="form-control" <button @click="editableRecipe.steps.splice(index, 1)" class="btn btn-outline-danger" type="button">
rows="3"> <i class="bi bi-trash"></i>
</textarea> </button>
<button @click="editableRecipe.steps.splice(index, 1)" class="btn btn-outline-danger" type="button"><i class="bi bi-trash"></i></button> </div>
</div> </div>
<button @click="editableRecipe.steps.push({ description: '' })" class="btn btn-outline-primary-custom mt-2 w-100"> <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 <i class="bi bi-plus-circle"></i> Añadir Paso
...@@ -120,6 +139,7 @@ ...@@ -120,6 +139,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -175,12 +195,12 @@ const processedIngredients = computed(() => { ...@@ -175,12 +195,12 @@ const processedIngredients = computed(() => {
return recipe.value.ingredients.map(ing => { return recipe.value.ingredients.map(ing => {
const unit = ing.unitOfMeasure ? ing.unitOfMeasure.toLowerCase() : ''; const unit = ing.unitOfMeasure ? ing.unitOfMeasure.toLowerCase() : '';
const name = ing.name ? ing.name.toLowerCase() : ''; const name = ing.name ? ing.name.toLowerCase() : '';
const connective = ing.unitOfMeasure ? 'de' : ''; // const connective = ing.unitOfMeasure ? 'de' : '';
return { return {
...ing, ...ing,
unit, unit,
name, name
connective // connective
}; };
}); });
}); });
...@@ -333,4 +353,73 @@ h5 { ...@@ -333,4 +353,73 @@ h5 {
color: white; color: white;
font-size: 14px; 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> </style>
\ No newline at end of file
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 sign in to comment