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/14] 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/14] 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/14] 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/14] 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/14] 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 178650a73530b321f13de574d24b332964921cff Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 7 Dec 2023 10:39:58 +0100 Subject: [PATCH 06/14] 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 fb9f25b3c660e4a9898065773a42cc34bc3e5fdf Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Wed, 13 Dec 2023 23:46:15 +0100 Subject: [PATCH 07/14] 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 cb597bba4724b7537b7a61e54336fd7a50eb6f87 Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Thu, 14 Dec 2023 14:34:50 +0100 Subject: [PATCH 08/14] 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 09/14] 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 10/14] 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 12/14] 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 13/14] 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 14/14] 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); }