From 4d358687e08a12e59822d5e980e0eeaa64884297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreu=20Vall=20Hern=C3=A0ndez?= Date: Sun, 3 Dec 2023 10:55:56 +0100 Subject: [PATCH 01/59] Stage deploy again --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index abf903e..87e2a1a 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,4 @@ npm install -g serve npm run build serve -s build ``` + From 23030bdb1643cb964a6fa102622ccf767962f403 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Sun, 3 Dec 2023 11:57:17 +0100 Subject: [PATCH 02/59] AC1 - Follow and unfollow user on recipe details page --- src/components/RecipeDetail.jsx | 169 +++++++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 23 deletions(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index b19f4a1..b603d90 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -36,7 +36,8 @@ import AddRecipeToCollection from "./AddRecipeToCollection"; function RecipeDetail() { const { token, isLogged } = useAuth(); - const [username, setUsername] = useState(localStorage.getItem("currentUser")); + const [myUserId, setMyUserId] = useState(''); + const [myUserName, setMyUserName] = useState(localStorage.getItem("currentUser")); const [userId, setUserId] = useState(''); const [userName, setUserName] = useState(''); const [userImage, setUserImage] = useState(''); @@ -46,6 +47,13 @@ function RecipeDetail() { const [recipe, setRecipe] = useState({ images: [] }); const [showReviews, setShowReviews] = useState(false); const [isLiked, setIsLiked] = useState(false); + const [adminMode, setadminMode] = useState(false); + const [showUnfollowModal, setShowUnfollowModal] = useState(false); + const [showLoginRedirectModal, setShowLoginRedirectModal] = useState(false); + const [confirmationMessage, setConfirmationMessage] = useState(""); + const [showConfirmation, setShowConfirmation] = useState(false); + const [isFollowing, setIsFollowing] = useState(null); + const [userFollowers, setUserFollowers] = useState([]); const [imageModal, setImageModal] = useState({ show: false, @@ -74,10 +82,18 @@ function RecipeDetail() { useEffect(() => { getRecipe(); if (isLogged()) { - getIsLiked(username); + getIsLiked(userName); } }, [id, reloadReviews]); + useEffect(() => { + if(myUserName == userName){ + setadminMode(true); + }else{ + setIsFollowing(userFollowers.includes(myUserName)); + } + }, [userName, userFollowers]); + const getRecipe = () => { fetch(process.env.REACT_APP_API_URL + `/recipe/${id}`) .then((response) => response.json()) @@ -106,12 +122,16 @@ function RecipeDetail() { setUserId(data.user_id); setUserName(data.username); setUserImage(data.profile_picture || ''); + setUserFollowers(data.followers || []); } catch (error) { console.error('Error fetching user data:', error); } }; - const handleNavigate = (userId) => { + const handleNavigate = (event, userId) => { + if (event) { + event.stopPropagation(); + } navigate(`/UserProfile/${userId}`); }; @@ -207,6 +227,68 @@ function RecipeDetail() { )); + const handleFollow = async () => { + if (!isLogged()) { + setShowLoginRedirectModal(true); + return; + } + + try { + const response = await fetch(process.env.REACT_APP_API_URL + `/user/follow/${userName}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + + if (response.ok) { + setConfirmationMessage(`You're now following ${userName}.`); + setIsFollowing(true); + setToastData({ + message: `You're now following ${userName}.`, + variant: 'success', + show: true, + }); + } else { + throw new Error('There was an error following the user.'); + } + } catch (error) { + console.error('Error following user:', error); + setConfirmationMessage(error.toString()); + setShowConfirmation(true); + } + }; + + const handleUnfollow = async () => { + try { + const response = await fetch(process.env.REACT_APP_API_URL + `/user/unfollow/${userName}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + + if (response.ok) { + setConfirmationMessage("You have unfollowed the user."); + setIsFollowing(false) + setToastData({ + message: `You' have unfollowed ${userName}.`, + variant: 'secondary', + show: true, + }); + } else { + throw new Error('There was an error unfollowing the user.'); + } + } catch (error) { + console.error('Error unfollowing user:', error); + setConfirmationMessage(error.toString()); + setShowConfirmation(true); + } + setShowUnfollowModal(false); + }; + return (

{recipe.name}

- -
handleNavigate(recipe.user_id)}> - - - {'Author:'} - - - - - - {userName} - - -
-

More information

@@ -315,7 +378,36 @@ function RecipeDetail() { - + +
handleNavigate(null, recipe.user_id)}> + + + + + +

{userName}

+ + + {!adminMode && ( + + )} + +
+
+ +
{toastData.message} + + setShowUnfollowModal(false)}> + + Unfollow User + + Do you want to unfollow this user? + + + + + + + setShowLoginRedirectModal(false)}> + + Required log in + + You need to log in to follow this user. + + + + + + ); } From efe24a52591eb11b4c89346dd8d36e75f927ae8c Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Sun, 3 Dec 2023 13:29:09 +0100 Subject: [PATCH 03/59] AC2 - Display 'From people you follow' recipes or 'For You' recipes --- src/components/UserFeed.jsx | 129 +++++++++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 25 deletions(-) diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index 4d359d0..de2b331 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -1,43 +1,122 @@ -//React +import { useAuth } from "./AuthContext"; import React, { useState, useEffect } from "react"; import RecipeList from "./RecipeList"; - -//Bootstrap -import { Container, Spinner } from "react-bootstrap"; +import { Container, Spinner, ButtonGroup, Button, Row } from "react-bootstrap"; function UserFeed() { + const [myUserName, setMyUserName] = useState(localStorage.getItem("currentUser")); + const { token, logout, isLogged } = useAuth(); + const [myUserFollowing, setMyUserFollowing] = useState([]); const [recipes, setRecipes] = useState([]); + const [FollowingRecipes, setFollowingRecipes] = useState([]); const [loading, setLoading] = useState(true); + const [feedType, setFeedType] = useState("forYou"); + + useEffect(() => { + getUserFollowing(); + }, [feedType]); useEffect(() => { getRecipes(); - }, []); + }, [myUserFollowing]); + + const getUserFollowing = async (userId) => { + try { + const response = await fetch(process.env.REACT_APP_API_URL + '/user/' + myUserName, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + if (!response.ok) { + throw new Error('Failed to fetch user data'); + } + const data = await response.json(); + setMyUserFollowing(data.following || []); + } catch (error) { + console.error('Error fetching user data:', error); + } + }; + + const getRecipesFollowing = async () => { + setLoading(true); + const allFollowingRecipes = []; + for (const following of myUserFollowing) { + try { + const response = await fetch(process.env.REACT_APP_API_URL + '/recipe/user/' + following + '?limit=2', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + if (!response.ok) { + throw new Error('Failed to fetch recipes for following'); + } + const followingRecipes = await response.json(); + + allFollowingRecipes.push(...followingRecipes); + } catch (error) { + console.error('Error fetching recipes for following:', error); + } + } + + setRecipes(allFollowingRecipes); + setLoading(false); + }; const getRecipes = () => { setLoading(true); - fetch(process.env.REACT_APP_API_URL + "/recipe/") - .then((response) => response.json()) - .then((data) => { - console.log(data) - setRecipes(data); - setLoading(false); - }) - .catch((error) => { - console.error("Error al obtener recetas:", error); - setLoading(false); - }); - } + if (feedType === "following") { + getRecipesFollowing(); + } else { + let url = process.env.REACT_APP_API_URL + "/recipe/"; + fetch(url) + .then((response) => response.json()) + .then((allRecipes) => { + const filteredRecipes = allRecipes.filter(recipe => + !myUserFollowing.includes(recipe.username) + ); + setRecipes(filteredRecipes); + setLoading(false); + }) + .catch((error) => { + console.error("Error al obtener recetas:", error); + setLoading(false); + }); + } +}; + return ( - loading ? ( - - - - ) : ( - - ) + + +
+ + + + +
+ {loading ? ( + + + + ) : ( + + )} +
); - } export default UserFeed; From a402502d9678f78d63d55e98c170e90c9cc6d713 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Mon, 4 Dec 2023 09:53:50 +0100 Subject: [PATCH 04/59] AC 5 - Discover creators! --- src/components/UserFeed.jsx | 38 ++++++++++-------- src/components/UserProfile.jsx | 72 ++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index de2b331..e49c068 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -11,6 +11,7 @@ function UserFeed() { const [FollowingRecipes, setFollowingRecipes] = useState([]); const [loading, setLoading] = useState(true); const [feedType, setFeedType] = useState("forYou"); + const [maxRecipesPerUser, setMaxRecipesperUser] = useState(2); useEffect(() => { getUserFollowing(); @@ -44,7 +45,7 @@ function UserFeed() { const allFollowingRecipes = []; for (const following of myUserFollowing) { try { - const response = await fetch(process.env.REACT_APP_API_URL + '/recipe/user/' + following + '?limit=2', { + const response = await fetch(process.env.REACT_APP_API_URL + '/recipe/user/' + following, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, @@ -55,8 +56,8 @@ function UserFeed() { throw new Error('Failed to fetch recipes for following'); } const followingRecipes = await response.json(); - - allFollowingRecipes.push(...followingRecipes); + allFollowingRecipes.push(...followingRecipes.slice(0, maxRecipesPerUser)); + console.error(followingRecipes) } catch (error) { console.error('Error fetching recipes for following:', error); } @@ -93,20 +94,23 @@ function UserFeed() {
- - - - + + + + +
{loading ? ( diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index ecb1c5a..dc57a7c 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -49,6 +49,7 @@ const UserProfile = () => { const [isFollowing, setIsFollowing] = useState(null); const [followerDetails, setFollowerDetails] = useState([]); const [followingDetails, setFollowingDetails] = useState([]); + const [suggestedUsers, setSuggestedUsers] = useState([]); const [adminMode, setadminMode] = useState(false); @@ -154,9 +155,29 @@ const UserProfile = () => { setUserDetails(userDetails); }; + const fetchSuggestedUsers = async () => { + try { + const response = await fetch(process.env.REACT_APP_API_URL + `/user/new/discover`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + if (!response.ok) { + throw new Error('Failed to fetch suggested users'); + } + const data = await response.json(); + setSuggestedUsers(data); + } catch (error) { + console.error('Error fetching suggested users:', error); + } + }; + useEffect(() => { if (isLogged) { fetchMyUserData(); + fetchSuggestedUsers(); } }, [token, navigate]); @@ -484,11 +505,10 @@ const UserProfile = () => { useEffect(() => { setIsFollowing(userFollowers.includes(myUserName)); - console.error(userFollowers) }, [userFollowers]); const handleFollow = async () => { - if (isLogged) { + if (!isLogged) { setShowLoginRedirectModal(true); return; } @@ -820,30 +840,60 @@ const UserProfile = () => { Following - {followingDetails.length > 0 ? ( - followingDetails.map((follower, index) => ( - - handleNavigate(follower._id)}> + {userFollowing.length > 0 ? ( + followingDetails.map((following, index) => ( + + handleNavigate(following._id)}> - {follower.username} + {following.username} - )) + )) + ) : ( + adminMode ? ( + <> +

Discover creators that match your taste!

+ {suggestedUsers.length > 0 ? ( + suggestedUsers.map((user, index) => ( + + handleNavigate(user._id)}> + + + + + + + {user.username} + + + + + + )) + ) : ( +

Loading...

+ )} + ) : ( -

There are no users to display.

- )} +

You are not following anyone yet.

+ ) + )}
From a1b731d34d70cf89f5dd4ec331c5164903e9e777 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Mon, 4 Dec 2023 17:46:59 +0100 Subject: [PATCH 05/59] Some fixed errors and suggested changes by the pr's reviewers --- src/components/AuthContext.jsx | 1 + src/components/RecipeDetail.jsx | 10 +++--- src/components/UserFeed.jsx | 56 ++++++++++++++++++++++++++------- src/components/UserProfile.jsx | 2 +- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/components/AuthContext.jsx b/src/components/AuthContext.jsx index 5ca9be0..7db0259 100644 --- a/src/components/AuthContext.jsx +++ b/src/components/AuthContext.jsx @@ -26,5 +26,6 @@ export const useAuth = () => { if (context === undefined) { throw new Error('useAuth debe ser usado dentro de un AuthProvider'); } + console.error("TOKEN?", context) return context; } \ No newline at end of file diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index b603d90..6cd9f0d 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -379,22 +379,22 @@ function RecipeDetail() { -
handleNavigate(null, recipe.user_id)}> +
handleNavigate(null, recipe.user_id)}> - + - +

{userName}

{!adminMode && ( @@ -117,8 +135,24 @@ function UserFeed() { ) : ( - + )} + + setShowLoginRedirectModal(false)}> + + Required log in + + You need to log in to view the recipes of the people you follow. + + + + + + ); } diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index dc57a7c..9ab4c48 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -864,7 +864,7 @@ const UserProfile = () => { ) : ( adminMode ? ( <> -

Discover creators that match your taste!

+

You're not following anyone. Discover creators that match your taste!

{suggestedUsers.length > 0 ? ( suggestedUsers.map((user, index) => ( From d55ea44854e31074fb2fdd5378476d0281a053ac Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:43:56 +0100 Subject: [PATCH 06/59] feat: clean reviews --- src/components/Reviews.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index d8b6fc5..39aec25 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from "react"; import { Container, Row, Col, Button, Image } from "react-bootstrap"; import PostReview from "./PostReview"; -import chefIcon from "../assets/icons/chef.png"; import { StarFill, PatchCheck } from "react-bootstrap-icons"; import LikesReview from "./LikesReview"; import ImageModal from "./ImageModal"; From e74a19f60b08d8cb1bd8b95649a0191e45ac0cfa Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:12:11 +0100 Subject: [PATCH 07/59] feat: Change variable env --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 3ed23bb..2737d5b 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -REACT_APP_API_URL = 'http://127.0.0.1:8000' +REACT_APP_API_URL = 'https://kasula-develop-5q5vehm3ja-ew.a.run.app' # LA VARIABLE ESTÀ PENSADA PERQUÈ NO PORTI BARRA AL FINAL From 8d8baa5da3cf1506d775c2c92ad9e53a15358d02 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:25:36 +0100 Subject: [PATCH 08/59] feat: Create and implement new component ModifyReview --- src/components/ModifyReview.jsx | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/components/ModifyReview.jsx diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx new file mode 100644 index 0000000..f67efd2 --- /dev/null +++ b/src/components/ModifyReview.jsx @@ -0,0 +1,91 @@ +// ModifyReview.js +import React, { useState } from 'react'; +import { Button, Modal, Form } from 'react-bootstrap'; + +const ModifyReview = ({ reviewId, recipeId, onHide, reloadReviews }) => { + const [newComment, setNewComment] = useState(''); + + const handleUpdateReview = async () => { + // Realizar la lógica para actualizar la revisión usando el endpoint PUT + try { + const response = await fetch( + `${process.env.REACT_APP_API_URL}/review/${recipeId}/${reviewId}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ comment: newComment }), + } + ); + + const data = await response.json(); + if (response.ok) { + console.log('Review actualizada:', data); + reloadReviews(); + } else { + console.error('Error al actualizar la revisión:', data); + } + } catch (error) { + console.error('Error en la solicitud de actualización de la revisión:', error); + } + + onHide(); + }; + + const handleDeleteReview = async () => { + // Realizar la lógica para eliminar la revisión usando el endpoint DELETE + try { + const response = await fetch( + `${process.env.REACT_APP_API_URL}/review/${recipeId}/${reviewId}`, + { + method: 'DELETE', + } + ); + + const data = await response.json(); + if (response.ok) { + console.log('Review eliminada:', data); + reloadReviews(); + } else { + console.error('Error al eliminar la revisión:', data); + } + } catch (error) { + console.error('Error en la solicitud de eliminación de la revisión:', error); + } + + onHide(); + }; + + return ( + + + Modificar o eliminar revisión + + +
+ + Nuevo Comentario + setNewComment(e.target.value)} + /> + +
+
+ + + + +
+ ); +}; + +export default ModifyReview; From 41f78df69e3dee9553f8dbfc9c97e173697c1871 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:00:13 +0100 Subject: [PATCH 09/59] feat: Implement edit and delete review --- src/components/ModifyReview.jsx | 70 ++++++++++++++++++++++----------- src/components/Reviews.jsx | 49 +++++++++++++++++++++-- 2 files changed, 92 insertions(+), 27 deletions(-) diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx index f67efd2..319bda2 100644 --- a/src/components/ModifyReview.jsx +++ b/src/components/ModifyReview.jsx @@ -1,19 +1,21 @@ // ModifyReview.js import React, { useState } from 'react'; import { Button, Modal, Form } from 'react-bootstrap'; +import { useAuth } from "./AuthContext"; -const ModifyReview = ({ reviewId, recipeId, onHide, reloadReviews }) => { +const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct }) => { const [newComment, setNewComment] = useState(''); + const { token } = useAuth(); const handleUpdateReview = async () => { - // Realizar la lógica para actualizar la revisión usando el endpoint PUT try { const response = await fetch( `${process.env.REACT_APP_API_URL}/review/${recipeId}/${reviewId}`, { method: 'PUT', headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, }, body: JSON.stringify({ comment: newComment }), } @@ -34,12 +36,15 @@ const ModifyReview = ({ reviewId, recipeId, onHide, reloadReviews }) => { }; const handleDeleteReview = async () => { - // Realizar la lógica para eliminar la revisión usando el endpoint DELETE try { const response = await fetch( `${process.env.REACT_APP_API_URL}/review/${recipeId}/${reviewId}`, { method: 'DELETE', + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, } ); @@ -57,32 +62,49 @@ const ModifyReview = ({ reviewId, recipeId, onHide, reloadReviews }) => { onHide(); }; + const handleConfirmDelete = () => { + // Aquí puedes agregar lógica adicional antes de confirmar la eliminación + handleDeleteReview(); + }; + return ( - + - Modificar o eliminar revisión + {funct === 'Edit' ? 'Modificar' : 'Eliminar'} revisión -
- - Nuevo Comentario - setNewComment(e.target.value)} - /> - -
+ {funct === 'Edit' ? ( +
+ + Nuevo Comentario + setNewComment(e.target.value)} + /> + +
+ ) : ( +

Are you sure you want to delete the review?

+ )}
- - + {funct === 'Edit' ? ( + + ) : ( + <> + + + + )}
); diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 39aec25..cadcfba 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -1,9 +1,10 @@ import React, { useState, useEffect } from "react"; import { Container, Row, Col, Button, Image } from "react-bootstrap"; import PostReview from "./PostReview"; -import { StarFill, PatchCheck } from "react-bootstrap-icons"; +import { StarFill, PatchCheck, PencilSquare, Trash } from 'react-bootstrap-icons'; import LikesReview from "./LikesReview"; import ImageModal from "./ImageModal"; +import ModifyReview from "./ModifyReview"; function Reviews(props) { @@ -11,7 +12,10 @@ function Reviews(props) { const [reviews, setReviews] = useState(null); const [showModal, setShowModal] = useState(false); const [showModalImage, setShowModalImage] = useState(false); + const [showModalReview, setShowModalReview] = useState(false); const [selectedImage, setSelectedImage] = useState(null); + const [selectedReview, setSelectedReview] = useState(null); + const [selectedFunct, setSelectedFunct] = useState(null); const isLogged = window.localStorage.getItem("logged"); const currentUserUsername = localStorage.getItem('currentUser'); @@ -35,7 +39,7 @@ function Reviews(props) { setShowModalImage(false); }; - const handleOpenModal = (image) => { + const handleOpenModal = () => { setShowModal(true); }; @@ -43,6 +47,21 @@ function Reviews(props) { setShowModal(false); }; + const handleOpenModalReview = (image, review, funct) => { + setSelectedImage(image); + setSelectedReview(review); + setShowModalReview(true); + setSelectedFunct(funct); + + }; + + const handleCloseModalReview = () => { + setSelectedImage(null); + setSelectedReview(null); + setShowModalReview(false); + setSelectedFunct(null); + }; + return ( {isLogged === 'true' ? @@ -114,7 +133,21 @@ function Reviews(props) { initialLikes={review.likes || 0} likedBy={review.liked_by} reloadReviews={reloadReviews} - /> + /> + {currentUserUsername === review.username && ( + <> + handleOpenModalReview(null, review, 'Edit')} + /> + handleOpenModalReview(null, review, 'Trash')} + /> + + )}
@@ -134,6 +167,16 @@ function Reviews(props) { recipeImage={selectedImage} recipeName={selectedImage ? "Image" : null} // Puedes cambiar esto según tus necesidades /> + {selectedReview && ( + + )} ); } From 15db87f67f99896842d8f2ae0368833d89a85652 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:01:23 +0100 Subject: [PATCH 10/59] Wip: Actually only edit comment --- src/components/ModifyReview.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx index 319bda2..bdfb466 100644 --- a/src/components/ModifyReview.jsx +++ b/src/components/ModifyReview.jsx @@ -63,7 +63,6 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct } }; const handleConfirmDelete = () => { - // Aquí puedes agregar lógica adicional antes de confirmar la eliminación handleDeleteReview(); }; From dd64ef1d38e7a5120a9479d586e4488472e8f45a Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:37:17 +0100 Subject: [PATCH 11/59] feat: Update rating algo --- src/components/ModifyReview.jsx | 57 ++++++++++++++++++++++++++------- src/components/Reviews.jsx | 9 +++--- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx index bdfb466..69bcccd 100644 --- a/src/components/ModifyReview.jsx +++ b/src/components/ModifyReview.jsx @@ -2,9 +2,15 @@ import React, { useState } from 'react'; import { Button, Modal, Form } from 'react-bootstrap'; import { useAuth } from "./AuthContext"; +import { + StarFill, + Star +} from "react-bootstrap-icons"; -const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct }) => { - const [newComment, setNewComment] = useState(''); +const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, reviewInfo }) => { + const [newComment, setNewComment] = useState(reviewInfo.comment); + const [newRating, setNewRating] = useState(reviewInfo.rating); + const [newImage, setNewImage] = useState(reviewInfo.file); // Para almacenar la nueva imagen const { token } = useAuth(); const handleUpdateReview = async () => { @@ -17,7 +23,7 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct } "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ comment: newComment }), + body: JSON.stringify({ comment: newComment, rating: newRating }), } ); @@ -42,7 +48,6 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct } { method: 'DELETE', headers: { - "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, } @@ -62,6 +67,25 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct } onHide(); }; + const renderStars = (amount) => { + let stars = []; + for (let i = 1; i <= 5; i++) { + stars.push( + setNewRating(i)} + > + {i <= amount ? : } + + ); + } + return stars; + }; + const handleConfirmDelete = () => { handleDeleteReview(); }; @@ -75,7 +99,7 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct } {funct === 'Edit' ? (
- Nuevo Comentario + New Comment setNewComment(e.target.value)} /> + + New Rating +
{renderStars(newRating)}
+
+ + New Image + setNewImage(e.target.files[0])} + /> +
) : ( -

Are you sure you want to delete the review?

+

Are you sure delete review?

)} {funct === 'Edit' ? ( - + ) : ( <> )} diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index cadcfba..d8b79c4 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -47,8 +47,7 @@ function Reviews(props) { setShowModal(false); }; - const handleOpenModalReview = (image, review, funct) => { - setSelectedImage(image); + const handleOpenModalReview = (review, funct) => { setSelectedReview(review); setShowModalReview(true); setSelectedFunct(funct); @@ -56,7 +55,6 @@ function Reviews(props) { }; const handleCloseModalReview = () => { - setSelectedImage(null); setSelectedReview(null); setShowModalReview(false); setSelectedFunct(null); @@ -139,12 +137,12 @@ function Reviews(props) { handleOpenModalReview(null, review, 'Edit')} + onClick={() => handleOpenModalReview(review, 'Edit')} /> handleOpenModalReview(null, review, 'Trash')} + onClick={() => handleOpenModalReview(review, 'Trash')} /> )} @@ -175,6 +173,7 @@ function Reviews(props) { onHide={handleCloseModalReview} reloadReviews={reloadReviews} funct={selectedFunct} + reviewInfo={selectedReview} /> )} From 178650a73530b321f13de574d24b332964921cff Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 7 Dec 2023 10:39:58 +0100 Subject: [PATCH 12/59] Fix minor errors --- src/components/AuthContext.jsx | 1 - src/components/KasulaNavbar.jsx | 5 +++-- src/components/UserFeed.jsx | 1 - src/components/UserProfile.jsx | 16 +++++++++------- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/AuthContext.jsx b/src/components/AuthContext.jsx index 7db0259..5ca9be0 100644 --- a/src/components/AuthContext.jsx +++ b/src/components/AuthContext.jsx @@ -26,6 +26,5 @@ export const useAuth = () => { if (context === undefined) { throw new Error('useAuth debe ser usado dentro de un AuthProvider'); } - console.error("TOKEN?", context) return context; } \ No newline at end of file diff --git a/src/components/KasulaNavbar.jsx b/src/components/KasulaNavbar.jsx index d97dbef..b514b6c 100644 --- a/src/components/KasulaNavbar.jsx +++ b/src/components/KasulaNavbar.jsx @@ -1,6 +1,6 @@ //React import React, { useState, useEffect } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useAuth } from "./AuthContext"; import "../css/common.css"; @@ -27,6 +27,7 @@ import chef from "../assets/chef.png"; function KasulaNavbar() { const { token, logout, isLogged } = useAuth(); + const navigate = useNavigate(); const [user, setUser] = useState({}); const [showModal, setShowModal] = useState(false); const [showPostRecipe, setShowPostRecipe] = useState(false); @@ -68,13 +69,13 @@ function KasulaNavbar() { const handleClosePostRecipeSuccessfulModal = () => { setShowPostRecipe(false); - //window.location.reload(); }; const handleLogout = () => { localStorage.setItem("logged", "false"); // This will update the localStorage handleCloseModal(); // This will close the modal logout(); // This will remove the token from the localStorage + window.location.reload(); }; const CustomToggle = React.forwardRef(({ onClick }, ref) => ( diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index 8c7b3b2..45afc4f 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -11,7 +11,6 @@ function UserFeed() { const [myUserFollowing, setMyUserFollowing] = useState([]); const [recipesForYou, setRecipesForYou] = useState([]); const [recipesFollowing, setRecipesFollowing] = useState([]); - const [followingRecipes, setFollowingRecipes] = useState([]); const [logged, setLogged] = useState(false); const [loading, setLoading] = useState(true); const [feedType, setFeedType] = useState("forYou"); diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index 9ab4c48..c0f76b7 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -175,13 +175,14 @@ const UserProfile = () => { }; useEffect(() => { - if (isLogged) { + if (token!=null) { fetchMyUserData(); fetchSuggestedUsers(); } - }, [token, navigate]); + }, [token]); useEffect(() => { + if(myUserId == userId){ setadminMode(true); } @@ -260,15 +261,16 @@ const UserProfile = () => { setUserName(updatedData.username); setUserMail(updatedData.email); setUserBio(updatedData.bio || ''); + setOperationSuccess(true) setConfirmationMessage("Profile updated successfully"); } catch (error) { console.error('Error updating user data:', error); - setConfirmationMessage("Failed to update profile"); + setOperationSuccess(false) + setConfirmationMessage("Oops! Something went wrong."); } setShowConfirmation(true); - setOperationSuccess(true) setEditMode(false); }; @@ -298,14 +300,14 @@ const UserProfile = () => { }); if (!response.ok) { - throw new Error('Failed to update user data'); + throw new Error('Oops! Something went wrong.'); } const data = await response.json(); setConfirmationMessage("Profile updated successfully"); } catch (error) { console.error('Error updating user data:', error); - setConfirmationMessage("Failed to update profile"); + setConfirmationMessage("Oops! Something went wrong."); } setShowConfirmation(true); } @@ -508,7 +510,7 @@ const UserProfile = () => { }, [userFollowers]); const handleFollow = async () => { - if (!isLogged) { + if (token==null) { setShowLoginRedirectModal(true); return; } From a084972fd925d4f16f5cfebec00f34be174de972 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:24:35 +0100 Subject: [PATCH 13/59] WIP: update image in progess --- src/components/ModifyReview.jsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx index 69bcccd..db5ba48 100644 --- a/src/components/ModifyReview.jsx +++ b/src/components/ModifyReview.jsx @@ -14,6 +14,15 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, const { token } = useAuth(); const handleUpdateReview = async () => { + const bodyData = { + comment: newComment, + rating: newRating, + }; + if (newImage) { + bodyData.file = newImage; + } + console.log(">>>>NewImage: ", newImage) + try { const response = await fetch( `${process.env.REACT_APP_API_URL}/review/${recipeId}/${reviewId}`, @@ -23,7 +32,8 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ comment: newComment, rating: newRating }), + body: JSON.stringify({ comment: newComment, rating: newRating }), + file: newImage } ); From 80649225cbce06891367adb1002d65465a762153 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:49:50 +0100 Subject: [PATCH 14/59] feat: Button Review of RecipeDetail change text and design --- src/components/RecipeDetail.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index b19f4a1..734ccce 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -307,8 +307,8 @@ function RecipeDetail() { {recipe.energy ?? "No info of"} kcal
-
From baa3c1e9f045ef2e1f25f089d85babf87ec7cb7a Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:21:54 +0100 Subject: [PATCH 15/59] feat: Control errors with no reviews, etc --- src/components/Reviews.jsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index d8b79c4..522d578 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -64,6 +64,7 @@ function Reviews(props) { {isLogged === 'true' ? + {reviews && reviews.length > 0 ? (<> @@ -73,12 +74,14 @@ function Reviews(props) { onHide={handleCloseModal} reloadReviews={reloadReviews} /> + ) : null} : null}
    - {reviews ? ( + {console.log(">>>>>REVIEWS:",reviews)} + {reviews && reviews.length > 0 ? ( reviews.map((review, index) => (
  • @@ -154,7 +157,19 @@ function Reviews(props) {
  • )) ) : ( - Reviews not available +
    +

    There are currently no reviews.

    +

    Be the first one to post a review and share your thoughts!

    + + +
    )}
From 7b359e93aa198944dce7de3893a921078f226356 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:25:05 +0100 Subject: [PATCH 16/59] feat: Post review button change design --- src/components/Reviews.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 522d578..49a6db3 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -65,7 +65,7 @@ function Reviews(props) { {isLogged === 'true' ? {reviews && reviews.length > 0 ? (<> -

There are currently no reviews.

Be the first one to post a review and share your thoughts!

- Date: Thu, 7 Dec 2023 13:40:51 +0100 Subject: [PATCH 17/59] feat: Control different things, when is recipe owner --- src/components/RecipeDetail.jsx | 2 +- src/components/Reviews.jsx | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index 734ccce..733754c 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -431,7 +431,7 @@ function RecipeDetail() { Reviews - + { @@ -64,7 +64,7 @@ function Reviews(props) { {isLogged === 'true' ? - {reviews && reviews.length > 0 ? (<> + {reviews && reviews.length > 0 && owner === currentUser ? (<> @@ -80,7 +80,6 @@ function Reviews(props) {
    - {console.log(">>>>>REVIEWS:",reviews)} {reviews && reviews.length > 0 ? ( reviews.map((review, index) => (
  • @@ -135,7 +134,7 @@ function Reviews(props) { likedBy={review.liked_by} reloadReviews={reloadReviews} /> - {currentUserUsername === review.username && ( + {currentUser === review.username && ( <>
  • )) - ) : ( -
    -

    There are currently no reviews.

    + ) : ( owner !== currentUser ? + (
    +

    There are currently no reviews

    Be the first one to post a review and share your thoughts!

    +
    ) :
    +

    There are currently no reviews

    + {/*

    Be the first one to post a review and share your thoughts!

    */} +

    You are the owner of the recipe you can't do reviews

    +
    )}
From fb99c4e3be5c45c0299bb31f3b8998b4f671a4e4 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:44:57 +0100 Subject: [PATCH 18/59] feat: Comment doesn't overlap username --- src/components/Reviews.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 3c75496..a7546da 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -84,10 +84,10 @@ function Reviews(props) { reviews.map((review, index) => (
  • - + {review.username}:{" "} - {review.comment} + {review.comment} handleOpenModalReview(review, 'Edit')} /> handleOpenModalReview(review, 'Trash')} /> From b0ffbd088c5168909ddd78b43479df930b2e6be0 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:22:00 +0100 Subject: [PATCH 19/59] fix: Post review enabled no recipes no owner --- src/components/RecipeDetail.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index 733754c..f99b268 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -431,7 +431,7 @@ function RecipeDetail() { Reviews - + Date: Thu, 7 Dec 2023 14:28:14 +0100 Subject: [PATCH 20/59] fix: Fix bug enabled button --- src/components/Reviews.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index a7546da..7619ba9 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -64,7 +64,7 @@ function Reviews(props) { {isLogged === 'true' ? - {reviews && reviews.length > 0 && owner === currentUser ? (<> + {reviews && reviews.length > 0 && owner !== currentUser ? (<> From 665a8998fb53c7e42175e66e0d4840f609871306 Mon Sep 17 00:00:00 2001 From: Antonio Tigri Date: Thu, 7 Dec 2023 16:33:28 +0100 Subject: [PATCH 21/59] US22, 30 and 5 --- .env | 2 +- src/assets/jsonData/sort_options.json | 28 ++++ src/components/KasulaNavbar.jsx | 7 + src/components/RecipeBrowser.jsx | 209 ++++++++++++++++++++++++++ src/components/RecipeFilters.jsx | 147 ++++++++++++++++++ src/components/RecipeList.jsx | 31 +++- src/components/Root.js | 1 + src/components/StarSelector.jsx | 39 +++++ src/components/UserFeed.jsx | 96 ++++++++++-- src/css/PostRecipe.css | 14 +- src/css/common.css | 48 ++++++ src/css/slider.css | 37 +++++ 12 files changed, 640 insertions(+), 19 deletions(-) create mode 100644 src/assets/jsonData/sort_options.json create mode 100644 src/components/RecipeBrowser.jsx create mode 100644 src/components/RecipeFilters.jsx create mode 100644 src/components/StarSelector.jsx create mode 100644 src/css/slider.css diff --git a/.env b/.env index 3ed23bb..2737d5b 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -REACT_APP_API_URL = 'http://127.0.0.1:8000' +REACT_APP_API_URL = 'https://kasula-develop-5q5vehm3ja-ew.a.run.app' # LA VARIABLE ESTÀ PENSADA PERQUÈ NO PORTI BARRA AL FINAL diff --git a/src/assets/jsonData/sort_options.json b/src/assets/jsonData/sort_options.json new file mode 100644 index 0000000..a88e6c0 --- /dev/null +++ b/src/assets/jsonData/sort_options.json @@ -0,0 +1,28 @@ +{"fields": [{ + "name": "none", + "displayName": "---" +}, { + "name": "name", + "displayName": "Title" +}, { + "name": "average_rating", + "displayName": "Rating" +}, { + "name": "cooking_time", + "displayName": "Cooking Time" +}, { + "name": "difficulty", + "displayName": "Difficulty" +}, { + "name": "energy", + "displayName": "Energy" +}, { + "name": "username", + "displayName": "Recipe Author" +}, { + "name": "creation_date", + "displayName": "Creation Date" +}, { + "name": "updated_at", + "displayName": "Last Modified" +}]} \ No newline at end of file diff --git a/src/components/KasulaNavbar.jsx b/src/components/KasulaNavbar.jsx index d97dbef..0b7f7ec 100644 --- a/src/components/KasulaNavbar.jsx +++ b/src/components/KasulaNavbar.jsx @@ -135,6 +135,13 @@ function KasulaNavbar() { + Profile + + + { handleOpenModal(); }} diff --git a/src/components/RecipeBrowser.jsx b/src/components/RecipeBrowser.jsx new file mode 100644 index 0000000..15d22d9 --- /dev/null +++ b/src/components/RecipeBrowser.jsx @@ -0,0 +1,209 @@ +import { useState } from "react"; +import { + Container, + Stack, + InputGroup, + Form, + Offcanvas, + Badge, +} from "react-bootstrap"; +import { Funnel, FunnelFill, X, SortUp, SortDown } from "react-bootstrap-icons"; + +import RecipeFilters from "./RecipeFilters"; +import "../css/common.css"; + +function RecipeBrowser({ onSearch }) { + const defaultFilters = { + sortBy: "none", + sortAscending: false, + maxDifficulty: 5, + maxTime: 180, + minRating: 0, + maxCalories: 5000, + }; + + const [recipeName, setRecipeName] = useState(""); + const [filtersOffCanvas, setFiltersOffCanvas] = useState({ + title: "Filters", + show: false, + values: JSON.parse( + localStorage.getItem("filters") || { + sortBy: "none", + sortAscending: false, + maxDifficulty: 5, + maxTime: 180, + minRating: 0, + maxCalories: 5000, + } + ), + }); + + const handleCloseOffcanvas = ( + setOffCanvasState, + offCanvasState, + applyFilters, + filterValues + ) => { + setOffCanvasState({ + ...offCanvasState, + show: false, + values: filterValues || offCanvasState.values, + }); + if (applyFilters) { + onSearch(filterValues || offCanvasState.values, recipeName); + } + }; + + return ( + + + { + setFiltersOffCanvas({ ...filtersOffCanvas, show: true }); + }} + > + + + + { + setRecipeName(e.target.value); + onSearch(filtersOffCanvas.values, e.target.value); + }} + /> + + {recipeName.length > 0 && ( + { + setRecipeName(""); + onSearch(filtersOffCanvas.values, ""); + }} + /> + )} + + + + + {JSON.stringify(filtersOffCanvas.values) !== + JSON.stringify(defaultFilters) && ( +
    { + setFiltersOffCanvas({ + ...filtersOffCanvas, + values: defaultFilters, + }); + localStorage.setItem("filters", JSON.stringify(defaultFilters)); + onSearch(defaultFilters, recipeName); + }} + > + + Clear filters + +
    + )} + + {Object.entries(filtersOffCanvas.values) + .filter( + ([key, value]) => + value !== defaultFilters[key] || + (key === "sortAscending" && + JSON.stringify(filtersOffCanvas.values) !== + JSON.stringify(defaultFilters)) + ) + .map(([key, value]) => ( + + {key}:{" "} + {value === true + ? "Yes" + : value === false + ? "No" + : value.toString()} + {key === "sortAscending" ? ( +
    { + const newValues = { ...filtersOffCanvas.values }; + newValues[key] = !newValues[key]; + setFiltersOffCanvas({ + ...filtersOffCanvas, + values: newValues, + }); + localStorage.setItem( + "filters", + JSON.stringify(newValues) + ); + onSearch(newValues, recipeName); + }} + > + {value ? ( + + ) : ( + + )} +
    + ) : ( + { + const newValues = { ...filtersOffCanvas.values }; + newValues[key] = defaultFilters[key]; + setFiltersOffCanvas({ + ...filtersOffCanvas, + values: newValues, + }); + localStorage.setItem( + "filters", + JSON.stringify(newValues) + ); + onSearch(newValues, recipeName); + }} + /> + )} +
    + ))} +
    +
    + + handleCloseOffcanvas(setFiltersOffCanvas, filtersOffCanvas, false) + } + > + + + {filtersOffCanvas.title} + + + + { + localStorage.setItem("filters", JSON.stringify(filters)); + handleCloseOffcanvas( + setFiltersOffCanvas, + filtersOffCanvas, + true, + filters + ); + }} + inValues={filtersOffCanvas.values} + /> + + +
    + ); +} + +export default RecipeBrowser; diff --git a/src/components/RecipeFilters.jsx b/src/components/RecipeFilters.jsx new file mode 100644 index 0000000..902627f --- /dev/null +++ b/src/components/RecipeFilters.jsx @@ -0,0 +1,147 @@ +import { useState } from "react"; +import { Container, Button, Form } from "react-bootstrap"; +import { SortUp, SortDown, X, Funnel } from "react-bootstrap-icons"; +import StarSelector from "./StarSelector"; +import sortOptions from "../assets/jsonData/sort_options.json"; + +function RecipeFilters({ onClose, inValues }) { + const defaultFilters = { + sortBy: "none", + sortAscending: false, + maxDifficulty: 5, + maxTime: 180, + minRating: 0, + maxCalories: 5000, + }; + + const [filters, setFilters] = useState(inValues || defaultFilters); + + return ( + + {JSON.stringify(filters) !== JSON.stringify(defaultFilters) && ( +
    setFilters(defaultFilters)} + > + + Clear filters + +
    + )} +
    + + Sort By +
    + + {sortOptions.fields.map((option) => ( + + ))} + + { + setFilters({ + ...filters, + sortAscending: !filters.sortAscending, + }); + }} + > + {filters.sortAscending ? : } + +
    +
    + + Difficulty +
    + + setFilters({ ...filters, maxDifficulty: value }) + } + /> +
    +
    + + Rating +
    + setFilters({ ...filters, minRating: value })} + /> +
    +
    + + Time +
    + + setFilters({ ...filters, maxTime: e.target.value }) + } + /> +
    + {filters.maxTime} min +
    +
    +
    +
    + 0 min + 180 min +
    + + Calories +
    + + setFilters({ ...filters, maxCalories: e.target.value }) + } + /> +
    + {filters.maxCalories} cal +
    +
    +
    +
    + 0 cal + 5000 cal +
    +
    + +
    +
    +
    + ); +} + +export default RecipeFilters; diff --git a/src/components/RecipeList.jsx b/src/components/RecipeList.jsx index c94d110..87c7867 100644 --- a/src/components/RecipeList.jsx +++ b/src/components/RecipeList.jsx @@ -1,5 +1,6 @@ //React import { CSSTransition } from "react-transition-group"; +import { useRef, useEffect } from "react"; //Bootstrap import { Link } from "react-router-dom"; @@ -11,21 +12,39 @@ import RecipeCard from "./RecipeCard"; //CSS import "../css/Transitions.css"; +import { wait } from "@testing-library/user-event/dist/utils"; -function recipeList({ recipes, canDelete, onDeleteRecipe, id, token }) { +function RecipeList({ recipes, canDelete, onDeleteRecipe, onRequestLoadMore, id, token, finished }) { + const myRef = useRef(); + useEffect(() => { + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !finished) { + onRequestLoadMore(); + observer.unobserve(entries[0].target); + } + }); + if (myRef.current) { + observer.observe(myRef.current); + } + return () => { + if (myRef.current) { + observer.unobserve(myRef.current); + } + }; + }, [myRef, recipes]); return ( {recipes && recipes.length > 0 ? ( - recipes.map((recipe) => ( - + recipes.map((recipe, index) => ( + -
    +
    - There are currently no recipes + {canDelete ? "There are currently no recipes" : "There are no recipes matching the search or filters"}
    )} @@ -75,4 +94,4 @@ function recipeList({ recipes, canDelete, onDeleteRecipe, id, token }) { ); } -export default recipeList; +export default RecipeList; diff --git a/src/components/Root.js b/src/components/Root.js index 38a5423..1700475 100644 --- a/src/components/Root.js +++ b/src/components/Root.js @@ -21,6 +21,7 @@ import CollectionView from "./CollectionView"; import "../css/Transitions.css"; import "../css/Root.css"; import "../css/common.css"; +import "../css/slider.css"; function Root() { return ( diff --git a/src/components/StarSelector.jsx b/src/components/StarSelector.jsx new file mode 100644 index 0000000..87ac0e2 --- /dev/null +++ b/src/components/StarSelector.jsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { Star, StarFill } from "react-bootstrap-icons"; + +function StarSelector({ onSelect, maxValue, value, starSize, type }) { + const [hoveredStar, setHoveredStar] = useState({ + value: 0, + active: false, + }); + let stars = []; + for (let i = 1; i <= maxValue; i++) { + const isActive = i <= value; + const isHovered = i <= hoveredStar.value && hoveredStar.active; + const aboveHovered = i > hoveredStar.value && hoveredStar.active; + stars.push( + { + onSelect((isActive && i === value ? i - 1 : i)); + }} + onMouseEnter={() => setHoveredStar({ value: i, active: true })} + onMouseLeave={() => setHoveredStar({ value: 0, active: false })} + > + {isActive || isHovered ? : } + + ); + } + return stars; +} + +export default StarSelector; diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index 4d359d0..04ea2f9 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -1,25 +1,59 @@ //React import React, { useState, useEffect } from "react"; +import { useAuth } from "./AuthContext"; import RecipeList from "./RecipeList"; +import RecipeBrowser from "./RecipeBrowser"; //Bootstrap import { Container, Spinner } from "react-bootstrap"; function UserFeed() { + const { isLogged } = useAuth(); + const numRecipes = isLogged() ? 24 : 9; + const loggedOutFilters = { + sortBy: "average_rating", + sortAscending: false, + }; + const [page, setPage] = useState(0); + const [filters, setFilters] = useState(isLogged() ? (JSON.parse(localStorage.getItem("filters"))) : loggedOutFilters); + const [recipeName, setRecipeName] = useState(null); const [recipes, setRecipes] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); + const [finished, setFinished] = useState(isLogged() ? false : true); useEffect(() => { - getRecipes(); + getRecipes(filters, recipeName, page, numRecipes, true); }, []); - const getRecipes = () => { + useEffect(() => { + if (!isLogged()) { + setFinished(true); + setPage(0); + setFilters(loggedOutFilters); + setRecipeName(null); + getRecipes(loggedOutFilters, null, 0, 9, true); + } + }, [isLogged]); + + const getRecipes = ( filters, recipeName, page, numRecipes, reset ) => { + if (loading) return; setLoading(true); - fetch(process.env.REACT_APP_API_URL + "/recipe/") - .then((response) => response.json()) + fetch(buildRequestUrl(filters, recipeName, page, numRecipes)) + .then((response) => { + if (!response.ok) { + if (response.status === 400) { + setPage(page - 1); + setFinished(true); + } + } + return response.json(); + }) .then((data) => { - console.log(data) - setRecipes(data); + if (reset) { + setRecipes(data); + } else { + setRecipes(recipes.concat(data)); + } setLoading(false); }) .catch((error) => { @@ -28,14 +62,54 @@ function UserFeed() { }); } + const buildRequestUrl = (filters, recipeName, page, numRecipes) => { + let url = process.env.REACT_APP_API_URL + "/recipe/" + (filters || recipeName || page || numRecipes ? "magic?" : ""); + if (filters) { + url += (filters.sortBy === 'none') ? '' : `sort_by=${filters.sortBy}&`; + url += (filters.sortBy === 'none') ? '' : `order=${filters.sortAscending}&`; + url += filters.maxDifficulty ? `max_difficulty=${filters.maxDifficulty}&` : ''; + url += filters.maxTime ? `max_cooking_time=${filters.maxTime}&` : ''; + url += filters.minRating ? `min_rating=${filters.minRating}&` : ''; + url += filters.maxCalories ? `max_energy=${filters.maxCalories}&` : ''; + } + if (recipeName) { + url += `search=${recipeName}&`; + } + if (page !== null && numRecipes !== null) { + url += `start=${page * numRecipes}&`; + url += `size=${numRecipes}`; + } + console.log(url); + return url; + } + return ( - loading ? ( + + {isLogged() && ( + { + setPage(0); + setFinished(false); + setFilters(filters); + setRecipeName(recipeName); + getRecipes(filters, recipeName, 0, numRecipes, true); + }}/> + )} + {loading && recipes.length === 0 ? ( - ) : ( - - ) + ) : ( + { + setPage(page + 1); + getRecipes(filters, recipeName, page + 1, numRecipes, false); + }} recipes={recipes} finished={finished}/> + )} + {!isLogged() && ( +
    + This is as far as you can go. Please, login or register to see more recipes. +
    + )} +
    ); } diff --git a/src/css/PostRecipe.css b/src/css/PostRecipe.css index ee63642..36dc33a 100644 --- a/src/css/PostRecipe.css +++ b/src/css/PostRecipe.css @@ -3,7 +3,15 @@ } .difficulty-star.active:hover { - color: rgb(151, 114, 1); + color: rgb(151, 91, 1); +} + +.difficulty-star.active.hovered { + color: rgb(151, 91, 1); +} + +.difficulty-star.active.above-hovered { + color: #7c7c7c; } .difficulty-star.inactive { @@ -14,6 +22,10 @@ color: #7c7c7c; } +.difficulty-star.inactive.hovered { + color: rgb(151, 91, 1); +} + .add-ingredient-button.active { color: rgb(0, 194, 0); } diff --git a/src/css/common.css b/src/css/common.css index 9f8e993..a13f463 100644 --- a/src/css/common.css +++ b/src/css/common.css @@ -119,4 +119,52 @@ .user-nav-image { border-top-right-radius: 25px; border-bottom-right-radius: 25px; +} + +#search-bar { + border-right: none; + transition: none; +} + +#search-bar-clear { + background-color: white; + border-left: none; + cursor: text; + transition: none; +} + +#search-bar-container:focus-within #search-bar { + border-color: #80bdff; +} + +#search-bar-container:focus-within #search-bar-clear { + border-color: #80bdff; +} + +.rating-star.active { + color: #e0b700; +} + +.rating-star.active:hover { + color: rgb(151, 124, 43); +} + +.rating-star.active.hovered { + color: rgb(151, 124, 43); +} + +.rating-star.active.above-hovered { + color: #7c7c7c; +} + +.rating-star.inactive { + color: #a7a7a7; +} + +.rating-star.inactive:hover { + color: #7c7c7c; +} + +.rating-star.inactive.hovered { + color: rgb(151, 124, 43); } \ No newline at end of file diff --git a/src/css/slider.css b/src/css/slider.css new file mode 100644 index 0000000..edc0470 --- /dev/null +++ b/src/css/slider.css @@ -0,0 +1,37 @@ +input[type=range]::-webkit-slider-runnable-track { + background: #ddd; +} + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + background: rgb(255, 74, 42); +} + +input[type=range]:focus { + outline: none; +} + +input[type=range]:focus::-webkit-slider-runnable-track { + background: #ffafaf; +} + +input[type=range]::-moz-range-track { + background: #ffafaf; +} + +input[type=range]::-moz-range-thumb { + background: rgb(255, 74, 42); +} + +input[type=range]:focus::-moz-range-thumb { + background: rgb(155, 26, 4); +} + +/*hide the outline behind the border*/ +input[type=range]:-moz-focusring { + outline: none; +} + +input[type=range]:focus::-moz-range-track { + background: #ffc0c0; +} \ No newline at end of file From 47f4b9298ab9bc8b35aa818e6595e67fc5df5e6e Mon Sep 17 00:00:00 2001 From: Antonio Tigri Date: Thu, 7 Dec 2023 16:54:17 +0100 Subject: [PATCH 22/59] Bugfix regarding ascending/descending visibility --- src/components/RecipeBrowser.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/RecipeBrowser.jsx b/src/components/RecipeBrowser.jsx index 15d22d9..7a5103b 100644 --- a/src/components/RecipeBrowser.jsx +++ b/src/components/RecipeBrowser.jsx @@ -118,8 +118,7 @@ function RecipeBrowser({ onSearch }) { ([key, value]) => value !== defaultFilters[key] || (key === "sortAscending" && - JSON.stringify(filtersOffCanvas.values) !== - JSON.stringify(defaultFilters)) + filtersOffCanvas.values.sortBy !== "none") ) .map(([key, value]) => ( From ec2e3b2aa94bda2f3d955c992a6ef4d3cda5e8ee Mon Sep 17 00:00:00 2001 From: Antonio Tigri Date: Thu, 7 Dec 2023 17:27:01 +0100 Subject: [PATCH 23/59] Pleasing PO --- src/components/RecipeBrowser.jsx | 33 +++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/components/RecipeBrowser.jsx b/src/components/RecipeBrowser.jsx index 7a5103b..98c3992 100644 --- a/src/components/RecipeBrowser.jsx +++ b/src/components/RecipeBrowser.jsx @@ -10,9 +10,28 @@ import { import { Funnel, FunnelFill, X, SortUp, SortDown } from "react-bootstrap-icons"; import RecipeFilters from "./RecipeFilters"; +import sort_options from "../assets/jsonData/sort_options.json"; import "../css/common.css"; function RecipeBrowser({ onSearch }) { + const keyParsing = { + sortBy: "Sort by:", + sortAscending: "", + maxDifficulty: "Difficulty:", + maxTime: "Time:", + minRating: "Rating:", + maxCalories: "Calories", + }; + + const valueParsing = { + sortBy: "", + sortAscending: "", + maxDifficulty: " stars or less", + maxTime: " min or less", + minRating: " stars or more", + maxCalories: " cal or less", + }; + const defaultFilters = { sortBy: "none", sortAscending: false, @@ -122,12 +141,16 @@ function RecipeBrowser({ onSearch }) { ) .map(([key, value]) => ( - {key}:{" "} - {value === true - ? "Yes" + {keyParsing[key]}{" "} + {key === "sortBy" + ? sort_options.fields.filter( + (option) => option.name === value + )[0].displayName + : value === true + ? "Ascending" : value === false - ? "No" - : value.toString()} + ? "Descending" + : value.toString().concat(valueParsing[key])} {key === "sortAscending" ? (
    Date: Thu, 7 Dec 2023 17:46:03 +0100 Subject: [PATCH 24/59] Fixed local storage bug --- src/components/RecipeBrowser.jsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/RecipeBrowser.jsx b/src/components/RecipeBrowser.jsx index 98c3992..9b9a6ca 100644 --- a/src/components/RecipeBrowser.jsx +++ b/src/components/RecipeBrowser.jsx @@ -45,16 +45,7 @@ function RecipeBrowser({ onSearch }) { const [filtersOffCanvas, setFiltersOffCanvas] = useState({ title: "Filters", show: false, - values: JSON.parse( - localStorage.getItem("filters") || { - sortBy: "none", - sortAscending: false, - maxDifficulty: 5, - maxTime: 180, - minRating: 0, - maxCalories: 5000, - } - ), + values: JSON.parse(localStorage.getItem("filters")) || defaultFilters, }); const handleCloseOffcanvas = ( From 2beea1fd51d352ef2b13ac13d3bdb7a5d4cf08e5 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:42:47 +0100 Subject: [PATCH 25/59] feat: New style for reviews without images --- src/components/Reviews.jsx | 61 +++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 7619ba9..40d3b1d 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -83,6 +83,7 @@ function Reviews(props) { {reviews && reviews.length > 0 ? ( reviews.map((review, index) => (
  • + {review.image ? {review.username}:{" "} @@ -152,7 +153,65 @@ function Reviews(props) { - + + : + + + {review.username}:{" "} + + {review.comment} + + + +
    +
    + {" "} + {Array(review.rating || 0) + .fill() + .map((_, index) => ( + + + + ))} +
    +
    + + +
    + + {currentUser === review.username && ( + <> + handleOpenModalReview(review, 'Edit')} + /> + handleOpenModalReview(review, 'Trash')} + /> + + )} +
    + +
    + +
    }
  • )) ) : ( owner !== currentUser ? From 9c4866d33a7616391d87a6ebbf0887cc1652530a Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:13:01 +0100 Subject: [PATCH 26/59] feat: same design post review as page --- src/components/PostReview.jsx | 16 ++++++++-------- src/components/Reviews.jsx | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index bd7d92a..ef08265 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -103,12 +103,12 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { return (<> - - Post a review + + Post a review - +
    - + Review { /> - + Rating -
    {renderStars(difficulty)}
    +
    {renderStars(difficulty)}
    - + Select Image
    - + diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 40d3b1d..7bdd160 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -80,7 +80,7 @@ function Reviews(props) {
      - {reviews && reviews.length > 0 ? ( + {reviews && reviews.length > 0 ? ( reviews.map((review, index) => (
    • {review.image ? @@ -229,7 +229,6 @@ function Reviews(props) { />
    ) :

    There are currently no reviews

    - {/*

    Be the first one to post a review and share your thoughts!

    */}

    You are the owner of the recipe you can't do reviews

    )} @@ -240,7 +239,7 @@ function Reviews(props) { show={showModalImage} onHide={handleCloseModalImage} recipeImage={selectedImage} - recipeName={selectedImage ? "Image" : null} // Puedes cambiar esto según tus necesidades + recipeName={selectedImage ? "Image" : null} /> {selectedReview && ( Date: Tue, 12 Dec 2023 14:40:24 +0100 Subject: [PATCH 27/59] feat: control no pass x characters --- src/components/PostReview.jsx | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index ef08265..037a7f8 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -17,6 +17,8 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { const { token } = useAuth(); const [error, setError] = useState(null); const [showErrorModal, setShowErrorModal] = useState(false); + const [characterCount, setCharacterCount] = useState(0); + useEffect(() => { @@ -39,6 +41,12 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { setImage(selectedImage); }; + const handleReviewChange = (e) => { + const inputReview = e.target.value; + setReview(inputReview); + setCharacterCount(inputReview.length); + }; + const handlePostReview = async () => { const reviewData = { username: username, @@ -103,10 +111,10 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { return (<> - - Post a review + + Post a review - +
    Review @@ -115,8 +123,17 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { rows={3} placeholder="Write your review here" value={review} - onChange={(e) => setReview(e.target.value)} + onChange={handleReviewChange} + style={{ borderColor: characterCount > 80 ? 'red' : null }} /> + {characterCount > 80 && ( +
    You exceeded 80 characters.
    + )} +
    + + + Rating +
    {renderStars(difficulty)}
    @@ -134,7 +151,12 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { - @@ -149,7 +171,7 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { - From 7a4f4d1c71474ba38771e618a30bf611d0fb9282 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:43:50 +0100 Subject: [PATCH 28/59] feat: num of characters in real time --- src/components/PostReview.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index 037a7f8..afba9a0 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -129,13 +129,8 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { {characterCount > 80 && (
    You exceeded 80 characters.
    )} +
    Num characters: {characterCount}
    - - - Rating -
    {renderStars(difficulty)}
    -
    - Rating
    {renderStars(difficulty)}
    From 238257956fd845e90a2d1e0a50c3bb3c80251d8f Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:31:07 +0100 Subject: [PATCH 29/59] feat: New design to Modify Review and Delete Review --- src/components/ModifyReview.jsx | 45 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx index db5ba48..9ebbd7a 100644 --- a/src/components/ModifyReview.jsx +++ b/src/components/ModifyReview.jsx @@ -10,7 +10,8 @@ import { const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, reviewInfo }) => { const [newComment, setNewComment] = useState(reviewInfo.comment); const [newRating, setNewRating] = useState(reviewInfo.rating); - const [newImage, setNewImage] = useState(reviewInfo.file); // Para almacenar la nueva imagen + const [newImage, setNewImage] = useState(reviewInfo.file); + const [characterCount, setCharacterCount] = useState(0); const { token } = useAuth(); const handleUpdateReview = async () => { @@ -96,47 +97,51 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, return stars; }; + const handleReviewChange = (e) => { + const inputReview = e.target.value; + setNewComment(inputReview) + setCharacterCount(inputReview.length); + }; + const handleConfirmDelete = () => { handleDeleteReview(); }; return ( - - {funct === 'Edit' ? 'Modificar' : 'Eliminar'} revisión + + {funct === 'Edit' ? 'Modify' : 'Delete'} review - + {funct === 'Edit' ? ( - - New Comment + + New Review setNewComment(e.target.value)} + onChange={handleReviewChange} + style={{ borderColor: characterCount > 80 ? 'red' : null }} /> + {characterCount > 80 && ( +
    You exceeded 80 characters.
    + )} +
    Num characters: {characterCount}
    - + New Rating -
    {renderStars(newRating)}
    -
    - - New Image - setNewImage(e.target.files[0])} - /> +
    {renderStars(newRating)}
    ) : ( -

    Are you sure delete review?

    +

    Are you sure delete review?

    )}
    - + {funct === 'Edit' ? ( - ) : ( From f315c52e71fe1a18eb852c68484bae2c4a2cdbc7 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:59:57 +0100 Subject: [PATCH 30/59] fix: Limit characters are 120 --- src/components/ModifyReview.jsx | 8 ++++---- src/components/PostReview.jsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx index 9ebbd7a..7eed118 100644 --- a/src/components/ModifyReview.jsx +++ b/src/components/ModifyReview.jsx @@ -123,10 +123,10 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, placeholder="Write your new review here" value={newComment} onChange={handleReviewChange} - style={{ borderColor: characterCount > 80 ? 'red' : null }} + style={{ borderColor: characterCount > 120 ? 'red' : null }} /> - {characterCount > 80 && ( -
    You exceeded 80 characters.
    + {characterCount > 120 && ( +
    You exceeded 120 characters.
    )}
    Num characters: {characterCount}
    @@ -141,7 +141,7 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct,
    {funct === 'Edit' ? ( - ) : ( diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index afba9a0..11a62bb 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -124,10 +124,10 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { placeholder="Write your review here" value={review} onChange={handleReviewChange} - style={{ borderColor: characterCount > 80 ? 'red' : null }} + style={{ borderColor: characterCount > 120 ? 'red' : null }} /> - {characterCount > 80 && ( -
    You exceeded 80 characters.
    + {characterCount > 120 && ( +
    You exceeded 120 characters.
    )}
    Num characters: {characterCount}
    @@ -150,7 +150,7 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { className='bg-danger fw-bold border-secondary text-white' variant="primary" onClick={handlePostReview} - disabled={characterCount > 80} + disabled={characterCount > 120} > Post Review From aec44483c644e2e3dfad944fec948b0feb3c4fd0 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:37:14 +0100 Subject: [PATCH 31/59] feat: change style and design of different places --- src/components/ModifyReview.jsx | 8 ++++---- src/components/PostReview.jsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ModifyReview.jsx b/src/components/ModifyReview.jsx index 9ebbd7a..8f70e33 100644 --- a/src/components/ModifyReview.jsx +++ b/src/components/ModifyReview.jsx @@ -109,10 +109,10 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, return ( - + {funct === 'Edit' ? 'Modify' : 'Delete'} review - + {funct === 'Edit' ? (
    @@ -132,14 +132,14 @@ const ModifyReview = ({ show, reviewId, recipeId, onHide, reloadReviews, funct, New Rating -
    {renderStars(newRating)}
    +
    {renderStars(newRating)}
    ) : (

    Are you sure delete review?

    )}
    - + {funct === 'Edit' ? ( From ff9f90c3e4b0b80f016f5bf1115cbaf08e53e15c Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:34:50 +0100 Subject: [PATCH 32/59] feat: Implement all about similar recipes DONE --- src/components/RecipeDetail.jsx | 41 +++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index f99b268..96d8974 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -2,8 +2,9 @@ import React, { useState, useEffect } from "react"; import { useAuth } from "./AuthContext"; import "../css/common.css"; import "../css/Transitions.css"; +import RecipeCard from "./RecipeCard"; import chefIcon from "../assets/icons/chef.png" -import { useParams, useNavigate } from "react-router-dom"; +import { useParams, useNavigate, Link } from "react-router-dom"; import defaultProfile from "../assets/defaultProfile.png"; import { CSSTransition } from "react-transition-group"; import gyozas from '../assets/gyozas.jpg'; @@ -40,8 +41,7 @@ function RecipeDetail() { const [userId, setUserId] = useState(''); const [userName, setUserName] = useState(''); const [userImage, setUserImage] = useState(''); - const [showModal, setShowModal] = useState(false); - const [user, setUser] = useState({}); + const [similarRecipes, setSimilarRecipes] = useState(''); const { id } = useParams(); const [recipe, setRecipe] = useState({ images: [] }); const [showReviews, setShowReviews] = useState(false); @@ -73,6 +73,7 @@ function RecipeDetail() { useEffect(() => { getRecipe(); + getSimilarRecipes(); if (isLogged()) { getIsLiked(username); } @@ -89,6 +90,17 @@ function RecipeDetail() { .catch((error) => console.error("Error al obtener receta:", error)); }; + const getSimilarRecipes = () => { + fetch(process.env.REACT_APP_API_URL + `/recipe/similar/${id}`) + .then((response) => response.json()) + .then((data) => { + setSimilarRecipes(data) + console.log(">> console.error("Error al obtener recetas similares:", error)); + }; + + const fetchUserData = async (userId) => { try { const response = await fetch(process.env.REACT_APP_API_URL + '/user/' + userId, { @@ -418,10 +430,31 @@ function RecipeDetail() {
    + + + + +

    Similar Recipes

    + + {similarRecipes.length > 0 ? similarRecipes?.map((recipe) => ( + + + + + + )): null} +
    +
    +
    - {/* Offcanvas para mostrar los comentarios */} + + + setShowReviews(false)} From 26d7a1147541df7e08b61c37fbe5c1b94ffd467b Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Wed, 13 Dec 2023 19:10:00 +0100 Subject: [PATCH 33/59] US15 with the AC done --- .env | 2 +- src/components/PrivacySettings.jsx | 116 +++++++++++++++ src/components/UserProfile.jsx | 226 ++++++++++++++++------------- 3 files changed, 243 insertions(+), 101 deletions(-) create mode 100644 src/components/PrivacySettings.jsx diff --git a/.env b/.env index 3ed23bb..2737d5b 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -REACT_APP_API_URL = 'http://127.0.0.1:8000' +REACT_APP_API_URL = 'https://kasula-develop-5q5vehm3ja-ew.a.run.app' # LA VARIABLE ESTÀ PENSADA PERQUÈ NO PORTI BARRA AL FINAL diff --git a/src/components/PrivacySettings.jsx b/src/components/PrivacySettings.jsx new file mode 100644 index 0000000..cbfb2a3 --- /dev/null +++ b/src/components/PrivacySettings.jsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from "react"; +import { Tooltip, OverlayTrigger, Container, Row, Col, Dropdown, Form, Button } from "react-bootstrap"; +import { InfoCircleFill } from "react-bootstrap-icons"; + +const PrivacySettings = ({ + onClose, + handleSaveProfile, + handleVisibilityChange, + imPrivate, + usernameValidationMessage, + emailValidationMessage, + userNameAux, + userMailAux, + userBioAux, + usernameValid, + emailValid, + usernameValidated, + emailValidated, + onUsernameChange, + onEmailChange, + setUserBioAux +}) => { + + const renderTooltip = (message) => ( + {message} + ); + + useEffect(() => { + console.error(usernameValid==true) + }, [usernameValid]); + + return ( + + + +
    + {/* Sección de edición de perfil */} + + Username + onUsernameChange(e)} + isInvalid={usernameValidated && !usernameValid} + /> + + {usernameValidationMessage} + + + + Email + onEmailChange(e)} + isInvalid={emailValidated && !emailValid} + /> + + {emailValidationMessage} + + + + Biography + setUserBioAux(e.target.value)} + /> + + {/* Botón para guardar cambios */} + +
    + +
    + + +
    Profile Visibility:
    + + + + + {imPrivate ? "Private" : "Public"} + + + + handleVisibilityChange(false)}> + + Public + + + + handleVisibilityChange(true)}> + + Private + + + + + +
    + +
    +
    + ); +}; + +export default PrivacySettings; diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index ecb1c5a..3a6bc42 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -12,12 +12,13 @@ import chefIcon from "../assets/icons/chef.png"; import logo from "../assets/logo.png"; // Bootstrap -import { StarFill, Pencil, ExclamationTriangleFill, X} from "react-bootstrap-icons"; +import { StarFill, Pencil, ExclamationTriangleFill, X, GearFill} from "react-bootstrap-icons"; import { Container, Row, Col, Card, Button, Form, Image, Modal, Dropdown, ListGroup } from "react-bootstrap"; // CSS import "../css/common.css"; import "../css/UserProfile.css"; +import PrivacySettings from "./PrivacySettings"; const UserProfile = () => { const { token, logout, isLogged } = useAuth(); @@ -49,13 +50,15 @@ const UserProfile = () => { const [isFollowing, setIsFollowing] = useState(null); const [followerDetails, setFollowerDetails] = useState([]); const [followingDetails, setFollowingDetails] = useState([]); + const [imPrivate, setImPrivate] = useState(true); + const [userIsPrivate, setUserIsPrivate] = useState(true); const [adminMode, setadminMode] = useState(false); const [editMode, setEditMode] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const [showDropdown, setShowDropdown] = useState(false); - const [showDropdown2, setShowDropdown2] = useState(false); + const [showPrivacySettings, setShowPrivacySettings] = useState(false); const [showRemoveQuestion, setRemoveQuestion] = useState(false); const [showRemoveRecipeModal, setShowRemoveRecipeModal] = useState(false); const [selectedRecipeId, setSelectedRecipeId] = useState(null); @@ -107,6 +110,7 @@ const UserProfile = () => { const data = await response.json(); setMyUserId(data._id) setMyUserName(data.username) + setImPrivate(data.is_private) } catch (error) { console.error('Error fetching user data:', error); } @@ -139,6 +143,8 @@ const UserProfile = () => { setUserFollowing(data.following || []); setProfilePicture(data.profile_picture || ''); + + setUserIsPrivate(data.is_private); } catch (error) { console.error('Error fetching user data:', error); } @@ -195,6 +201,14 @@ const UserProfile = () => { setShowRemoveRecipeModal(true); }; + const handleOpenPrivacySettings = () => { + setShowPrivacySettings(true); + }; + + const handleClosePrivacySettings = () => { + setEditMode(false); + }; + const handleSaveProfile = async () => { if (!userNameAux) { @@ -252,6 +266,38 @@ const UserProfile = () => { }; +const handleVisibilityChange = async (newVisibility) => { + console.error(newVisibility) + setImPrivate(newVisibility); + const formData = new FormData(); + const userProfile = { + is_private: newVisibility, + }; + + // Convert the user profile object to a JSON string and append to FormData + formData.append('user', JSON.stringify(userProfile)); + + try { + const response = await fetch(process.env.REACT_APP_API_URL + `/user/${userId}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}` + }, + body: formData, + }); + + if (!response.ok) { + throw new Error('Failed to update user data'); + } + + const updatedData = await response.json(); + } catch (error) { + console.error('Error updating user data:', error); + setConfirmationMessage("Failed to update profile"); + } +}; + + const handleImageUpload = async () => { const userData = { }; @@ -353,6 +399,8 @@ const UserProfile = () => { setUserNameAux(value); setUsernameValid(isUsernameValid(value)); setUsernameValidated(true); + console.error(usernameValid==true) + console.error("hola") }; const onEmailChange = (e) => { @@ -573,14 +621,16 @@ const UserProfile = () => { -

    @{userName}

    + +

    @{userName}

    +
    {userBio}
    {/* Followers Section */} -
    +

    Followers

    {userFollowers.length}

    @@ -589,7 +639,7 @@ const UserProfile = () => {
    {/* Following Section */} -
    +

    Following

    {userFollowing.length}

    @@ -622,48 +672,54 @@ const UserProfile = () => { - {recipes && recipes.length > 0 ? ( + {!userIsPrivate || adminMode ? + ( + recipes && recipes.length > 0 ? ( recipes.map((recipe) => ( - - - - - - - - - {recipe.name} -
    - - {Array(recipe.difficulty || 0).fill().map((_, index) => ( - - ))} -
    - {adminMode && ( -
    - - -
    - )} -
    -
    - -
    - + + + + + + + + {recipe.name} +
    + + {Array(recipe.difficulty || 0).fill().map((_, index) => ( + + ))} +
    + {adminMode && ( +
    + + +
    + )} +
    +
    +
    + )) - ) : ( + ) : (
    There are currently no Recipes
    - )} + ) + ) : ( +
    The user has a private profile
    + ) + } + +
    {} @@ -712,62 +768,6 @@ const UserProfile = () => { - - - - Edit Profile - - -
    - - Name - onUsernameChange(e)} - isInvalid={usernameValidated && !usernameValid} - /> - - {usernameValidationMessage} - - - - Email - onEmailChange(e)} - isInvalid={emailValidated && !emailValid} - /> - - {emailValidationMessage} - - - - Biography - setUserBioAux(e.target.value)} - /> - -
    -
    - - - - -
    - {operationSuccess ? 'Success' : 'Error'} @@ -877,6 +877,32 @@ const UserProfile = () => { + + + Profile Settings + + + + + +
    ); From b78238837d311959bed5652e0beb5007887d29e8 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Wed, 13 Dec 2023 19:49:49 +0100 Subject: [PATCH 34/59] New changes requested by PO --- src/components/PrivacySettings.jsx | 2 +- src/components/UserProfile.jsx | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/PrivacySettings.jsx b/src/components/PrivacySettings.jsx index cbfb2a3..ad6a981 100644 --- a/src/components/PrivacySettings.jsx +++ b/src/components/PrivacySettings.jsx @@ -69,7 +69,7 @@ const PrivacySettings = ({ /> {/* Botón para guardar cambios */} - diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index 3a6bc42..b952f7a 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -619,10 +619,19 @@ const handleVisibilityChange = async (newVisibility) => { )}
    - +

    @{userName}

    + + + + + {adminMode && ( + + )}
    {userBio}
    @@ -649,13 +658,6 @@ const handleVisibilityChange = async (newVisibility) => { - - {adminMode && ( - - )} - @@ -663,6 +665,7 @@ const handleVisibilityChange = async (newVisibility) => { @@ -715,7 +718,7 @@ const handleVisibilityChange = async (newVisibility) => {
    There are currently no Recipes
    ) ) : ( -
    The user has a private profile
    +
    The user has a private profile. You cannot see his recipes.
    ) } From 06281cc5e085fc1611ac5e068d63e91ffa29bc28 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Wed, 13 Dec 2023 19:52:50 +0100 Subject: [PATCH 35/59] Minor changes --- src/components/PrivacySettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PrivacySettings.jsx b/src/components/PrivacySettings.jsx index ad6a981..a062778 100644 --- a/src/components/PrivacySettings.jsx +++ b/src/components/PrivacySettings.jsx @@ -69,7 +69,7 @@ const PrivacySettings = ({ /> {/* Botón para guardar cambios */} - From fb9f25b3c660e4a9898065773a42cc34bc3e5fdf Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Wed, 13 Dec 2023 23:46:15 +0100 Subject: [PATCH 36/59] Some merge corrections --- src/components/AuthContext.jsx | 1 + src/components/KasulaNavbar.jsx | 7 + src/components/RecipeBrowser.jsx | 222 ++++++++++++++++++++++++++ src/components/RecipeDetail.jsx | 19 ++- src/components/RecipeFilters.jsx | 147 +++++++++++++++++ src/components/RecipeList.jsx | 31 +++- src/components/Root.js | 1 + src/components/StarSelector.jsx | 39 +++++ src/components/UserFeed.jsx | 260 +++++++++++++++++-------------- src/css/PostRecipe.css | 14 +- src/css/common.css | 48 ++++++ src/css/slider.css | 37 +++++ 12 files changed, 692 insertions(+), 134 deletions(-) create mode 100644 src/components/RecipeBrowser.jsx create mode 100644 src/components/RecipeFilters.jsx create mode 100644 src/components/StarSelector.jsx create mode 100644 src/css/slider.css diff --git a/src/components/AuthContext.jsx b/src/components/AuthContext.jsx index 5ca9be0..6fdfc6e 100644 --- a/src/components/AuthContext.jsx +++ b/src/components/AuthContext.jsx @@ -8,6 +8,7 @@ export const AuthProvider = ({ children }) => { const logout = () => { setToken(null); localStorage.removeItem("token"); + localStorage.removeItem("currentUser"); }; const isLogged = () => { diff --git a/src/components/KasulaNavbar.jsx b/src/components/KasulaNavbar.jsx index b514b6c..de6fd8b 100644 --- a/src/components/KasulaNavbar.jsx +++ b/src/components/KasulaNavbar.jsx @@ -136,6 +136,13 @@ function KasulaNavbar() { + Profile + + + { handleOpenModal(); }} diff --git a/src/components/RecipeBrowser.jsx b/src/components/RecipeBrowser.jsx new file mode 100644 index 0000000..9b9a6ca --- /dev/null +++ b/src/components/RecipeBrowser.jsx @@ -0,0 +1,222 @@ +import { useState } from "react"; +import { + Container, + Stack, + InputGroup, + Form, + Offcanvas, + Badge, +} from "react-bootstrap"; +import { Funnel, FunnelFill, X, SortUp, SortDown } from "react-bootstrap-icons"; + +import RecipeFilters from "./RecipeFilters"; +import sort_options from "../assets/jsonData/sort_options.json"; +import "../css/common.css"; + +function RecipeBrowser({ onSearch }) { + const keyParsing = { + sortBy: "Sort by:", + sortAscending: "", + maxDifficulty: "Difficulty:", + maxTime: "Time:", + minRating: "Rating:", + maxCalories: "Calories", + }; + + const valueParsing = { + sortBy: "", + sortAscending: "", + maxDifficulty: " stars or less", + maxTime: " min or less", + minRating: " stars or more", + maxCalories: " cal or less", + }; + + const defaultFilters = { + sortBy: "none", + sortAscending: false, + maxDifficulty: 5, + maxTime: 180, + minRating: 0, + maxCalories: 5000, + }; + + const [recipeName, setRecipeName] = useState(""); + const [filtersOffCanvas, setFiltersOffCanvas] = useState({ + title: "Filters", + show: false, + values: JSON.parse(localStorage.getItem("filters")) || defaultFilters, + }); + + const handleCloseOffcanvas = ( + setOffCanvasState, + offCanvasState, + applyFilters, + filterValues + ) => { + setOffCanvasState({ + ...offCanvasState, + show: false, + values: filterValues || offCanvasState.values, + }); + if (applyFilters) { + onSearch(filterValues || offCanvasState.values, recipeName); + } + }; + + return ( + + + { + setFiltersOffCanvas({ ...filtersOffCanvas, show: true }); + }} + > + + + + { + setRecipeName(e.target.value); + onSearch(filtersOffCanvas.values, e.target.value); + }} + /> + + {recipeName.length > 0 && ( + { + setRecipeName(""); + onSearch(filtersOffCanvas.values, ""); + }} + /> + )} + + + + + {JSON.stringify(filtersOffCanvas.values) !== + JSON.stringify(defaultFilters) && ( +
    { + setFiltersOffCanvas({ + ...filtersOffCanvas, + values: defaultFilters, + }); + localStorage.setItem("filters", JSON.stringify(defaultFilters)); + onSearch(defaultFilters, recipeName); + }} + > + + Clear filters + +
    + )} + + {Object.entries(filtersOffCanvas.values) + .filter( + ([key, value]) => + value !== defaultFilters[key] || + (key === "sortAscending" && + filtersOffCanvas.values.sortBy !== "none") + ) + .map(([key, value]) => ( + + {keyParsing[key]}{" "} + {key === "sortBy" + ? sort_options.fields.filter( + (option) => option.name === value + )[0].displayName + : value === true + ? "Ascending" + : value === false + ? "Descending" + : value.toString().concat(valueParsing[key])} + {key === "sortAscending" ? ( +
    { + const newValues = { ...filtersOffCanvas.values }; + newValues[key] = !newValues[key]; + setFiltersOffCanvas({ + ...filtersOffCanvas, + values: newValues, + }); + localStorage.setItem( + "filters", + JSON.stringify(newValues) + ); + onSearch(newValues, recipeName); + }} + > + {value ? ( + + ) : ( + + )} +
    + ) : ( + { + const newValues = { ...filtersOffCanvas.values }; + newValues[key] = defaultFilters[key]; + setFiltersOffCanvas({ + ...filtersOffCanvas, + values: newValues, + }); + localStorage.setItem( + "filters", + JSON.stringify(newValues) + ); + onSearch(newValues, recipeName); + }} + /> + )} +
    + ))} +
    +
    + + handleCloseOffcanvas(setFiltersOffCanvas, filtersOffCanvas, false) + } + > + + + {filtersOffCanvas.title} + + + + { + localStorage.setItem("filters", JSON.stringify(filters)); + handleCloseOffcanvas( + setFiltersOffCanvas, + filtersOffCanvas, + true, + filters + ); + }} + inValues={filtersOffCanvas.values} + /> + + +
    + ); +} + +export default RecipeBrowser; diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index 6cd9f0d..ebd686f 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -381,13 +381,18 @@ function RecipeDetail() {
    handleNavigate(null, recipe.user_id)}> - - - + + +

    {userName}

    diff --git a/src/components/RecipeFilters.jsx b/src/components/RecipeFilters.jsx new file mode 100644 index 0000000..902627f --- /dev/null +++ b/src/components/RecipeFilters.jsx @@ -0,0 +1,147 @@ +import { useState } from "react"; +import { Container, Button, Form } from "react-bootstrap"; +import { SortUp, SortDown, X, Funnel } from "react-bootstrap-icons"; +import StarSelector from "./StarSelector"; +import sortOptions from "../assets/jsonData/sort_options.json"; + +function RecipeFilters({ onClose, inValues }) { + const defaultFilters = { + sortBy: "none", + sortAscending: false, + maxDifficulty: 5, + maxTime: 180, + minRating: 0, + maxCalories: 5000, + }; + + const [filters, setFilters] = useState(inValues || defaultFilters); + + return ( + + {JSON.stringify(filters) !== JSON.stringify(defaultFilters) && ( +
    setFilters(defaultFilters)} + > + + Clear filters + +
    + )} +
    + + Sort By +
    + + {sortOptions.fields.map((option) => ( + + ))} + + { + setFilters({ + ...filters, + sortAscending: !filters.sortAscending, + }); + }} + > + {filters.sortAscending ? : } + +
    +
    + + Difficulty +
    + + setFilters({ ...filters, maxDifficulty: value }) + } + /> +
    +
    + + Rating +
    + setFilters({ ...filters, minRating: value })} + /> +
    +
    + + Time +
    + + setFilters({ ...filters, maxTime: e.target.value }) + } + /> +
    + {filters.maxTime} min +
    +
    +
    +
    + 0 min + 180 min +
    + + Calories +
    + + setFilters({ ...filters, maxCalories: e.target.value }) + } + /> +
    + {filters.maxCalories} cal +
    +
    +
    +
    + 0 cal + 5000 cal +
    +
    + +
    +
    +
    + ); +} + +export default RecipeFilters; diff --git a/src/components/RecipeList.jsx b/src/components/RecipeList.jsx index c94d110..87c7867 100644 --- a/src/components/RecipeList.jsx +++ b/src/components/RecipeList.jsx @@ -1,5 +1,6 @@ //React import { CSSTransition } from "react-transition-group"; +import { useRef, useEffect } from "react"; //Bootstrap import { Link } from "react-router-dom"; @@ -11,21 +12,39 @@ import RecipeCard from "./RecipeCard"; //CSS import "../css/Transitions.css"; +import { wait } from "@testing-library/user-event/dist/utils"; -function recipeList({ recipes, canDelete, onDeleteRecipe, id, token }) { +function RecipeList({ recipes, canDelete, onDeleteRecipe, onRequestLoadMore, id, token, finished }) { + const myRef = useRef(); + useEffect(() => { + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !finished) { + onRequestLoadMore(); + observer.unobserve(entries[0].target); + } + }); + if (myRef.current) { + observer.observe(myRef.current); + } + return () => { + if (myRef.current) { + observer.unobserve(myRef.current); + } + }; + }, [myRef, recipes]); return ( {recipes && recipes.length > 0 ? ( - recipes.map((recipe) => ( - + recipes.map((recipe, index) => ( + -
    +
    - There are currently no recipes + {canDelete ? "There are currently no recipes" : "There are no recipes matching the search or filters"}
    )} @@ -75,4 +94,4 @@ function recipeList({ recipes, canDelete, onDeleteRecipe, id, token }) { ); } -export default recipeList; +export default RecipeList; diff --git a/src/components/Root.js b/src/components/Root.js index 38a5423..1700475 100644 --- a/src/components/Root.js +++ b/src/components/Root.js @@ -21,6 +21,7 @@ import CollectionView from "./CollectionView"; import "../css/Transitions.css"; import "../css/Root.css"; import "../css/common.css"; +import "../css/slider.css"; function Root() { return ( diff --git a/src/components/StarSelector.jsx b/src/components/StarSelector.jsx new file mode 100644 index 0000000..87ac0e2 --- /dev/null +++ b/src/components/StarSelector.jsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { Star, StarFill } from "react-bootstrap-icons"; + +function StarSelector({ onSelect, maxValue, value, starSize, type }) { + const [hoveredStar, setHoveredStar] = useState({ + value: 0, + active: false, + }); + let stars = []; + for (let i = 1; i <= maxValue; i++) { + const isActive = i <= value; + const isHovered = i <= hoveredStar.value && hoveredStar.active; + const aboveHovered = i > hoveredStar.value && hoveredStar.active; + stars.push( + { + onSelect((isActive && i === value ? i - 1 : i)); + }} + onMouseEnter={() => setHoveredStar({ value: i, active: true })} + onMouseLeave={() => setHoveredStar({ value: 0, active: false })} + > + {isActive || isHovered ? : } + + ); + } + return stars; +} + +export default StarSelector; diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index 45afc4f..0452b73 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -1,159 +1,179 @@ -import { useAuth } from "./AuthContext"; -import { useNavigate, useParams } from "react-router-dom"; import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "./AuthContext"; import RecipeList from "./RecipeList"; -import { Container, Spinner, ButtonGroup, Button, Row, Modal } from "react-bootstrap"; +import RecipeBrowser from "./RecipeBrowser"; +import { Container, Spinner, ButtonGroup, Button, Modal, Row, Col } from "react-bootstrap"; function UserFeed() { - const [myUserName, setMyUserName] = useState(localStorage.getItem("currentUser")); + const {isLogged } = useAuth(); const navigate = useNavigate(); - const { token, logout, isLogged } = useAuth(); - const [myUserFollowing, setMyUserFollowing] = useState([]); - const [recipesForYou, setRecipesForYou] = useState([]); - const [recipesFollowing, setRecipesFollowing] = useState([]); - const [logged, setLogged] = useState(false); + const numRecipes = isLogged() ? 24 : 9; + const [recipes, setRecipes] = useState([]); const [loading, setLoading] = useState(true); - const [feedType, setFeedType] = useState("forYou"); + const [feedType, setFeedType] = useState(localStorage.getItem('feedType') || 'foryou'); const [showLoginRedirectModal, setShowLoginRedirectModal] = useState(false); - useEffect(() => { - getUserFollowing(); - }, [feedType]); + const loggedOutFilters = { + sortBy: "average_rating", + sortAscending: false, + }; + const [filters, setFilters] = useState(isLogged() ? (JSON.parse(localStorage.getItem("filters"))) : loggedOutFilters); + const [recipeName, setRecipeName] = useState(null); + const [page, setPage] = useState(0); + const [finished, setFinished] = useState(isLogged() ? false : true); useEffect(() => { - setLogged(isLogged); - }, [token]); + if (!isLogged()) { + setFinished(true); + setPage(0); + setFilters(loggedOutFilters); + setRecipeName(null); + getRecipes(loggedOutFilters, null, 0, 9, true, feedType); + } + }, [isLogged]); useEffect(() => { - getRecipes(); - }, [myUserFollowing]); + getRecipes(filters, recipeName, page, numRecipes, true, feedType); + }, []); - const getUserFollowing = async (userId) => { + const getRecipes = async (filters, recipeName, page, numRecipes, reset, feedType) => { + setLoading(true); + let url = buildRequestUrl(filters, recipeName, page, numRecipes, feedType); try { - const response = await fetch(process.env.REACT_APP_API_URL + '/user/' + myUserName, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - }); + const response = await fetch(url); if (!response.ok) { - throw new Error('Failed to fetch user data'); + if (response.status === 400) { + setPage(page - 1); + setFinished(true); + } + throw new Error('Failed to fetch recipes'); } const data = await response.json(); - setMyUserFollowing(data.following || []); + if (reset) { + setRecipes(data) + } else { + setRecipes(recipes.concat(data)); + } } catch (error) { - console.error('Error fetching user data:', error); + console.error('Error fetching recipes:', error); + } finally { + setLoading(false); } }; - const getRecipesFollowing = async () => { - setLoading(true); - const allFollowingRecipes = []; - for (const following of myUserFollowing) { - try { - const response = await fetch(process.env.REACT_APP_API_URL + '/recipe/user/' + following, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - }); - if (!response.ok) { - throw new Error('Failed to fetch recipes for following'); - } - const followingRecipes = await response.json(); - allFollowingRecipes.push(...followingRecipes.slice(0, 2)); - console.error(followingRecipes) - } catch (error) { - console.error('Error fetching recipes for following:', error); - } + const buildRequestUrl = (filters, recipeName, page, numRecipes, feedType) => { + let url = process.env.REACT_APP_API_URL + "/recipe/" + (filters || recipeName || page || numRecipes ? "magic?" : ""); + if (filters) { + url += (filters.sortBy === 'none') ? '' : `sort_by=${filters.sortBy}&`; + url += (filters.sortBy === 'none') ? '' : `order=${filters.sortAscending}&`; + url += filters.maxDifficulty ? `max_difficulty=${filters.maxDifficulty}&` : ''; + url += filters.maxTime ? `max_cooking_time=${filters.maxTime}&` : ''; + url += filters.minRating ? `min_rating=${filters.minRating}&` : ''; + url += filters.maxCalories ? `max_energy=${filters.maxCalories}&` : ''; } - - setRecipesFollowing(allFollowingRecipes); - setLoading(false); - }; - - const displayedRecipes = () => { - return feedType === "forYou" ? recipesForYou : recipesFollowing; + if (recipeName) { + url += `search=${recipeName}&`; + } + if (page !== null && numRecipes !== null) { + url += `start=${page * numRecipes}&`; + url += `size=${numRecipes}&`; + } + url += `feedType=${feedType}`; + console.error(url) + return url; }; - const getRecipes = () => { - setLoading(true); - if (feedType === "following") { - getRecipesFollowing(); - } else { - let url = process.env.REACT_APP_API_URL + "/recipe/"; - fetch(url) - .then((response) => response.json()) - .then((allRecipes) => { - const filteredRecipes = allRecipes.filter(recipe => - !myUserFollowing.includes(recipe.username) - ); - setRecipesForYou(filteredRecipes); - setLoading(false); - }) - .catch((error) => { - console.error("Error al obtener recetas:", error); - setLoading(false); - }); + const handleTabChange = (tab) => { + setPage(0); + setFeedType(tab); + setFinished(false); + localStorage.setItem('feedType', tab) + if (tab === "following") { + if(!isLogged()){ + setShowLoginRedirectModal(true); + }else{ + getRecipes(filters, recipeName, 0, numRecipes, true, 'following'); + } + }else{ + if(!isLogged()){ + getRecipes(loggedOutFilters, null, 0, 9, true); + }else{ + getRecipes(filters, recipeName, 0, numRecipes, true, 'foryou'); + } } -}; - + }; return ( - -
    - - - - + + + + + + + + + + + + {isLogged() && ( + { + setPage(0); + setFinished(false); + setFilters(newFilters); + setRecipeName(newRecipeName); + getRecipes(filters, recipeName, 0, numRecipes, true, feedType); + }}/> + )} -
    {loading ? ( - ) : ( - + ) : ( + { + setPage(page + 1); + if (feedType === "foryou") { + getRecipes(filters, recipeName, page, numRecipes, false, 'foryou'); + } else { + getRecipes(filters, recipeName, page, numRecipes, false, 'following'); + } + }} + finished={finished} + /> )} - setShowLoginRedirectModal(false)}> - - Required log in - - You need to log in to view the recipes of the people you follow. - - - - - - + setShowLoginRedirectModal(false)}> + + Required log in + + You need to log in to view the recipes of the people you follow. + + + + +
    ); } -export default UserFeed; +export default UserFeed; \ No newline at end of file diff --git a/src/css/PostRecipe.css b/src/css/PostRecipe.css index ee63642..36dc33a 100644 --- a/src/css/PostRecipe.css +++ b/src/css/PostRecipe.css @@ -3,7 +3,15 @@ } .difficulty-star.active:hover { - color: rgb(151, 114, 1); + color: rgb(151, 91, 1); +} + +.difficulty-star.active.hovered { + color: rgb(151, 91, 1); +} + +.difficulty-star.active.above-hovered { + color: #7c7c7c; } .difficulty-star.inactive { @@ -14,6 +22,10 @@ color: #7c7c7c; } +.difficulty-star.inactive.hovered { + color: rgb(151, 91, 1); +} + .add-ingredient-button.active { color: rgb(0, 194, 0); } diff --git a/src/css/common.css b/src/css/common.css index 9f8e993..a13f463 100644 --- a/src/css/common.css +++ b/src/css/common.css @@ -119,4 +119,52 @@ .user-nav-image { border-top-right-radius: 25px; border-bottom-right-radius: 25px; +} + +#search-bar { + border-right: none; + transition: none; +} + +#search-bar-clear { + background-color: white; + border-left: none; + cursor: text; + transition: none; +} + +#search-bar-container:focus-within #search-bar { + border-color: #80bdff; +} + +#search-bar-container:focus-within #search-bar-clear { + border-color: #80bdff; +} + +.rating-star.active { + color: #e0b700; +} + +.rating-star.active:hover { + color: rgb(151, 124, 43); +} + +.rating-star.active.hovered { + color: rgb(151, 124, 43); +} + +.rating-star.active.above-hovered { + color: #7c7c7c; +} + +.rating-star.inactive { + color: #a7a7a7; +} + +.rating-star.inactive:hover { + color: #7c7c7c; +} + +.rating-star.inactive.hovered { + color: rgb(151, 124, 43); } \ No newline at end of file diff --git a/src/css/slider.css b/src/css/slider.css new file mode 100644 index 0000000..edc0470 --- /dev/null +++ b/src/css/slider.css @@ -0,0 +1,37 @@ +input[type=range]::-webkit-slider-runnable-track { + background: #ddd; +} + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + background: rgb(255, 74, 42); +} + +input[type=range]:focus { + outline: none; +} + +input[type=range]:focus::-webkit-slider-runnable-track { + background: #ffafaf; +} + +input[type=range]::-moz-range-track { + background: #ffafaf; +} + +input[type=range]::-moz-range-thumb { + background: rgb(255, 74, 42); +} + +input[type=range]:focus::-moz-range-thumb { + background: rgb(155, 26, 4); +} + +/*hide the outline behind the border*/ +input[type=range]:-moz-focusring { + outline: none; +} + +input[type=range]:focus::-moz-range-track { + background: #ffc0c0; +} \ No newline at end of file From be51ec6fc4a5940ca52999659d411101fb39ffa5 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:06:11 +0100 Subject: [PATCH 37/59] feat: post review to post a review --- src/components/PostReview.jsx | 2 +- src/components/Reviews.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index f78e937..cc53a5f 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -152,7 +152,7 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { onClick={handlePostReview} disabled={characterCount > 120} > - Post Review + Post review diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 7bdd160..48850e4 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -66,7 +66,7 @@ function Reviews(props) { {reviews && reviews.length > 0 && owner !== currentUser ? (<> There are currently no reviews

    Be the first one to post a review and share your thoughts!

    Date: Thu, 14 Dec 2023 14:12:15 +0100 Subject: [PATCH 38/59] feat: Change design about similar recipes section --- src/components/RecipeDetail.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index 96d8974..9cf7b2f 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -431,13 +431,13 @@ function RecipeDetail() { - + -

    Similar Recipes

    +

    Similar Recipes

    {similarRecipes.length > 0 ? similarRecipes?.map((recipe) => ( - + Date: Thu, 14 Dec 2023 14:18:16 +0100 Subject: [PATCH 39/59] feat: character count from 120 to 0 --- src/components/PostReview.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index cc53a5f..e03849d 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -43,8 +43,9 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { const handleReviewChange = (e) => { const inputReview = e.target.value; + const remainingCharacters = 120 - inputReview.length; setReview(inputReview); - setCharacterCount(inputReview.length); + setCharacterCount(remainingCharacters); }; const handlePostReview = async () => { @@ -124,12 +125,14 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { placeholder="Write your review here" value={review} onChange={handleReviewChange} - style={{ borderColor: characterCount > 120 ? 'red' : null }} + style={{ borderColor: characterCount < 0 ? 'red' : null }} /> - {characterCount > 120 && ( -
    You exceeded 120 characters.
    + + {characterCount < 0 && ( +
    You exceeded the character limit.
    )} -
    Num characters: {characterCount}
    + +
    Characters remaining: {characterCount}
    Rating @@ -150,7 +153,7 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { className='bg-danger fw-bold border-secondary text-white' variant="primary" onClick={handlePostReview} - disabled={characterCount > 120} + disabled={characterCount < 0} > Post review From 9bff1c9bac8cafc588f6e047d5e09af841ea6878 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:21:42 +0100 Subject: [PATCH 40/59] fix: Change size of Similars title --- src/components/RecipeDetail.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index 9cf7b2f..55b3151 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -434,7 +434,7 @@ function RecipeDetail() { -

    Similar Recipes

    +

    Similar Recipes

    {similarRecipes.length > 0 ? similarRecipes?.map((recipe) => ( From cb597bba4724b7537b7a61e54336fd7a50eb6f87 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 14:34:50 +0100 Subject: [PATCH 41/59] US10 edit recipe and minor changes --- src/assets/jsonData/sort_options.json | 28 ++++++ src/components/PostRecipe.jsx | 119 ++++++++++++++++++++------ src/components/UserFeed.jsx | 19 ++-- src/components/UserProfile.jsx | 31 ++++++- 4 files changed, 162 insertions(+), 35 deletions(-) create mode 100644 src/assets/jsonData/sort_options.json diff --git a/src/assets/jsonData/sort_options.json b/src/assets/jsonData/sort_options.json new file mode 100644 index 0000000..a88e6c0 --- /dev/null +++ b/src/assets/jsonData/sort_options.json @@ -0,0 +1,28 @@ +{"fields": [{ + "name": "none", + "displayName": "---" +}, { + "name": "name", + "displayName": "Title" +}, { + "name": "average_rating", + "displayName": "Rating" +}, { + "name": "cooking_time", + "displayName": "Cooking Time" +}, { + "name": "difficulty", + "displayName": "Difficulty" +}, { + "name": "energy", + "displayName": "Energy" +}, { + "name": "username", + "displayName": "Recipe Author" +}, { + "name": "creation_date", + "displayName": "Creation Date" +}, { + "name": "updated_at", + "displayName": "Last Modified" +}]} \ No newline at end of file diff --git a/src/components/PostRecipe.jsx b/src/components/PostRecipe.jsx index ba64552..bba13b9 100644 --- a/src/components/PostRecipe.jsx +++ b/src/components/PostRecipe.jsx @@ -28,7 +28,7 @@ import { Table, } from "react-bootstrap"; -const RecipePost = ({ onClose }) => { +const RecipePost = ({onClose, edit, id}) => { const { token } = useAuth(); const Unit = { @@ -76,6 +76,42 @@ const RecipePost = ({ onClose }) => { } }, [localStorage.getItem("logged"), navigate]); + useEffect(() => { + if(edit){ + fetchRecipeData(); + } + }, []); + + const fetchRecipeData = async () => { + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/recipe/${id}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Error fetching recipe data'); + } + + const data = await response.json(); + + setRecipeName(data.name); + setIngredients(data.ingredients); + setPreparation(data.instructions); + setTime(data.cooking_time); + setEnergy(data.energy); + setDifficulty(data.difficulty); + setImagePreviewUrl(data.main_image) + + } catch (error) { + console.error('Error:', error); + } + }; + + const renderStars = (amount) => { let stars = []; for (let i = 1; i <= 5; i++) { @@ -228,34 +264,61 @@ const RecipePost = ({ onClose }) => { formData.append("recipe", JSON.stringify(recipeData)); if (image) { formData.append("files", image); - console.log(formData); + } - - - try { - const response = await fetch(process.env.REACT_APP_API_URL + "/recipe/", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - body: formData, - }); - - const data = await response.json(); - - if (response.ok) { - setSubmitMessage("Recipe posted successfully", data); - setPostRecipeSuccess(true); - //onClose(); - } else { - setSubmitMessage("Error posting recipe: " + JSON.stringify(data)); + console.log(formData["recipe"]); + if(edit){ + try { + const response = await fetch(process.env.REACT_APP_API_URL + "/recipe/" + id, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + + const data = await response.json(); + + if (response.ok) { + setSubmitMessage("Updated correctly!", data); + setPostRecipeSuccess(true); + //onClose(); + } else { + setSubmitMessage("Error updating recipe: " + JSON.stringify(data)); + } + } catch (error) { + setSubmitMessage(JSON.stringify(error)); + setPostRecipeSuccess(false); + } finally { + setShowPostRecipeConfirmation(true); + } + }else{ + try { + const response = await fetch(process.env.REACT_APP_API_URL + "/recipe/", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + + const data = await response.json(); + + if (response.ok) { + setSubmitMessage("Posted correctly!", data); + setPostRecipeSuccess(true); + //onClose(); + } else { + setSubmitMessage("Error posting recipe: " + JSON.stringify(data)); + } + } catch (error) { + setSubmitMessage(JSON.stringify(error)); + setPostRecipeSuccess(false); + } finally { + setShowPostRecipeConfirmation(true); } - } catch (error) { - setSubmitMessage(JSON.stringify(error)); - setPostRecipeSuccess(false); - } finally { - setShowPostRecipeConfirmation(true); } + }; const handleImageUpload = (event) => { @@ -268,7 +331,7 @@ const RecipePost = ({ onClose }) => { const handlePostRecipeConfirmationClose = () => { setShowPostRecipeConfirmation(false); if (postSuccess) { - navigate("/"); + onClose(); } }; @@ -661,7 +724,7 @@ const RecipePost = ({ onClose }) => { variant={postSuccess ? "success" : "secondary"} onClick={handlePostRecipeConfirmationClose} > - {postSuccess ? "Go to recipes" : "Close"} + {postSuccess ? "Okay" : "Close"} diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index 0452b73..70e6445 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -6,7 +6,7 @@ import RecipeBrowser from "./RecipeBrowser"; import { Container, Spinner, ButtonGroup, Button, Modal, Row, Col } from "react-bootstrap"; function UserFeed() { - const {isLogged } = useAuth(); + const {token, isLogged } = useAuth(); const navigate = useNavigate(); const numRecipes = isLogged() ? 24 : 9; const [recipes, setRecipes] = useState([]); @@ -31,17 +31,23 @@ function UserFeed() { setRecipeName(null); getRecipes(loggedOutFilters, null, 0, 9, true, feedType); } - }, [isLogged]); + }, [isLogged, feedType]); useEffect(() => { getRecipes(filters, recipeName, page, numRecipes, true, feedType); - }, []); + }, [feedType]); const getRecipes = async (filters, recipeName, page, numRecipes, reset, feedType) => { setLoading(true); let url = buildRequestUrl(filters, recipeName, page, numRecipes, feedType); try { - const response = await fetch(url); + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); if (!response.ok) { if (response.status === 400) { setPage(page - 1); @@ -50,6 +56,7 @@ function UserFeed() { throw new Error('Failed to fetch recipes'); } const data = await response.json(); + console.log(data) if (reset) { setRecipes(data) } else { @@ -135,11 +142,11 @@ function UserFeed() { setFinished(false); setFilters(newFilters); setRecipeName(newRecipeName); - getRecipes(filters, recipeName, 0, numRecipes, true, feedType); + getRecipes(newFilters, newRecipeName, 0, numRecipes, true, feedType); }}/> )} - {loading ? ( + {loading && recipes.length === 0 ? ( diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index c0f76b7..b1cbeee 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -19,6 +19,9 @@ import { Container, Row, Col, Card, Button, Form, Image, Modal, Dropdown, ListGr import "../css/common.css"; import "../css/UserProfile.css"; +//Components +import PostRecipe from "./PostRecipe"; + const UserProfile = () => { const { token, logout, isLogged } = useAuth(); const navigate = useNavigate(); @@ -56,6 +59,7 @@ const UserProfile = () => { const [editMode, setEditMode] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const [showDropdown, setShowDropdown] = useState(false); + const [showEditRecipe, setShowEditRecipe] = useState(false); const [showDropdown2, setShowDropdown2] = useState(false); const [showRemoveQuestion, setRemoveQuestion] = useState(false); const [showRemoveRecipeModal, setShowRemoveRecipeModal] = useState(false); @@ -213,6 +217,14 @@ const UserProfile = () => { setEditMode(false); }; + const handleCloseEditRecipeModal = () => { + setShowEditRecipe(false); + }; + + const handleCloseEditRecipeSuccessfulModal = () => { + setShowEditRecipe(false); + }; + const handleShowRemoveRecipeModal = () => { setShowRemoveRecipeModal(true); }; @@ -665,7 +677,7 @@ const UserProfile = () => {
    @@ -929,6 +941,23 @@ const UserProfile = () => { + + + Edit Recipe + + + + + +
    ); From 58293b2851e8608db3fd60673893aa95674676bf Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 18:26:42 +0100 Subject: [PATCH 42/59] Added a button to follow users in the lists displayed in all the profiles --- src/components/UserFeed.jsx | 25 ++++------- src/components/UserProfile.jsx | 79 ++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index 70e6445..ef8f739 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -31,7 +31,7 @@ function UserFeed() { setRecipeName(null); getRecipes(loggedOutFilters, null, 0, 9, true, feedType); } - }, [isLogged, feedType]); + }, [isLogged]); useEffect(() => { getRecipes(filters, recipeName, page, numRecipes, true, feedType); @@ -95,20 +95,13 @@ function UserFeed() { setPage(0); setFeedType(tab); setFinished(false); - localStorage.setItem('feedType', tab) - if (tab === "following") { - if(!isLogged()){ - setShowLoginRedirectModal(true); - }else{ - getRecipes(filters, recipeName, 0, numRecipes, true, 'following'); - } - }else{ - if(!isLogged()){ - getRecipes(loggedOutFilters, null, 0, 9, true); - }else{ - getRecipes(filters, recipeName, 0, numRecipes, true, 'foryou'); - } + setRecipes([]); + localStorage.setItem('feedType', tab); + if (!isLogged() && tab === "following") { + setShowLoginRedirectModal(true); + return; } + getRecipes(filters, recipeName, 0, numRecipes, true, tab); }; return ( @@ -156,9 +149,9 @@ function UserFeed() { onRequestLoadMore={() => { setPage(page + 1); if (feedType === "foryou") { - getRecipes(filters, recipeName, page, numRecipes, false, 'foryou'); + getRecipes(filters, recipeName, page+1, numRecipes, false, 'foryou'); } else { - getRecipes(filters, recipeName, page, numRecipes, false, 'following'); + getRecipes(filters, recipeName, page+1, numRecipes, false, 'following'); } }} finished={finished} diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index b1cbeee..39e51c3 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -29,6 +29,7 @@ const UserProfile = () => { const { userId } = useParams(); + const [updateCount, setUpdateCount] = useState(0); const [myUserId, setMyUserId] = useState(''); const [myUserName, setMyUserName] = useState(''); const [userName, setUserName] = useState(''); @@ -36,6 +37,7 @@ const UserProfile = () => { const [userBio, setUserBio] = useState(''); const [userFollowers, setUserFollowers] = useState([]); const [userFollowing, setUserFollowing] = useState([]); + const [myFollowing, setMyFollowing] = useState([]); const [userNameAux, setUserNameAux] = useState(''); const [userMailAux, setUserMailAux] = useState(''); const [userBioAux, setUserBioAux] = useState(''); @@ -112,11 +114,16 @@ const UserProfile = () => { const data = await response.json(); setMyUserId(data._id) setMyUserName(data.username) + setMyFollowing(data.following) } catch (error) { console.error('Error fetching user data:', error); } }; + const isFollowed = (follower) => { + return myFollowing.includes(follower.username); + }; + const fetchUserData = async () => { try { const response = await fetch(process.env.REACT_APP_API_URL + '/user/' + userId, { @@ -197,10 +204,11 @@ const UserProfile = () => { if (userName) { getRecipes(); } - }, [userName]); + }, [userName, updateCount]); useEffect(() => { fetchUserDetails(userFollowers, setFollowerDetails); + console.error(followerDetails) }, [userFollowers]); useEffect(() => { @@ -219,10 +227,12 @@ const UserProfile = () => { const handleCloseEditRecipeModal = () => { setShowEditRecipe(false); + setUpdateCount(prevCount => prevCount + 1); }; const handleCloseEditRecipeSuccessfulModal = () => { setShowEditRecipe(false); + setUpdateCount(prevCount => prevCount + 1); }; const handleShowRemoveRecipeModal = () => { @@ -521,6 +531,41 @@ const UserProfile = () => { setIsFollowing(userFollowers.includes(myUserName)); }, [userFollowers]); + const checkIfFollowed = (username) => { + // Verifica si el usuario actual está siguiendo a 'username' + return userFollowing.includes(username); + }; + + const handleFollowUnfollow = async (username, isCurrentlyFollowed) => { + const url = process.env.REACT_APP_API_URL + `/user/${isCurrentlyFollowed ? 'unfollow' : 'follow'}/${username}`; + const method = 'POST'; + + try { + const response = await fetch(url, { + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + + if (!response.ok) { + throw new Error(`Failed to ${isCurrentlyFollowed ? 'unfollow' : 'follow'} user`); + } + + // Actualizar el estado local + setMyFollowing(prevFollowing => { + return isCurrentlyFollowed + ? prevFollowing.filter(user => user !== username) + : [...prevFollowing, username]; + }); + } catch (error) { + console.error('Error:', error); + } + }; + + + const handleFollow = async () => { if (token==null) { setShowLoginRedirectModal(true); @@ -834,9 +879,22 @@ const UserProfile = () => { style={{ width: '30px', marginRight: '10px' }} /> - + {follower.username} + + {follower.username !== myUserName && ( + + )} +
    @@ -859,7 +917,7 @@ const UserProfile = () => { handleNavigate(following._id)}> - + { style={{ width: '30px', marginRight: '10px' }} /> - + {following.username} + + {following.username !== myUserName && ( + + )} + From fa3f76881a41fa2893296cc4b35f9cb5382c02b1 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 20:30:47 +0100 Subject: [PATCH 43/59] Follow and unfollow buttons appear only if the user is logged --- src/components/UserProfile.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index 39e51c3..5c26ce4 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -883,7 +883,7 @@ const UserProfile = () => { {follower.username} - {follower.username !== myUserName && ( + {follower.username !== myUserName && !token==null && ( - )} + {(token !== null) && (follower.username !== myUserName) && ( + + )} +
    @@ -908,10 +909,10 @@ const UserProfile = () => { - + Following - + {userFollowing.length > 0 ? ( followingDetails.map((following, index) => ( @@ -929,17 +930,17 @@ const UserProfile = () => { {following.username} - {following.username !== myUserName && !token==null && ( - - )} + {(token !== null) && (following.username !== myUserName) && ( + + )}
    @@ -951,29 +952,30 @@ const UserProfile = () => { <>

    You're not following anyone. Discover creators that match your taste!

    {suggestedUsers.length > 0 ? ( - suggestedUsers.map((user, index) => ( - - handleNavigate(user._id)}> - - - - - - - {user.username} - - - - - + suggestedUsers.slice(0, 5).map((user, index) => ( + + handleNavigate(user._id)}> + + + + + + + {user.username} + + + + + )) ) : (

    Loading...

    )} + ) : (

    You are not following anyone yet.

    @@ -983,11 +985,11 @@ const UserProfile = () => { setShowUnfollowModal(false)}> - + Unfollow User - Do you want to unfollow this user? - + Do you want to unfollow this user? + From 437899aae43cd760f28e6ee2db7c21385d83504e Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 22:26:51 +0100 Subject: [PATCH 45/59] Minor changes --- src/components/UserFeed.jsx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index 859df4f..fc3722e 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -32,7 +32,7 @@ function UserFeed() { setFilters(loggedOutFilters); setRecipeName(null); console.error("hola") - getRecipes(loggedOutFilters, null, 0, 9, true, feedType); + getRecipesLogout(loggedOutFilters, null, 0, 9, true, feedType); } fetchMyUserData() }, [isLogged]); @@ -60,6 +60,34 @@ function UserFeed() { } }; + const getRecipesLogout = async (filters, recipeName, page, numRecipes, reset, feedType) => { + setLoading(true); + let url = buildRequestUrl(filters, recipeName, page, numRecipes, feedType); + try { + const response = await fetch(url, { + method: 'GET', + }); + if (!response.ok) { + if (response.status === 400) { + setPage(page - 1); + setFinished(true); + } + throw new Error('Failed to fetch recipes'); + } + const data = await response.json(); + console.log(data) + if (reset) { + setRecipes(data) + } else { + setRecipes(recipes.concat(data)); + } + } catch (error) { + console.error('Error fetching recipes:', error); + } finally { + setLoading(false); + } + }; + const getRecipes = async (filters, recipeName, page, numRecipes, reset, feedType) => { setLoading(true); let url = buildRequestUrl(filters, recipeName, page, numRecipes, feedType); @@ -122,6 +150,7 @@ function UserFeed() { localStorage.setItem('feedType', tab); if (!isLogged() && tab === "following") { setShowLoginRedirectModal(true); + setFeedType('foryou'); return; } getRecipes(filters, recipeName, 0, numRecipes, true, tab); From b1d36db248132181f0b85cd974143cc50b612718 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 22:33:12 +0100 Subject: [PATCH 46/59] Minor changes --- src/components/UserFeed.jsx | 6 ++++-- src/components/UserProfile.jsx | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/UserFeed.jsx b/src/components/UserFeed.jsx index fc3722e..608bd73 100644 --- a/src/components/UserFeed.jsx +++ b/src/components/UserFeed.jsx @@ -146,7 +146,9 @@ function UserFeed() { setPage(0); setFeedType(tab); setFinished(false); - setRecipes([]); + if(isLogged()){ + setRecipes([]); + } localStorage.setItem('feedType', tab); if (!isLogged() && tab === "following") { setShowLoginRedirectModal(true); @@ -232,7 +234,7 @@ function UserFeed() { Required log in You need to log in to view the recipes of the people you follow. - + diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index 0644aac..cb136cd 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -769,7 +769,7 @@ const UserProfile = () => { - + @@ -784,7 +784,7 @@ const UserProfile = () => { Profile Update {confirmationMessage} - + @@ -833,7 +833,7 @@ const UserProfile = () => { - + @@ -852,7 +852,7 @@ const UserProfile = () => { {operationSuccess ? 'Success' : 'Error'} {confirmationMessage} - + @@ -989,7 +989,7 @@ const UserProfile = () => { Unfollow User Do you want to unfollow this user? - + @@ -1004,7 +1004,7 @@ const UserProfile = () => { Required log in You need to log in to follow this user. - + From dc594f6d01f6ba0f8a6044318725020941f84ea7 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 23:06:39 +0100 Subject: [PATCH 47/59] Minor changes --- src/components/UserProfile.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index cb136cd..221cef1 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -532,7 +532,6 @@ const UserProfile = () => { }, [userFollowers]); const checkIfFollowed = (username) => { - // Verifica si el usuario actual está siguiendo a 'username' return userFollowing.includes(username); }; @@ -553,12 +552,19 @@ const UserProfile = () => { throw new Error(`Failed to ${isCurrentlyFollowed ? 'unfollow' : 'follow'} user`); } - // Actualizar el estado local setMyFollowing(prevFollowing => { return isCurrentlyFollowed ? prevFollowing.filter(user => user !== username) : [...prevFollowing, username]; }); + + if(myUserName===userName){ + setUserFollowing(prevFollowing => { + return isCurrentlyFollowed + ? prevFollowing.filter(user => user !== username) + : [...prevFollowing, username]; + }) + } } catch (error) { console.error('Error:', error); } From da8cfe2819c3dd7930d696031348f5c290ab6253 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 23:18:13 +0100 Subject: [PATCH 48/59] Merge --- src/components/RecipeDetail.jsx | 183 +++++++++++++++++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index b19f4a1..a8f2687 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -8,6 +8,7 @@ import defaultProfile from "../assets/defaultProfile.png"; import { CSSTransition } from "react-transition-group"; import gyozas from '../assets/gyozas.jpg'; import "bootstrap/dist/css/bootstrap.min.css"; +import { FaWhatsapp, FaLinkedin, FaTwitter, FaCopy } from 'react-icons/fa'; import { Container, Row, @@ -28,6 +29,7 @@ import { FolderSymlinkFill, Heart, HeartFill, + Share } from "react-bootstrap-icons"; import ImageModal from "./ImageModal"; import Reviews from "./Reviews"; @@ -207,6 +209,131 @@ function RecipeDetail() {
    )); + const handleFollow = async () => { + if (!isLogged()) { + setShowLoginRedirectModal(true); + return; + } + + try { + const response = await fetch(process.env.REACT_APP_API_URL + `/user/follow/${userName}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + + if (response.ok) { + setConfirmationMessage(`You're now following ${userName}.`); + setIsFollowing(true); + setToastData({ + message: `You're now following ${userName}.`, + variant: 'success', + show: true, + }); + } else { + throw new Error('There was an error following the user.'); + } + } catch (error) { + console.error('Error following user:', error); + setConfirmationMessage(error.toString()); + setShowConfirmation(true); + } + }; + + const handleUnfollow = async () => { + try { + const response = await fetch(process.env.REACT_APP_API_URL + `/user/unfollow/${userName}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + + if (response.ok) { + setConfirmationMessage("You have unfollowed the user."); + setIsFollowing(false) + setToastData({ + message: `You' have unfollowed ${userName}.`, + variant: 'secondary', + show: true, + }); + } else { + throw new Error('There was an error unfollowing the user.'); + } + } catch (error) { + console.error('Error unfollowing user:', error); + setConfirmationMessage(error.toString()); + setShowConfirmation(true); + } + setShowUnfollowModal(false); + }; + + const createShareLink = (socialNetwork) => { + const url = encodeURIComponent(window.location.href); + const text = encodeURIComponent("Hola, check out this awesome recipe I found!"); + switch (socialNetwork) { + case "twitter": + return `https://twitter.com/intent/tweet?text=${text}&url=${url}`; + case "twitter": + return `https://twitter.com/intent/tweet?text=${text}&url=${url}`; + case "facebook": + return `https://www.facebook.com/sharer/sharer.php?u=${url}`; + case "linkedin": + return `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`; + default: + return ""; + } + }; + + const copyToClipboard = () => { + const url = window.location.href; + navigator.clipboard.writeText(url).then(() => { + setToastData({ + message: "Link copied to clipboard!", + variant: "success", + show: true, + }); + }, (err) => { + console.error('Could not copy text: ', err); + }); + }; + + const handleShare = async () => { + console.error('hola') + if (navigator.share) { + try { + await navigator.share({ + title: 'Check out this recipe!', + text: 'Hola, check out this awesome recipe I found!', + url: window.location.href, + }); + console.log('Content shared successfully'); + } catch (error) { + console.error('Error sharing content:', error); + } + } else { + console.log('Web Share API is not supported in your browser.'); + } + }; + + const recipeUrl = `www.kasula.live/RecipeDetail/${id}`; + const message = encodeURIComponent('Check this amazing recipe that I found in Kasulà! ' + recipeUrl); + + const shareOnWhatsApp = () => { + window.open(`https://wa.me/?text=${message}`, '_blank'); + }; + + const shareOnLinkedIn = () => { + window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${recipeUrl}&title=${encodeURIComponent('Mira esta receta que chula!')}`, '_blank'); + }; + + const shareOnTwitter = () => { + window.open(`https://twitter.com/intent/tweet?text=${message}`, '_blank'); + }; + return ( - + +
    handleNavigate(null, recipe.user_id)}> + + + + + +

    {userName}

    + + + {!adminMode && ( + + )} + +
    +
    + +
    + + + + + + + + WhatsApp + + + LinkedIn + + + Twitter + + + Copy Link + + +
    From 6ca320374b887b66a6ebec3a1e91ae2e28b95126 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 23:45:14 +0100 Subject: [PATCH 49/59] Solve conflicts --- src/components/UserProfile.jsx | 220 +++++++++++++++++++++++++++------ 1 file changed, 182 insertions(+), 38 deletions(-) diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index b952f7a..3bda809 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -20,6 +20,9 @@ import "../css/common.css"; import "../css/UserProfile.css"; import PrivacySettings from "./PrivacySettings"; +//Components +import PostRecipe from "./PostRecipe"; + const UserProfile = () => { const { token, logout, isLogged } = useAuth(); const navigate = useNavigate(); @@ -27,6 +30,7 @@ const UserProfile = () => { const { userId } = useParams(); + const [updateCount, setUpdateCount] = useState(0); const [myUserId, setMyUserId] = useState(''); const [myUserName, setMyUserName] = useState(''); const [userName, setUserName] = useState(''); @@ -34,6 +38,7 @@ const UserProfile = () => { const [userBio, setUserBio] = useState(''); const [userFollowers, setUserFollowers] = useState([]); const [userFollowing, setUserFollowing] = useState([]); + const [myFollowing, setMyFollowing] = useState([]); const [userNameAux, setUserNameAux] = useState(''); const [userMailAux, setUserMailAux] = useState(''); const [userBioAux, setUserBioAux] = useState(''); @@ -52,6 +57,7 @@ const UserProfile = () => { const [followingDetails, setFollowingDetails] = useState([]); const [imPrivate, setImPrivate] = useState(true); const [userIsPrivate, setUserIsPrivate] = useState(true); + const [suggestedUsers, setSuggestedUsers] = useState([]); const [adminMode, setadminMode] = useState(false); @@ -59,6 +65,8 @@ const UserProfile = () => { const [showConfirmation, setShowConfirmation] = useState(false); const [showDropdown, setShowDropdown] = useState(false); const [showPrivacySettings, setShowPrivacySettings] = useState(false); + const [showEditRecipe, setShowEditRecipe] = useState(false); + const [showDropdown2, setShowDropdown2] = useState(false); const [showRemoveQuestion, setRemoveQuestion] = useState(false); const [showRemoveRecipeModal, setShowRemoveRecipeModal] = useState(false); const [selectedRecipeId, setSelectedRecipeId] = useState(null); @@ -111,11 +119,16 @@ const UserProfile = () => { setMyUserId(data._id) setMyUserName(data.username) setImPrivate(data.is_private) + setMyFollowing(data.following) } catch (error) { console.error('Error fetching user data:', error); } }; + const isFollowed = (follower) => { + return myFollowing.includes(follower.username); + }; + const fetchUserData = async () => { try { const response = await fetch(process.env.REACT_APP_API_URL + '/user/' + userId, { @@ -160,13 +173,34 @@ const UserProfile = () => { setUserDetails(userDetails); }; + const fetchSuggestedUsers = async () => { + try { + const response = await fetch(process.env.REACT_APP_API_URL + `/user/new/discover`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + if (!response.ok) { + throw new Error('Failed to fetch suggested users'); + } + const data = await response.json(); + setSuggestedUsers(data); + } catch (error) { + console.error('Error fetching suggested users:', error); + } + }; + useEffect(() => { - if (isLogged) { + if (token!=null) { fetchMyUserData(); + fetchSuggestedUsers(); } - }, [token, navigate]); + }, [token]); useEffect(() => { + if(myUserId == userId){ setadminMode(true); } @@ -177,10 +211,11 @@ const UserProfile = () => { if (userName) { getRecipes(); } - }, [userName]); + }, [userName, updateCount]); useEffect(() => { fetchUserDetails(userFollowers, setFollowerDetails); + console.error(followerDetails) }, [userFollowers]); useEffect(() => { @@ -197,6 +232,16 @@ const UserProfile = () => { setEditMode(false); }; + const handleCloseEditRecipeModal = () => { + setShowEditRecipe(false); + setUpdateCount(prevCount => prevCount + 1); + }; + + const handleCloseEditRecipeSuccessfulModal = () => { + setShowEditRecipe(false); + setUpdateCount(prevCount => prevCount + 1); + }; + const handleShowRemoveRecipeModal = () => { setShowRemoveRecipeModal(true); }; @@ -253,15 +298,16 @@ const UserProfile = () => { setUserName(updatedData.username); setUserMail(updatedData.email); setUserBio(updatedData.bio || ''); + setOperationSuccess(true) setConfirmationMessage("Profile updated successfully"); } catch (error) { console.error('Error updating user data:', error); - setConfirmationMessage("Failed to update profile"); + setOperationSuccess(false) + setConfirmationMessage("Oops! Something went wrong."); } setShowConfirmation(true); - setOperationSuccess(true) setEditMode(false); }; @@ -323,14 +369,14 @@ const handleVisibilityChange = async (newVisibility) => { }); if (!response.ok) { - throw new Error('Failed to update user data'); + throw new Error('Oops! Something went wrong.'); } const data = await response.json(); setConfirmationMessage("Profile updated successfully"); } catch (error) { console.error('Error updating user data:', error); - setConfirmationMessage("Failed to update profile"); + setConfirmationMessage("Oops! Something went wrong."); } setShowConfirmation(true); } @@ -532,11 +578,51 @@ const handleVisibilityChange = async (newVisibility) => { useEffect(() => { setIsFollowing(userFollowers.includes(myUserName)); - console.error(userFollowers) }, [userFollowers]); + const checkIfFollowed = (username) => { + return userFollowing.includes(username); + }; + + const handleFollowUnfollow = async (username, isCurrentlyFollowed) => { + const url = process.env.REACT_APP_API_URL + `/user/${isCurrentlyFollowed ? 'unfollow' : 'follow'}/${username}`; + const method = 'POST'; + + try { + const response = await fetch(url, { + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + }); + + if (!response.ok) { + throw new Error(`Failed to ${isCurrentlyFollowed ? 'unfollow' : 'follow'} user`); + } + + setMyFollowing(prevFollowing => { + return isCurrentlyFollowed + ? prevFollowing.filter(user => user !== username) + : [...prevFollowing, username]; + }); + + if(myUserName===userName){ + setUserFollowing(prevFollowing => { + return isCurrentlyFollowed + ? prevFollowing.filter(user => user !== username) + : [...prevFollowing, username]; + }) + } + } catch (error) { + console.error('Error:', error); + } + }; + + + const handleFollow = async () => { - if (isLogged) { + if (token==null) { setShowLoginRedirectModal(true); return; } @@ -734,10 +820,10 @@ const handleVisibilityChange = async (newVisibility) => { setShowRemoveRecipeModal(false)}> - + Remove recipe - + @@ -749,7 +835,7 @@ const handleVisibilityChange = async (newVisibility) => { - + @@ -760,11 +846,11 @@ const handleVisibilityChange = async (newVisibility) => { show={showConfirmation} onHide={handleConfirmationClose} > - + Profile Update - {confirmationMessage} - + {confirmationMessage} + @@ -772,11 +858,11 @@ const handleVisibilityChange = async (newVisibility) => { - + {operationSuccess ? 'Success' : 'Error'} - {confirmationMessage} - + {confirmationMessage} + @@ -784,12 +870,12 @@ const handleVisibilityChange = async (newVisibility) => { - + Followers - + {followerDetails.length > 0 ? ( followerDetails.map((follower, index) => ( @@ -803,9 +889,23 @@ const handleVisibilityChange = async (newVisibility) => { style={{ width: '30px', marginRight: '10px' }} /> - + {follower.username} + + {(token !== null) && (follower.username !== myUserName) && ( + + )} + +
    @@ -819,43 +919,87 @@ const handleVisibilityChange = async (newVisibility) => { - + Following - - {followingDetails.length > 0 ? ( - followingDetails.map((follower, index) => ( - - handleNavigate(follower._id)}> + + {userFollowing.length > 0 ? ( + followingDetails.map((following, index) => ( + + handleNavigate(following._id)}> - + - - {follower.username} + + {following.username} + + + {(token !== null) && (following.username !== myUserName) && ( + + )} - )) + )) + ) : ( + adminMode ? ( + <> +

    You're not following anyone. Discover creators that match your taste!

    + {suggestedUsers.length > 0 ? ( + suggestedUsers.slice(0, 5).map((user, index) => ( + + handleNavigate(user._id)}> + + + + + + + {user.username} + + + + + + )) + ) : ( +

    Loading...

    + )} + + ) : ( -

    There are no users to display.

    - )} +

    You are not following anyone yet.

    + ) + )}
    setShowUnfollowModal(false)}> - + Unfollow User - Do you want to unfollow this user? - + Do you want to unfollow this user? + @@ -870,7 +1014,7 @@ const handleVisibilityChange = async (newVisibility) => { Required log in You need to log in to follow this user. - + From 0b694cbf268a5918cdbb7157ae69c1adc5f1b4a4 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Fri, 15 Dec 2023 00:09:08 +0100 Subject: [PATCH 50/59] Minor changes --- src/components/UserProfile.jsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/UserProfile.jsx b/src/components/UserProfile.jsx index 3bda809..bf5dc47 100644 --- a/src/components/UserProfile.jsx +++ b/src/components/UserProfile.jsx @@ -783,7 +783,7 @@ const handleVisibilityChange = async (newVisibility) => {
    @@ -1050,10 +1050,26 @@ const handleVisibilityChange = async (newVisibility) => { + + + Edit Recipe + + + + + +
    ); }; export default UserProfile; - From 9a9dacba6deb1092524c2c3fd19a534452c62919 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Fri, 15 Dec 2023 00:33:00 +0100 Subject: [PATCH 51/59] Share recipes --- src/components/RecipeDetail.jsx | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index c5cddff..7c02d49 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -9,6 +9,7 @@ import defaultProfile from "../assets/defaultProfile.png"; import { CSSTransition } from "react-transition-group"; import gyozas from '../assets/gyozas.jpg'; import "bootstrap/dist/css/bootstrap.min.css"; +import { FaWhatsapp, FaLinkedin, FaTwitter, FaCopy } from 'react-icons/fa'; import { Container, Row, @@ -29,6 +30,7 @@ import { FolderSymlinkFill, Heart, HeartFill, + Share } from "react-bootstrap-icons"; import ImageModal from "./ImageModal"; import Reviews from "./Reviews"; @@ -301,6 +303,34 @@ function RecipeDetail() { setShowUnfollowModal(false); }; + const recipeUrl = `www.kasula.live/RecipeDetail/${id}`; + const message = encodeURIComponent('Check this amazing recipe that I found in Kasulà! ' + recipeUrl); + + const shareOnWhatsApp = () => { + window.open(`https://wa.me/?text=${message}`, '_blank'); + }; + + const shareOnLinkedIn = () => { + window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${recipeUrl}&title=${encodeURIComponent('Mira esta receta que chula!')}`, '_blank'); + }; + + const shareOnTwitter = () => { + window.open(`https://twitter.com/intent/tweet?text=${message}`, '_blank'); + }; + + const copyToClipboard = () => { + const url = window.location.href; + navigator.clipboard.writeText(url).then(() => { + setToastData({ + message: "Link copied to clipboard!", + variant: "success", + show: true, + }); + }, (err) => { + console.error('Could not copy text: ', err); + }); + }; + return ( + + + + + + + + WhatsApp + + + LinkedIn + + + Twitter + + + Copy + + + From 3185e5ff56e1fe22a87b3636fe4a07bc20bcf02c Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Fri, 15 Dec 2023 00:36:18 +0100 Subject: [PATCH 52/59] feat: Username of reviews clickable and redirect to userProfile --- src/components/Reviews.jsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 48850e4..265ff5d 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -2,11 +2,11 @@ import React, { useState, useEffect } from "react"; import { Container, Row, Col, Button, Image } from "react-bootstrap"; import PostReview from "./PostReview"; import { StarFill, PatchCheck, PencilSquare, Trash } from 'react-bootstrap-icons'; +import { useNavigate } from "react-router-dom"; import LikesReview from "./LikesReview"; import ImageModal from "./ImageModal"; import ModifyReview from "./ModifyReview"; - function Reviews(props) { const { id, reloadReviews, owner } = props; const [reviews, setReviews] = useState(null); @@ -18,6 +18,7 @@ function Reviews(props) { const [selectedFunct, setSelectedFunct] = useState(null); const isLogged = window.localStorage.getItem("logged"); const currentUser = localStorage.getItem('currentUser'); + const navigate = useNavigate(); useEffect(() => { @@ -60,6 +61,10 @@ function Reviews(props) { setSelectedFunct(null); }; + const handleNavigate = (userId) => { + navigate(`/UserProfile/${userId}`); + }; + return ( {isLogged === 'true' ? @@ -85,8 +90,10 @@ function Reviews(props) {
  • {review.image ? - - {review.username}:{" "} + +
    handleNavigate(review.username)}> + {review.username}:{" "} +
    {review.comment} @@ -156,8 +163,10 @@ function Reviews(props) {
    : - - {review.username}:{" "} + +
    handleNavigate(review.username)}> + {review.username}:{" "} +
    {review.comment} From ec8f580677b55fd04e941d624f5f30798418fdaa Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Fri, 15 Dec 2023 00:40:55 +0100 Subject: [PATCH 53/59] feat: AC done, red heart as likes --- src/components/LikesReview.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/LikesReview.jsx b/src/components/LikesReview.jsx index 71c56f9..df17209 100644 --- a/src/components/LikesReview.jsx +++ b/src/components/LikesReview.jsx @@ -74,7 +74,8 @@ function LikesReview({ reviewUsername, recipeId, reviewId, initialLikes, likedBy From c8bf9d2d15604f271cfc68898561ce28af250f0a Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Fri, 15 Dec 2023 00:49:12 +0100 Subject: [PATCH 54/59] Dependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1edbe37..86f5a9d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "react-bootstrap": "^2.9.1", "react-bootstrap-icons": "^1.10.3", "react-dom": "^18.2.0", + "react-icons": "^4.12.0", "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", "react-toastify": "^9.1.3", From e8d4efd3c810368b8a70f66093c482fca4c2a3ac Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Fri, 15 Dec 2023 00:51:19 +0100 Subject: [PATCH 55/59] feat: Redesign message of error reviewed recipe --- src/components/LikesReview.jsx | 4 +--- src/components/PostReview.jsx | 18 +++++------------- src/components/RecipeList.jsx | 9 +-------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/components/LikesReview.jsx b/src/components/LikesReview.jsx index df17209..909ae97 100644 --- a/src/components/LikesReview.jsx +++ b/src/components/LikesReview.jsx @@ -2,10 +2,9 @@ import React, { useState } from "react"; import {Row, Col } from "react-bootstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faThumbsUp, faHeart } from "@fortawesome/free-solid-svg-icons"; +import { faHeart } from "@fortawesome/free-solid-svg-icons"; import { useAuth } from "./AuthContext"; - function LikesReview({ reviewUsername, recipeId, reviewId, initialLikes, likedBy, reloadReviews }) { const [likes, setLikes] = useState(initialLikes); const [hasLiked, setHasLiked] = useState(false); @@ -43,7 +42,6 @@ function LikesReview({ reviewUsername, recipeId, reviewId, initialLikes, likedBy setLikes(likes + 1); setHasLiked(true); setHasLikedByUser(true); - // Realiza la lógica para enviar el like a la base de datos try { const response = await fetch( `${process.env.REACT_APP_API_URL}/review/like/${recipeId}/${reviewId}`, diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index e03849d..8f318a6 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -5,9 +5,6 @@ import { StarFill, Star } from "react-bootstrap-icons"; - - - const PostReview = ({ id, show, onHide, reloadReviews }) => { const [username, setUsername] = useState(''); const [review, setReview] = useState(''); @@ -19,8 +16,6 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { const [showErrorModal, setShowErrorModal] = useState(false); const [characterCount, setCharacterCount] = useState(0); - - useEffect(() => { fetch(process.env.REACT_APP_API_URL + "/user/me", { method: "GET", @@ -69,8 +64,6 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { }, body: formData, }); - // 403 owner - // 400 ya review const data = await response.json(); if (response.ok) { console.log(">>>Post hecho: ", data) @@ -79,7 +72,6 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { setShowErrorModal(true); } } catch (error) { - // alert("HA FALLADO EL POST") setError(error); setShowErrorModal(true); } @@ -160,15 +152,15 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { setShowErrorModal(false)}> - - Error + + Not allowed - - + + {error &&

    {error}

    }
    - + diff --git a/src/components/RecipeList.jsx b/src/components/RecipeList.jsx index 87c7867..353acb2 100644 --- a/src/components/RecipeList.jsx +++ b/src/components/RecipeList.jsx @@ -1,18 +1,11 @@ -//React + import { CSSTransition } from "react-transition-group"; import { useRef, useEffect } from "react"; - -//Bootstrap import { Link } from "react-router-dom"; import { Container, Row, Col } from "react-bootstrap"; import { X } from "react-bootstrap-icons"; - -//Components import RecipeCard from "./RecipeCard"; - -//CSS import "../css/Transitions.css"; -import { wait } from "@testing-library/user-event/dist/utils"; function RecipeList({ recipes, canDelete, onDeleteRecipe, onRequestLoadMore, id, token, finished }) { const myRef = useRef(); From 4e37fdd2221fb2a32c5257ab0b5376062a4705ff Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Fri, 15 Dec 2023 01:08:34 +0100 Subject: [PATCH 56/59] Dependencies --- package-lock.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/package-lock.json b/package-lock.json index 826559f..a88170a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react-bootstrap": "^2.9.1", "react-bootstrap-icons": "^1.10.3", "react-dom": "^18.2.0", + "react-icons": "^4.12.0", "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", "react-toastify": "^9.1.3", @@ -15542,6 +15543,14 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -29826,6 +29835,12 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "requires": {} + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", From 9954be1c6dfea46001bafc6facec43459b4f5419 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Fri, 15 Dec 2023 01:17:18 +0100 Subject: [PATCH 57/59] feat: likes and dislikes new implementation and redesign --- package-lock.json | 37 ++++++++++++++++++++++++++++++++++ package.json | 1 + src/components/LikesReview.jsx | 12 ++++++----- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 826559f..8fbdc59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@testing-library/jest-dom": "^5.17.0", @@ -2533,6 +2534,27 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", + "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", @@ -20325,6 +20347,21 @@ "@fortawesome/fontawesome-common-types": "6.4.2" } }, + "@fortawesome/free-regular-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", + "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "dependencies": { + "@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==" + } + } + }, "@fortawesome/free-solid-svg-icons": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", diff --git a/package.json b/package.json index 1edbe37..6b298f8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@testing-library/jest-dom": "^5.17.0", diff --git a/src/components/LikesReview.jsx b/src/components/LikesReview.jsx index 909ae97..74c3a32 100644 --- a/src/components/LikesReview.jsx +++ b/src/components/LikesReview.jsx @@ -2,7 +2,8 @@ import React, { useState } from "react"; import {Row, Col } from "react-bootstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faHeart } from "@fortawesome/free-solid-svg-icons"; +import { faHeart as heartSolid } from '@fortawesome/free-solid-svg-icons'; // Icono sólido +import { faHeart as heartRegular } from '@fortawesome/free-regular-svg-icons'; // Icono regular import { useAuth } from "./AuthContext"; function LikesReview({ reviewUsername, recipeId, reviewId, initialLikes, likedBy, reloadReviews }) { @@ -66,14 +67,15 @@ function LikesReview({ reviewUsername, recipeId, reviewId, initialLikes, likedBy const cursorStyle = isLogged === 'true' && !isOwnerReview ? { cursor: "pointer" } : { cursor: "not-allowed" }; + const iconoLike = hasLikedByUser ? heartSolid : heartRegular; + return ( @@ -81,7 +83,7 @@ function LikesReview({ reviewUsername, recipeId, reviewId, initialLikes, likedBy - ); + ); } export default LikesReview; From 6e1c95f58033a82a007875c8810826adafa4d134 Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Fri, 15 Dec 2023 01:49:12 +0100 Subject: [PATCH 58/59] fix: change message of error --- src/components/PostReview.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/PostReview.jsx b/src/components/PostReview.jsx index 8f318a6..b221400 100644 --- a/src/components/PostReview.jsx +++ b/src/components/PostReview.jsx @@ -68,7 +68,8 @@ const PostReview = ({ id, show, onHide, reloadReviews }) => { if (response.ok) { console.log(">>>Post hecho: ", data) } else{ - setError(data.detail); + setError("You have already reviewed this recipe!") + // setError(data.detail); setShowErrorModal(true); } } catch (error) { From fefe46f99d29095636d5fa0eb6d54f4a35dc370e Mon Sep 17 00:00:00 2001 From: Ivan Mansilla Flores <72189801+ivanmansilla@users.noreply.github.com> Date: Fri, 15 Dec 2023 02:34:52 +0100 Subject: [PATCH 59/59] feat: new component to change design of similar recipes --- package-lock.json | 35 +++++++++++++++------ package.json | 1 + src/components/RecipeDetail.jsx | 3 +- src/components/SimilarRecipes.jsx | 52 +++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 src/components/SimilarRecipes.jsx diff --git a/package-lock.json b/package-lock.json index 3873629..d16c86f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", @@ -2523,18 +2524,26 @@ } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", - "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", "hasInstallScript": true, - "peer": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.4.2" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" } }, + "node_modules/@fortawesome/fontawesome-svg-core/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-regular-svg-icons": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", @@ -20348,12 +20357,18 @@ "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==" }, "@fortawesome/fontawesome-svg-core": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", - "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", - "peer": true, + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", "requires": { - "@fortawesome/fontawesome-common-types": "6.4.2" + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "dependencies": { + "@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==" + } } }, "@fortawesome/free-regular-svg-icons": { diff --git a/package.json b/package.json index ce305b0..baed3d0 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/src/components/RecipeDetail.jsx b/src/components/RecipeDetail.jsx index 7c02d49..fe7fcd3 100644 --- a/src/components/RecipeDetail.jsx +++ b/src/components/RecipeDetail.jsx @@ -3,6 +3,7 @@ import { useAuth } from "./AuthContext"; import "../css/common.css"; import "../css/Transitions.css"; import RecipeCard from "./RecipeCard"; +import SimilarRecipes from "./SimilarRecipes"; import chefIcon from "../assets/icons/chef.png" import { useParams, useNavigate, Link } from "react-router-dom"; import defaultProfile from "../assets/defaultProfile.png"; @@ -589,7 +590,7 @@ function RecipeDetail() { to={`/RecipeDetail/${recipe._id}`} className="text-decoration-none" > - + )): null} diff --git a/src/components/SimilarRecipes.jsx b/src/components/SimilarRecipes.jsx new file mode 100644 index 0000000..89a9731 --- /dev/null +++ b/src/components/SimilarRecipes.jsx @@ -0,0 +1,52 @@ +import { Card, Image, Row, Col } from "react-bootstrap"; +import { StarFill } from "react-bootstrap-icons"; +import "../css/UserFeed.css"; +import chefIcon from "../assets/icons/chef.png"; +import gyoza from "../assets/gyozas.jpg"; + +function SimilarRecipes({ recipe }) { + return ( + + + + + + + {recipe.name} + + + + +
    + {Array(recipe.difficulty || 0) + .fill() + .map((_, index) => ( + + + + ))} +
    + + + + {recipe?.average_rating?.toFixed(1) || 0} + +
    +
    + By {recipe?.username} +
    + ); + + } + + export default SimilarRecipes;