diff --git a/src/features/recipes/components/recipe-details/Card.jsx b/src/features/recipes/components/recipe-details/Card.jsx
new file mode 100644
index 0000000..84fbb27
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Card.jsx
@@ -0,0 +1,18 @@
+import PropTypes from "prop-types";
+
+export const Card = ({ children, className, id }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+Card.propTypes = {
+ children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+ className: PropTypes.string,
+ id: PropTypes.string,
+};
diff --git a/src/features/recipes/components/recipe-details/Description.jsx b/src/features/recipes/components/recipe-details/Description.jsx
new file mode 100644
index 0000000..39663f3
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Description.jsx
@@ -0,0 +1,51 @@
+import PropTypes from "prop-types";
+import { Heading } from "@/features/ui/Heading";
+
+export const Description = ({ description }) => {
+ const words = description.split(" ");
+
+ let titleLength = 2;
+
+ const inflectionWords = [
+ "is",
+ "are",
+ "was",
+ "were",
+ "has",
+ "can",
+ "at",
+ "to",
+ "for",
+ "so",
+ "tastes",
+ ];
+
+ let indexOfInflectionWord = words
+ .slice(0, 6)
+ .findIndex((w) => inflectionWords.includes(w));
+ if (indexOfInflectionWord !== -1) {
+ titleLength = indexOfInflectionWord;
+ }
+
+ const title = `${words.slice(0, titleLength).join(" ")}`;
+ const body = words.slice(titleLength).join(" ");
+ console.log("body length:", body.length);
+ return description ? (
+
+
+ 10 ? "text-clamp-h3" : ""}>
+ {title}
+
+
+
100 ? "" : "text-2xl"} italic md:text-2xl`}
+ >
+ {body}
+
+
+ ) : null;
+};
+
+Description.propTypes = {
+ description: PropTypes.string,
+};
diff --git a/src/features/recipes/components/recipe-details/FavoriteButton.jsx b/src/features/recipes/components/recipe-details/FavoriteButton.jsx
new file mode 100644
index 0000000..3ed2107
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/FavoriteButton.jsx
@@ -0,0 +1,56 @@
+import PropTypes from "prop-types";
+import { useState, useEffect } from "react";
+import { Button, Icon } from "@/features/ui";
+
+export const FavoriteButton = ({ recipe }) => {
+ // Function to check if a recipe with a specific ID exists in the array
+ const hasFavoriteRecipeWithId = (recipeId) => {
+ return favorites.some((recipe) => recipe.id === recipeId);
+ };
+
+ const handleFavoriteClick = (recipe) => {
+ if (hasFavoriteRecipeWithId(recipe.id)) {
+ // Recipe is already a favorite, so remove it.
+ const updatedFavorites = favorites.filter(
+ (favRecipe) => favRecipe.id !== recipe.id,
+ );
+ setFavorites(updatedFavorites);
+ localStorage.setItem("favorites", JSON.stringify(updatedFavorites));
+ } else {
+ // Recipe is not a favorite, so add it.
+ const updatedFavorites = [...favorites, recipe];
+ setFavorites(updatedFavorites);
+ localStorage.setItem("favorites", JSON.stringify(updatedFavorites));
+ }
+ };
+
+ const [favorites, setFavorites] = useState([]);
+
+ useEffect(() => {
+ const savedFavorites = localStorage.getItem("favorites");
+ if (savedFavorites) {
+ // Parse the saved JSON data
+ setFavorites(JSON.parse(savedFavorites));
+ }
+ }, []);
+ return (
+
handleFavoriteClick(recipe)}
+ >
+
+ {" "}
+ {hasFavoriteRecipeWithId(recipe.id)
+ ? "Remove from Favorites"
+ : "Add to Favorites"}
+
+
+
+ );
+};
+
+FavoriteButton.propTypes = {
+ recipe: PropTypes.object,
+};
diff --git a/src/features/recipes/components/recipe-details/IngredientSections.jsx b/src/features/recipes/components/recipe-details/IngredientSections.jsx
new file mode 100644
index 0000000..2dddf7e
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/IngredientSections.jsx
@@ -0,0 +1,30 @@
+import PropTypes from "prop-types";
+import { Heading } from "@/features/ui";
+import { Section } from "./Section";
+import Lime from "@/assets/brand/lime/Lime";
+
+export const IngredientSections = ({ sections }) => {
+ return (
+
+
+ Ingredients
+
+
+ {sections.map((section, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
+
+IngredientSections.propTypes = {
+ sections: PropTypes.array,
+};
diff --git a/src/features/recipes/components/recipe-details/Instructions.jsx b/src/features/recipes/components/recipe-details/Instructions.jsx
new file mode 100644
index 0000000..4f21b67
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Instructions.jsx
@@ -0,0 +1,31 @@
+import PropTypes from "prop-types";
+import { Heading } from "@/features/ui";
+import YumiWithBasket from "@/assets/brand/yumi-with-basket/YumiWithBasket";
+
+export const Instructions = ({ instructions }) => {
+ return (
+
+
+ Preparation
+
+
+ {instructions.map((step, index) => {
+ const { display_text } = step;
+ return (
+
+ {`${display_text}`}
+
+ );
+ })}
+
+
+
+ );
+};
+
+Instructions.propTypes = {
+ instructions: PropTypes.array,
+};
diff --git a/src/features/recipes/components/recipe-details/IntroCard.jsx b/src/features/recipes/components/recipe-details/IntroCard.jsx
new file mode 100644
index 0000000..7f5d8ef
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/IntroCard.jsx
@@ -0,0 +1,49 @@
+import PropTypes from "prop-types";
+import { Description } from "./Description";
+import { Card } from "./Card";
+import PlantsTL from "@/assets/brand/plants-tl/PlantsTL";
+
+export const IntroCard = ({ name, imageUrl, description }) => {
+ if (description)
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ return (
+
+
+
+
+
+ );
+};
+
+IntroCard.propTypes = {
+ name: PropTypes.string,
+ imageUrl: PropTypes.string,
+ description: PropTypes.string,
+};
diff --git a/src/features/recipes/components/recipe-details/Nutrition.jsx b/src/features/recipes/components/recipe-details/Nutrition.jsx
new file mode 100644
index 0000000..262334d
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Nutrition.jsx
@@ -0,0 +1,38 @@
+import PropTypes from "prop-types";
+import { Heading } from "@/features/ui";
+
+export const NutritionSection = ({ nutrition }) => {
+ if (!nutrition || Object.keys(nutrition).length === 0) {
+ return
No nutritional information available.
;
+ }
+ const { updated_at, ...rest } = nutrition;
+ return (
+ <>
+
+ {Object.entries(rest).map(([key, value]) => {
+ return (
+
+
+ {key}
+
+
+ {value}
+ {key === "calories" ? "" : "g"}
+
+
+ );
+ })}
+
+
+ {`Last updated on ${updated_at.slice(0, 10)}`}
+
+ >
+ );
+};
+
+NutritionSection.propTypes = {
+ nutrition: PropTypes.object,
+};
diff --git a/src/features/recipes/components/recipe-details/QuickLink.jsx b/src/features/recipes/components/recipe-details/QuickLink.jsx
new file mode 100644
index 0000000..cb876fb
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/QuickLink.jsx
@@ -0,0 +1,22 @@
+import PropTypes from "prop-types";
+import { Button, Icon } from "@/features/ui";
+
+export const QuickLink = ({ label, to }) => {
+ return (
+
{
+ console.log("Scrolling to ", to);
+ document.querySelector(to)?.scrollIntoView();
+ }}
+ className="w-full text-center justify-between text-lg my-4"
+ >
+ {label}
+
+
+ );
+};
+
+QuickLink.propTypes = {
+ label: PropTypes.string,
+ to: PropTypes.string,
+};
diff --git a/src/features/recipes/components/recipe-details/RecipeDetails.jsx b/src/features/recipes/components/recipe-details/RecipeDetails.jsx
index 74a4e84..c4b1302 100644
--- a/src/features/recipes/components/recipe-details/RecipeDetails.jsx
+++ b/src/features/recipes/components/recipe-details/RecipeDetails.jsx
@@ -1,189 +1,41 @@
import PropTypes from "prop-types";
-import { Heading } from "@/features/ui/Heading";
-import { Link } from "react-router-dom";
-import { Button } from "@/features/ui";
-import Icon from "@/assets/icons/Icon";
+import { Heading } from "@/features/ui";
-export const RecipeDetails = ({ recipe }) => {
- const Card = ({ children, className }) => {
- return (
-
- {children}
-
- );
- };
-
- const Description = ({ description }) => {
- const words = recipe.description.split(" ");
-
- const title = `${words[0]} ${words[1]}...`;
- const body = words.slice(2).join(" ");
-
- return description ? (
-
- ) : null;
- };
-
- const Topics = ({ topics }) => {
- return (
-
- Topics:
- {topics.map((topic, index) => {
- return (
- <>
-
- {topic.name}
-
- {index !== recipe.topics.length - 1 ? ", " : ""}
- >
- );
- })}
-
- );
- };
-
- const Tags = ({ tags }) => {
- return (
-
- Tags:
- {tags.map((tag, index) => (
- <>
-
- {tag.display_name}
-
- {index !== recipe.tags.length - 1 ? ", " : ""}
- >
- ))}
-
- );
- };
-
- const QuickLink = ({ label, to }) => {
- return (
-
- {label}
-
-
- );
- };
-
- const RecipeVideo = ({ videoUrl, renditions }) => {
- if (!videoUrl) return null;
- const sources = Object.values(renditions).map((src, index) => {
- return
;
- });
- return (
-
-
- {sources}
-
-
- );
- };
-
- const Section = ({ list, name }) => {
- const components = list.map((component, index) => {
- console.log(component);
- const quantity = component.measurements[0].quantity;
- const unit = component.measurements[0].unit;
- const isPlural = quantity > 1 ? true : false;
- console.log(isPlural);
- const measurement = (
-
-
- {quantity && quantity !== "0" ? quantity + " " : "-"}
-
-
- {isPlural ? unit.display_plural + " " : unit.display_singular + " "}
-
-
- );
- const name =
- isPlural && unit.display_plural === ""
- ? component.ingredient.display_plural + " "
- : component.ingredient.display_singular + " ";
- const note = component.extra_comment
- ? ` (${component.extra_comment})`
- : "";
- return (
-
- {measurement}
- {name}
- {note}
-
- );
- });
- return (
-
-
- {name || "You'll Need:"}
-
-
-
- );
- };
-
- const IngredientSections = ({ sections }) => {
- return (
-
-
- Ingredients
-
- {sections.map((section, index) => {
- return (
-
- );
- })}
-
- );
- };
+// Page Components
+import { IntroCard } from "./IntroCard";
+import { Card } from "./Card";
+import { Tags } from "./Tags";
+import { QuickLink } from "./QuickLink";
+import { RecipeVideo } from "./RecipeVideo";
+import { RecipeDifficultyCard } from "./RecipeDifficultyCard";
+import { IngredientSections } from "./IngredientSections";
+import { Instructions } from "./Instructions";
+import { FavoriteButton } from "./FavoriteButton";
+import { NutritionSection } from "./Nutrition";
+import { Tips } from "./Tips";
+import usePageTitle from "../../../../hooks/usePageTitle";
+export const RecipeDetails = ({ recipe }) => {
+ usePageTitle(recipe.name);
return (
-
+
{recipe.name}
-
-
-
-
-
-
+
+
+
+
Filed Under
- {recipe.topics && }
Quick Links
@@ -192,16 +44,50 @@ export const RecipeDetails = ({ recipe }) => {
-
+
+
-
+
+
+
+
+
+
+
+
+
+ Nutrition
+
+
+
+
+
+ Tips
+
+
+
);
@@ -209,18 +95,6 @@ export const RecipeDetails = ({ recipe }) => {
RecipeDetails.propTypes = {
recipe: PropTypes.object,
- topics: PropTypes.object,
- tags: PropTypes.object,
- description: PropTypes.string,
- label: PropTypes.string,
- to: PropTypes.string,
- videoUrl: PropTypes.string,
- children: PropTypes.object,
- className: PropTypes.string,
- renditions: PropTypes.object,
- sections: PropTypes.object,
- list: PropTypes.object,
- name: PropTypes.string,
};
export default RecipeDetails;
diff --git a/src/features/recipes/components/recipe-details/RecipeDifficultyCard.jsx b/src/features/recipes/components/recipe-details/RecipeDifficultyCard.jsx
new file mode 100644
index 0000000..cf03af9
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/RecipeDifficultyCard.jsx
@@ -0,0 +1,61 @@
+import PropTypes from "prop-types";
+import ChefCarrots from "@/assets/brand/chef-carrots/ChefCarrots";
+import RecipeDifficultyDetail from "./RecipeDifficultyDetail";
+
+export const RecipeDifficultyCard = ({ tags }) => {
+ // Available cards:
+ // Difficulty: easy
+ // Time: under_15_minutes || under_30_minutes || under_45_minutes || under_1_hour
+ // Complexity: 5_ingredients_or_less
+
+ const under15Minutes = tags.find((tag) => tag.name === "under_15_minutes");
+ const under30Minutes = tags.find((tag) => tag.name === "under_30_minutes");
+ const under45Minutes = tags.find((tag) => tag.name === "under_45_minutes");
+ const under1Hour = tags.find((tag) => tag.name === "under_1_hour");
+
+ const easy = tags.find(
+ (tag) => tag.type === "difficulty" && tag.name === "easy",
+ );
+
+ let quickText = "";
+
+ if (under15Minutes) quickText = "Under 15 Minutes";
+ if (under30Minutes) quickText = "Under 30 Minutes";
+ if (under45Minutes) quickText = "Under 45 Minutes";
+ if (under1Hour) quickText = "Under 1 Hour";
+
+ const simple = tags.find(
+ (tag) => tag.type === "difficulty" && tag.name === "5_ingredients_or_less",
+ );
+ return (
+
+
+
+ {easy && (
+
+ )}
+ {quickText && (
+
+ )}
+ {simple && (
+
+ )}
+
+
+ );
+};
+
+RecipeDifficultyCard.propTypes = {
+ tags: PropTypes.array,
+};
diff --git a/src/features/recipes/components/recipe-details/RecipeDifficultyDetail.jsx b/src/features/recipes/components/recipe-details/RecipeDifficultyDetail.jsx
new file mode 100644
index 0000000..229030c
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/RecipeDifficultyDetail.jsx
@@ -0,0 +1,30 @@
+import { Heading, Icon } from "@/features/ui";
+import PropTypes from "prop-types";
+
+const RecipeDifficultyDetail = ({ title, icon, text }) => {
+ const className =
+ "sm:border-r-2 " +
+ "md:border-r-0 " +
+ "lg:border-r-2 " +
+ "last:border-r-0 " +
+ "border-dotted " +
+ "border-lava-300 " +
+ "px-2 md:px-0 " +
+ "lg:px-2 ";
+ return (
+
+ );
+};
+
+export default RecipeDifficultyDetail;
+
+RecipeDifficultyDetail.propTypes = {
+ title: PropTypes.string,
+ icon: PropTypes.string,
+ text: PropTypes.string,
+};
diff --git a/src/features/recipes/components/recipe-details/RecipeImage.jsx b/src/features/recipes/components/recipe-details/RecipeImage.jsx
new file mode 100644
index 0000000..8df0c91
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/RecipeImage.jsx
@@ -0,0 +1,30 @@
+import PropTypes from "prop-types";
+
+export const RecipeImage = ({ src, alt, description }) => {
+ let wrapperClasses = "";
+ let pictureClasses = "";
+ if (description) {
+ wrapperClasses +=
+ "picture-wrapper -mt-6 mb-4 box-content flex justify-center overflow-visible md:rotate-12 md:rounded-full md:border-[.5rem] border-b-[.3rem] border-dashed border-tangerine-500 pb-6 md:-mr-20 md:-mt-16 md:h-[17rem] md:w-[17rem] md:p-3 xl:h-[25rem] xl:max-h-[25vw] xl:w-[25rem] xl:max-w-[25vw]";
+ pictureClasses +=
+ "-mx-6 block h-full w-full overflow-clip rounded-t-xl object-cover md:rounded-full md:shadow-2xl";
+ } else {
+ wrapperClasses += "";
+ pictureClasses += "";
+ }
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+RecipeImage.propTypes = {
+ src: PropTypes.string,
+ alt: PropTypes.string,
+ description: PropTypes.string,
+};
diff --git a/src/features/recipes/components/recipe-details/RecipeVideo.jsx b/src/features/recipes/components/recipe-details/RecipeVideo.jsx
new file mode 100644
index 0000000..874b343
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/RecipeVideo.jsx
@@ -0,0 +1,20 @@
+import PropTypes from "prop-types";
+
+export const RecipeVideo = ({ videoUrl, renditions, ...attributes }) => {
+ if (!videoUrl) return null;
+ const sources = Object.values(renditions).map((src, index) => {
+ return
;
+ });
+ return (
+
+
+ {sources}
+
+
+ );
+};
+
+RecipeVideo.propTypes = {
+ videoUrl: PropTypes.string,
+ renditions: PropTypes.array,
+};
diff --git a/src/features/recipes/components/recipe-details/Section.jsx b/src/features/recipes/components/recipe-details/Section.jsx
new file mode 100644
index 0000000..225f38b
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Section.jsx
@@ -0,0 +1,46 @@
+import PropTypes from "prop-types";
+import { Heading } from "@/features/ui";
+
+// Section is the term the Tasty api uses for an individual ingredient for a recipe component
+export const Section = ({ list, name }) => {
+ const components = list.map((component, index) => {
+ const quantity = component.measurements[0].quantity;
+ const unit = component.measurements[0].unit;
+ const isPlural = quantity > 1 ? true : false;
+ const measurement = (
+
+
+ {quantity && quantity !== "0" ? quantity + " " : "-"}
+
+
+ {isPlural ? unit.display_plural + " " : unit.display_singular + " "}
+
+
+ );
+ const name =
+ isPlural && unit.display_plural === ""
+ ? component.ingredient.display_plural + " "
+ : component.ingredient.display_singular + " ";
+ const note = component.extra_comment ? ` (${component.extra_comment})` : "";
+ return (
+
+ {measurement}
+ {name}
+ {note}
+
+ );
+ });
+ return (
+
+
+ {name || "You'll Need:"}
+
+
+
+ );
+};
+
+Section.propTypes = {
+ list: PropTypes.array,
+ name: PropTypes.string,
+};
diff --git a/src/features/recipes/components/recipe-details/Tags.jsx b/src/features/recipes/components/recipe-details/Tags.jsx
new file mode 100644
index 0000000..dcb7db7
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Tags.jsx
@@ -0,0 +1,37 @@
+import PropTypes from "prop-types";
+import { Link } from "react-router-dom";
+
+export const Tags = ({ tags }) => {
+ const filteredCategories = [
+ "equipment",
+ "appliance",
+ "difficulty",
+ "business_tags",
+ ];
+ const displayTags = tags
+ .filter((tag) => !filteredCategories.includes(tag.type))
+ .slice(0, 20);
+ if (displayTags.length === 0) return null;
+ return (
+
+ {displayTags.map((tag, index) => {
+ return (
+
+
+ {tag.display_name}
+
+ {index !== displayTags.length - 1 ? ", " : ""}
+
+ );
+ })}
+
+ );
+};
+
+Tags.propTypes = {
+ tags: PropTypes.array,
+};
diff --git a/src/features/recipes/components/recipe-details/Tips.jsx b/src/features/recipes/components/recipe-details/Tips.jsx
new file mode 100644
index 0000000..7d82b76
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Tips.jsx
@@ -0,0 +1,93 @@
+import PropTypes from "prop-types";
+import { FetchTipsById } from "../../api";
+import { LoadingState } from "@/features/ui";
+import { Carousel } from "react-responsive-carousel";
+import "react-responsive-carousel/lib/styles/carousel.min.css";
+
+export const Tips = ({ recipeId }) => {
+ const { data: tips, isLoading, isError, error } = FetchTipsById(recipeId);
+
+ if (isLoading) {
+ return
;
+ }
+
+ if (isError) {
+ return
Error: {error.message}
;
+ }
+
+ return (
+
+ {tips.map((tip) => (
+
+
+
+ ))}
+
+ );
+};
+
+const TipCard = ({
+ tip_body,
+ author_name,
+ author_username,
+ author_avatar_url,
+ updated_at,
+}) => {
+ const RenderYearsSinceTimeStamp = (timeStamp) => {
+ const timeStampDate = new Date(timeStamp * 1000);
+ const currentDate = new Date();
+ const differenceInMilliseconds = currentDate - timeStampDate;
+ const differenceInYears =
+ differenceInMilliseconds / (1000 * 60 * 60 * 24 * 365);
+ const fullYears = Math.round(differenceInYears);
+
+ return `${fullYears} years ago`;
+ };
+
+ return (
+
+
+
+
+
+
+ {author_name ? author_name : author_username}
+
+
+
+
+
{RenderYearsSinceTimeStamp(updated_at)}
+
+
+ );
+};
+Tips.propTypes = {
+ recipeId: PropTypes.number,
+};
+
+TipCard.propTypes = {
+ tip_body: PropTypes.string,
+ author_avatar_url: PropTypes.string,
+ author_name: PropTypes.string,
+ author_username: PropTypes.string,
+ updated_at: PropTypes.number,
+};
diff --git a/src/features/recipes/components/recipe-details/Topics.jsx b/src/features/recipes/components/recipe-details/Topics.jsx
new file mode 100644
index 0000000..b80141d
--- /dev/null
+++ b/src/features/recipes/components/recipe-details/Topics.jsx
@@ -0,0 +1,29 @@
+import PropTypes from "prop-types";
+import { Link } from "react-router-dom";
+
+export const Topics = ({ topics }) => {
+ if (topics.length === 0) return null;
+ return (
+
+ Topics:
+ {topics.map((topic, index) => {
+ return (
+ <>
+
+ {topic.name}
+
+ {index !== topics.length - 1 ? ", " : ""}
+ >
+ );
+ })}
+
+ );
+};
+
+Topics.propTypes = {
+ topics: PropTypes.object,
+};
diff --git a/src/features/recipes/components/recipe-link-card/RecipeLinkCard.jsx b/src/features/recipes/components/recipe-link-card/RecipeLinkCard.jsx
index 77f07b0..401ef31 100644
--- a/src/features/recipes/components/recipe-link-card/RecipeLinkCard.jsx
+++ b/src/features/recipes/components/recipe-link-card/RecipeLinkCard.jsx
@@ -39,7 +39,7 @@ export const RecipeLinkCard = ({
-
+
{text}
diff --git a/src/features/recipes/components/recipe-list/recipe-list.jsx b/src/features/recipes/components/recipe-list/recipe-list.jsx
index c26c5da..977002d 100644
--- a/src/features/recipes/components/recipe-list/recipe-list.jsx
+++ b/src/features/recipes/components/recipe-list/recipe-list.jsx
@@ -1,11 +1,99 @@
+// eslint-disable-next-line react-hooks/exhaustive-deps
+
import PropTypes from "prop-types";
+import { useState, useEffect, useMemo } from "react";
import { RecipeCard } from "../recipe-card";
import { FetchRecipes } from "../../api";
+import { Filters } from "../filters";
+import { LoadingState, Heading } from "@/features/ui";
+import { Navigate } from "react-router-dom";
+import { BiPlus, BiMinus } from "react-icons/bi";
+const allowedTagTypes = [
+ "difficulty",
+ "meal",
+ "occasion",
+ "dietary",
+ "cuisine",
+ "cooking_style",
+];
export const RecipeList = ({ searchTerm }) => {
const { data: recipes, isLoading, isError, error } = FetchRecipes(searchTerm);
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [isOpen, setIsOpen] = useState(false);
+ // State to store the tags available in the fetched recipes
+ const [tagsCollection, setTagsCollection] = useState(() =>
+ allowedTagTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),
+ );
+
+ // This effect will run once on initial render to initialize tagsCollection
+ useEffect(() => {
+ if (recipes && recipes.results) {
+ const newTagsCollection = allowedTagTypes.reduce((collection, type) => {
+ collection[type] = recipes.results
+ .flatMap((recipe) => recipe.tags || [])
+ .filter((tag) => tag && tag.type === type)
+ .reduce((uniqueTags, tag) => {
+ if (!uniqueTags.some((uniqueTag) => uniqueTag.id === tag.id)) {
+ uniqueTags.push(tag);
+ }
+ return uniqueTags;
+ }, []);
+
+ return collection;
+ }, {});
+
+ setTagsCollection(newTagsCollection);
+ }
+ }, [recipes]); // Depends on 'recipes' to re-run if recipes change
+
+ const filteredRecipes =
+ selectedTags.length > 0
+ ? recipes.results.filter((recipe) => {
+ const recipeTags = recipe.tags || [];
+ const hasMatchingTag = recipeTags.some((tag) =>
+ selectedTags.some((selectedTag) => selectedTag.id === tag.id),
+ );
+
+ return hasMatchingTag;
+ })
+ : recipes?.results;
+
+ const displayedTags = useMemo(() => {
+ const filteredTags = {};
+
+ // Create a set of tag IDs present in the filtered recipes
+ const filteredTagIds = new Set(
+ (filteredRecipes || []).flatMap((recipe) =>
+ (recipe.tags || []).map((recipeTag) => recipeTag.id).filter(Boolean),
+ ),
+ );
+
+ // Iterate over the types of tags and filter them based on the selected tags and filtered recipes
+ Object.keys(tagsCollection).forEach((type) => {
+ filteredTags[type] = tagsCollection[type].filter(
+ (tag) =>
+ filteredTagIds.has(tag.id) ||
+ selectedTags.some((selectedTag) => selectedTag.id === tag.id),
+ );
+ });
+
+ // Remove the tag from the second type if it has already been added to the first type
+ const otherTypes = Object.keys(filteredTags).filter((t) => t !== t.type);
+ otherTypes.forEach((otherType) => {
+ const index = filteredTags[otherType].findIndex(
+ (tag) => tag.id === tag.id,
+ );
+ if (index !== -1) {
+ filteredTags[otherType].splice(index, 1);
+ }
+ });
+
+ return filteredTags;
+ }, [selectedTags, filteredRecipes, tagsCollection]);
+
if (isLoading) {
- return
Loading...
;
+ return
;
}
if (isError) {
@@ -18,16 +106,66 @@ export const RecipeList = ({ searchTerm }) => {
recipes.results.length === 0
) {
console.log(!recipes);
- console.log(Array.isArray(recipes.results));
- return
No recipes found
;
+ console.log(!Array.isArray(recipes.results));
+ console.log(recipes.results.length === 0);
+ return
;
}
+ const handleTagClick = (clickedTag) => {
+ setSelectedTags((currentSelectedTags) => {
+ // Check if the tag is already selected
+ const isSelected = currentSelectedTags.some(
+ (tag) => tag.id === clickedTag.id,
+ );
+ if (isSelected) {
+ // If the tag is already selected, remove it from the array
+ return currentSelectedTags.filter((tag) => tag.id !== clickedTag.id);
+ } else {
+ // If the tag is not selected, add it to the array
+ return [...currentSelectedTags, clickedTag];
+ }
+ });
+ };
+
return (
-
- {recipes.results.map((recipe) => (
-
- ))}
-
+ <>
+
+
+
+ {searchTerm}
+
+
+ {`${filteredRecipes.length} recipes`}
+
+
+
setIsOpen(!isOpen)}
+ >
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+ refine
+
+
+ {isOpen && (
+
+ )}
+
+ {filteredRecipes &&
+ filteredRecipes.map((recipe) => (
+
+ ))}
+
+ >
);
};
diff --git a/src/features/ui/Header/Header.jsx b/src/features/ui/Header/Header.jsx
index de147dc..380f9b5 100644
--- a/src/features/ui/Header/Header.jsx
+++ b/src/features/ui/Header/Header.jsx
@@ -7,8 +7,6 @@ import { Fragment } from "react";
import { Link } from "react-router-dom";
export const Header = () => {
- // const [toggle, setToggle] = useState(false);
-
return (
@@ -33,11 +31,6 @@ export const Header = () => {
{({ open }) => (
<>
- {/* setToggle(!toggle)}
- /> */}
{
leaveTo="transform scale-95 opacity-0"
>
{open && (
-
+
{navLinks.map((link) => (
@@ -64,7 +57,7 @@ export const Header = () => {
active
? "bg-sky-400 text-white"
: "bg-sky-300 text-white"
- } font-bold uppercase px-4`}
+ } font-bold uppercase px-10 py-3`}
>
{link.title}
diff --git a/src/features/ui/Heading/Heading.jsx b/src/features/ui/Heading/Heading.jsx
index 9ee54e4..48723af 100644
--- a/src/features/ui/Heading/Heading.jsx
+++ b/src/features/ui/Heading/Heading.jsx
@@ -19,7 +19,7 @@ export const Heading = (props) => {
break;
case "h4":
// textSizes = "text-[2rem] md:text-[2.5rem] xl:text-[3rem]";
- textSizes = "text-clamp-h4 text-shadow-sm";
+ textSizes = "text-clamp-h4 text-shadow-sm text-stroke-sm";
break;
default:
break;
diff --git a/src/features/ui/index.js b/src/features/ui/index.js
index 900d79d..4ca98f2 100644
--- a/src/features/ui/index.js
+++ b/src/features/ui/index.js
@@ -8,3 +8,4 @@ export * from "./Button";
export * from "./Picture";
export * from "./LoadingState";
export * from "../HomePage";
+export * from "../PageNotFound";
diff --git a/src/hooks/usePageTitle.js b/src/hooks/usePageTitle.js
new file mode 100644
index 0000000..70af4ce
--- /dev/null
+++ b/src/hooks/usePageTitle.js
@@ -0,0 +1,10 @@
+import { useEffect } from "react";
+import { name } from "@/constants";
+
+const usePageTitle = (title) => {
+ useEffect(() => {
+ document.title = title ? `${title} | ${name}` : `${name} Recipes`;
+ }, [title]);
+};
+
+export default usePageTitle;
diff --git a/src/main.jsx b/src/main.jsx
index 4762d38..a5a4064 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -18,3 +18,10 @@ ReactDOM.createRoot(document.getElementById("root")).render(
,
);
+if (process.env.NODE_ENV !== "development") {
+ console.log = function () {};
+ console.debug = function () {};
+ console.info = function () {};
+ console.warn = function () {};
+ console.error = function () {};
+}
diff --git a/src/mirageServer/endpoints/recipes/get-more-info.json b/src/mirageServer/endpoints/recipes/get-more-info.json
new file mode 100644
index 0000000..7e11a17
--- /dev/null
+++ b/src/mirageServer/endpoints/recipes/get-more-info.json
@@ -0,0 +1,886 @@
+{
+ "total_time_tier": {
+ "tier": "under_30_minutes",
+ "display_tier": "Under 30 minutes"
+ },
+ "buzz_id": null,
+ "total_time_minutes": 20,
+ "created_at": 1644534455,
+ "promotion": "full",
+ "updated_at": 1645052400,
+ "video_url": "https://vid.tasty.co/output/215487/hls24_1631150838.m3u8",
+ "brand": null,
+ "video_ad_content": "none",
+ "aspect_ratio": "16:9",
+ "topics": [
+ {
+ "name": "Best Vegetarian",
+ "slug": "best-vegetarian"
+ },
+ {
+ "name": "Bread Lovers",
+ "slug": "bread"
+ },
+ {
+ "name": "Sunday Brunch",
+ "slug": "brunch"
+ },
+ {
+ "name": "Breakfast",
+ "slug": "breakfast"
+ },
+ {
+ "name": "American",
+ "slug": "american"
+ },
+ {
+ "name": "Mother's Day",
+ "slug": "mothers-day"
+ }
+ ],
+ "language": "eng",
+ "cook_time_minutes": 10,
+ "servings_noun_plural": "servings",
+ "compilations": [],
+ "sections": [
+ {
+ "position": 1,
+ "components": [
+ {
+ "ingredient": {
+ "display_singular": "large egg",
+ "display_plural": "large eggs",
+ "created_at": 1494382414,
+ "updated_at": 1509035275,
+ "id": 253,
+ "name": "large egg"
+ },
+ "id": 92487,
+ "raw_text": "6 large eggs",
+ "extra_comment": "",
+ "position": 2,
+ "measurements": [
+ {
+ "id": 682360,
+ "quantity": "6",
+ "unit": {
+ "name": "",
+ "abbreviation": "",
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none"
+ }
+ }
+ ]
+ },
+ {
+ "id": 92488,
+ "raw_text": "¾ cup whole milk",
+ "extra_comment": "",
+ "position": 3,
+ "measurements": [
+ {
+ "id": 682357,
+ "quantity": "¾",
+ "unit": {
+ "name": "cup",
+ "abbreviation": "c",
+ "display_singular": "cup",
+ "display_plural": "cups",
+ "system": "imperial"
+ }
+ },
+ {
+ "unit": {
+ "abbreviation": "mL",
+ "display_singular": "mL",
+ "display_plural": "mL",
+ "system": "metric",
+ "name": "milliliter"
+ },
+ "id": 682355,
+ "quantity": "180"
+ }
+ ],
+ "ingredient": {
+ "id": 770,
+ "name": "whole milk",
+ "display_singular": "whole milk",
+ "display_plural": "whole milks",
+ "created_at": 1495732941,
+ "updated_at": 1509035235
+ }
+ },
+ {
+ "id": 92489,
+ "raw_text": "¾ cup heavy cream",
+ "extra_comment": "",
+ "position": 4,
+ "measurements": [
+ {
+ "id": 682354,
+ "quantity": "¾",
+ "unit": {
+ "name": "cup",
+ "abbreviation": "c",
+ "display_singular": "cup",
+ "display_plural": "cups",
+ "system": "imperial"
+ }
+ },
+ {
+ "unit": {
+ "name": "milliliter",
+ "abbreviation": "mL",
+ "display_singular": "mL",
+ "display_plural": "mL",
+ "system": "metric"
+ },
+ "id": 682351,
+ "quantity": "180"
+ }
+ ],
+ "ingredient": {
+ "id": 221,
+ "name": "heavy cream",
+ "display_singular": "heavy cream",
+ "display_plural": "heavy creams",
+ "created_at": 1494214054,
+ "updated_at": 1509035278
+ }
+ },
+ {
+ "raw_text": "2–4 tablespoons light brown sugar",
+ "extra_comment": "",
+ "position": 5,
+ "measurements": [
+ {
+ "unit": {
+ "abbreviation": "tbsp",
+ "display_singular": "tablespoon",
+ "display_plural": "tablespoons",
+ "system": "imperial",
+ "name": "tablespoon"
+ },
+ "id": 682359,
+ "quantity": "2"
+ }
+ ],
+ "ingredient": {
+ "name": "light brown sugar",
+ "display_singular": "light brown sugar",
+ "display_plural": "light brown sugars",
+ "created_at": 1495671124,
+ "updated_at": 1509035239,
+ "id": 707
+ },
+ "id": 92490
+ },
+ {
+ "raw_text": "½ teaspoon kosher salt",
+ "extra_comment": "",
+ "position": 6,
+ "measurements": [
+ {
+ "id": 682353,
+ "quantity": "½",
+ "unit": {
+ "name": "teaspoon",
+ "abbreviation": "tsp",
+ "display_singular": "teaspoon",
+ "display_plural": "teaspoons",
+ "system": "imperial"
+ }
+ }
+ ],
+ "ingredient": {
+ "updated_at": 1509035289,
+ "id": 11,
+ "name": "kosher salt",
+ "display_singular": "kosher salt",
+ "display_plural": "kosher salts",
+ "created_at": 1493307153
+ },
+ "id": 92491
+ },
+ {
+ "measurements": [
+ {
+ "id": 682349,
+ "quantity": "1",
+ "unit": {
+ "display_plural": "teaspoons",
+ "system": "imperial",
+ "name": "teaspoon",
+ "abbreviation": "tsp",
+ "display_singular": "teaspoon"
+ }
+ }
+ ],
+ "ingredient": {
+ "created_at": 1494985113,
+ "updated_at": 1509035263,
+ "id": 407,
+ "name": "ground cinnamon",
+ "display_singular": "ground cinnamon",
+ "display_plural": "ground cinnamons"
+ },
+ "id": 92492,
+ "raw_text": "1 teaspoon ground cinnamon",
+ "extra_comment": "",
+ "position": 7
+ },
+ {
+ "id": 92493,
+ "raw_text": "1 teaspoon vanilla bean paste or extract",
+ "extra_comment": "or extract",
+ "position": 8,
+ "measurements": [
+ {
+ "id": 682352,
+ "quantity": "1",
+ "unit": {
+ "system": "imperial",
+ "name": "teaspoon",
+ "abbreviation": "tsp",
+ "display_singular": "teaspoon",
+ "display_plural": "teaspoons"
+ }
+ }
+ ],
+ "ingredient": {
+ "created_at": 1527513590,
+ "updated_at": 1527513590,
+ "id": 4181,
+ "name": "vanilla bean paste",
+ "display_singular": "vanilla bean paste",
+ "display_plural": "vanilla bean pastes"
+ }
+ },
+ {
+ "extra_comment": "",
+ "position": 9,
+ "measurements": [
+ {
+ "id": 682356,
+ "quantity": "1",
+ "unit": {
+ "system": "none",
+ "name": "loaf",
+ "abbreviation": "loaf",
+ "display_singular": "loaf",
+ "display_plural": "loaves"
+ }
+ }
+ ],
+ "ingredient": {
+ "id": 9565,
+ "name": "challah bread",
+ "display_singular": "challah bread",
+ "display_plural": "challah breads",
+ "created_at": 1644586974,
+ "updated_at": 1644586974
+ },
+ "id": 92494,
+ "raw_text": "1 loaf of day-old challah bread, sliced 1–1½-inch-thick"
+ },
+ {
+ "raw_text": "Unsalted butter or ghee, for greasing",
+ "extra_comment": "or ghee, for greasing",
+ "position": 10,
+ "measurements": [
+ {
+ "id": 682347,
+ "quantity": "0",
+ "unit": {
+ "name": "",
+ "abbreviation": "",
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none"
+ }
+ }
+ ],
+ "ingredient": {
+ "updated_at": 1509035272,
+ "id": 291,
+ "name": "unsalted butter",
+ "display_singular": "unsalted butter",
+ "display_plural": "unsalted butters",
+ "created_at": 1494806355
+ },
+ "id": 92495
+ }
+ ],
+ "name": "French Toast"
+ },
+ {
+ "components": [
+ {
+ "id": 92497,
+ "raw_text": "Softened butter",
+ "extra_comment": "",
+ "position": 12,
+ "measurements": [
+ {
+ "id": 682358,
+ "quantity": "0",
+ "unit": {
+ "display_plural": "",
+ "system": "none",
+ "name": "",
+ "abbreviation": "",
+ "display_singular": ""
+ }
+ }
+ ],
+ "ingredient": {
+ "id": 4160,
+ "name": "softened butter",
+ "display_singular": "softened butter",
+ "display_plural": "softened butters",
+ "created_at": 1527026898,
+ "updated_at": 1527026898
+ }
+ },
+ {
+ "measurements": [
+ {
+ "unit": {
+ "abbreviation": "",
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none",
+ "name": ""
+ },
+ "id": 682348,
+ "quantity": "0"
+ }
+ ],
+ "ingredient": {
+ "id": 2477,
+ "name": "pure maple syrup",
+ "display_singular": "pure maple syrup",
+ "display_plural": "pure maple syrups",
+ "created_at": 1500699508,
+ "updated_at": 1509035127
+ },
+ "id": 92498,
+ "raw_text": "Pure maple syrup, warmed",
+ "extra_comment": "warmed",
+ "position": 13
+ },
+ {
+ "id": 92499,
+ "raw_text": "Flaky sea salt",
+ "extra_comment": "",
+ "position": 14,
+ "measurements": [
+ {
+ "quantity": "0",
+ "unit": {
+ "name": "",
+ "abbreviation": "",
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none"
+ },
+ "id": 682350
+ }
+ ],
+ "ingredient": {
+ "id": 3099,
+ "name": "flaky sea salt",
+ "display_singular": "flaky sea salt",
+ "display_plural": "flaky sea salts",
+ "created_at": 1507925704,
+ "updated_at": 1509035088
+ }
+ },
+ {
+ "id": 92500,
+ "raw_text": "Fresh berries",
+ "extra_comment": "",
+ "position": 15,
+ "measurements": [
+ {
+ "id": 682346,
+ "quantity": "0",
+ "unit": {
+ "name": "",
+ "abbreviation": "",
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none"
+ }
+ }
+ ],
+ "ingredient": {
+ "name": "fresh berries",
+ "display_singular": "fresh berry",
+ "display_plural": "fresh berries",
+ "created_at": 1500596638,
+ "updated_at": 1509035130,
+ "id": 2369
+ }
+ }
+ ],
+ "name": "For Serving",
+ "position": 2
+ }
+ ],
+ "tags": [
+ {
+ "display_name": "North American",
+ "type": "cuisine",
+ "root_tag_type": "cuisine",
+ "id": 64444,
+ "name": "north_american"
+ },
+ {
+ "id": 64449,
+ "name": "french",
+ "display_name": "French",
+ "type": "european",
+ "root_tag_type": "cuisine"
+ },
+ {
+ "type": "cooking_style",
+ "root_tag_type": "cooking_style",
+ "id": 64462,
+ "name": "comfort_food",
+ "display_name": "Comfort Food"
+ },
+ {
+ "id": 64469,
+ "name": "vegetarian",
+ "display_name": "Vegetarian",
+ "type": "dietary",
+ "root_tag_type": "dietary"
+ },
+ {
+ "id": 64471,
+ "name": "easy",
+ "display_name": "Easy",
+ "type": "difficulty",
+ "root_tag_type": "difficulty"
+ },
+ {
+ "id": 64472,
+ "name": "under_30_minutes",
+ "display_name": "Under 30 Minutes",
+ "type": "difficulty",
+ "root_tag_type": "difficulty"
+ },
+ {
+ "name": "valentines_day",
+ "display_name": "Valentine's Day",
+ "type": "holidays",
+ "root_tag_type": "seasonal",
+ "id": 64480
+ },
+ {
+ "name": "breakfast",
+ "display_name": "Breakfast",
+ "type": "meal",
+ "root_tag_type": "meal",
+ "id": 64483
+ },
+ {
+ "type": "meal",
+ "root_tag_type": "meal",
+ "id": 64484,
+ "name": "brunch",
+ "display_name": "Brunch"
+ },
+ {
+ "id": 64505,
+ "name": "weeknight",
+ "display_name": "Weeknight",
+ "type": "dinner",
+ "root_tag_type": "meal"
+ },
+ {
+ "root_tag_type": "appliance",
+ "id": 65848,
+ "name": "stove_top",
+ "display_name": "Stove Top",
+ "type": "appliance"
+ },
+ {
+ "display_name": "Big Batch",
+ "type": "cooking_style",
+ "root_tag_type": "cooking_style",
+ "id": 65851,
+ "name": "big_batch"
+ },
+ {
+ "display_name": "Special Occasion",
+ "type": "occasion",
+ "root_tag_type": "seasonal",
+ "id": 188967,
+ "name": "special_occasion"
+ },
+ {
+ "id": 1247785,
+ "name": "pyrex",
+ "display_name": "Pyrex",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "id": 1247788,
+ "name": "spatula",
+ "display_name": "Spatula",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "id": 1247790,
+ "name": "tongs",
+ "display_name": "Tongs",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "id": 1247793,
+ "name": "whisk",
+ "display_name": "Whisk",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "root_tag_type": "equipment",
+ "id": 1280506,
+ "name": "liquid_measuring_cup",
+ "display_name": "Liquid Measuring Cup",
+ "type": "equipment"
+ },
+ {
+ "id": 1280508,
+ "name": "measuring_spoons",
+ "display_name": "Measuring Spoons",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "id": 1280510,
+ "name": "mixing_bowl",
+ "display_name": "Mixing Bowl",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "type": "holidays",
+ "root_tag_type": "seasonal",
+ "id": 6854262,
+ "name": "mothers_day",
+ "display_name": "Mother's Day"
+ },
+ {
+ "display_name": "Under 45 Minutes",
+ "type": "difficulty",
+ "root_tag_type": "difficulty",
+ "id": 8091747,
+ "name": "under_45_minutes"
+ },
+ {
+ "root_tag_type": "difficulty",
+ "id": 8091748,
+ "name": "under_1_hour",
+ "display_name": "Under 1 Hour",
+ "type": "difficulty"
+ },
+ {
+ "display_name": "Country Crock Brunch",
+ "type": "feature_page",
+ "root_tag_type": "feature_page",
+ "id": 9104327,
+ "name": "country_crock_brunch"
+ },
+ {
+ "root_tag_type": "meal",
+ "id": 9299364,
+ "name": "sweet_breakfasts",
+ "display_name": "Sweet Breakfasts",
+ "type": "breakfast"
+ }
+ ],
+ "price": {
+ "portion": 750,
+ "consumption_total": 700,
+ "consumption_portion": 200,
+ "updated_at": "2023-11-05T06:27:03+01:00",
+ "total": 2900
+ },
+ "keywords": "",
+ "seo_title": "",
+ "thumbnail_url": "https://img.buzzfeed.com/thumbnailer-prod-us-east-1/video-api/assets/341495.jpg",
+ "show": {
+ "id": 63,
+ "name": "Tasty 101"
+ },
+ "yields": "Servings: 4",
+ "beauty_url": null,
+ "brand_id": null,
+ "servings_noun_singular": "serving",
+ "show_id": 63,
+ "description": "This iconic dish is all about the details. While french toast might seem simple, looks can be deceiving. To get that perfectly crispy exterior and creamy, silky custard on the inside we spent weeks eating tons of butter, bread, and syrup to discover the best classic french toast recipe. The end result is indulgent, delicious, and most importantly, easy to make. What’s not to love?",
+ "is_one_top": false,
+ "credits": [
+ {
+ "type": "internal",
+ "name": "Katie Aubin"
+ },
+ {
+ "type": "internal",
+ "name": "Codii Lopez"
+ },
+ {
+ "name": "Kelly Paige",
+ "type": "internal"
+ }
+ ],
+ "is_shoppable": true,
+ "prep_time_minutes": 5,
+ "inspired_by_url": null,
+ "is_subscriber_content": false,
+ "tips_and_ratings_enabled": true,
+ "original_video_url": "https://s3.amazonaws.com/video-api-prod/assets/42109c902ae449bda59cebafa04745ca/BFV81893_FrenchToast_ADB_090821_Final_16x9_YT.mp4",
+ "draft_status": "published",
+ "nutrition_visibility": "auto",
+ "facebook_posts": [],
+ "thumbnail_alt_text": "101",
+ "nutrition": {
+ "calories": 426,
+ "carbohydrates": 25,
+ "fat": 31,
+ "protein": 17,
+ "sugar": 11,
+ "fiber": 0,
+ "updated_at": "2022-02-12T07:09:37+01:00"
+ },
+ "approved_at": 1645125797,
+ "id": 8110,
+ "name": "How To Make Classic French Toast",
+ "seo_path": "8757513,9295874,64449",
+ "slug": "how-to-make-classic-french-toast",
+ "renditions": [
+ {
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/square_720/1631150838_00001.png",
+ "name": "mp4_720x404",
+ "width": 720,
+ "duration": 353896,
+ "bit_rate": 1243,
+ "minimum_bit_rate": null,
+ "content_type": "video/mp4",
+ "url": "https://vid.tasty.co/output/215487/square_720/1631150838",
+ "height": 404,
+ "file_size": 54947396,
+ "maximum_bit_rate": null,
+ "aspect": "landscape",
+ "container": "mp4"
+ },
+ {
+ "maximum_bit_rate": null,
+ "minimum_bit_rate": null,
+ "content_type": "video/mp4",
+ "aspect": "landscape",
+ "name": "mp4_320x180",
+ "height": 180,
+ "width": 320,
+ "bit_rate": 444,
+ "container": "mp4",
+ "url": "https://vid.tasty.co/output/215487/square_320/1631150838",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/square_320/1631150838_00001.png",
+ "duration": 353896,
+ "file_size": 19632223
+ },
+ {
+ "minimum_bit_rate": null,
+ "content_type": "video/mp4",
+ "container": "mp4",
+ "name": "mp4_720x404",
+ "height": 404,
+ "bit_rate": 1243,
+ "maximum_bit_rate": null,
+ "aspect": "landscape",
+ "url": "https://vid.tasty.co/output/215487/square_720/1631150838",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/square_720/1631150838_00001.png",
+ "width": 720,
+ "duration": 353896,
+ "file_size": 54947396
+ },
+ {
+ "name": "mp4_1280x720",
+ "width": 1280,
+ "bit_rate": 2602,
+ "maximum_bit_rate": null,
+ "minimum_bit_rate": null,
+ "content_type": "video/mp4",
+ "url": "https://vid.tasty.co/output/215487/landscape_720/1631150838",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/landscape_720/1631150838_00001.png",
+ "height": 720,
+ "duration": 353896,
+ "file_size": 115083637,
+ "aspect": "landscape",
+ "container": "mp4"
+ },
+ {
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/square_320/1631150838_00001.png",
+ "width": 320,
+ "file_size": 19632223,
+ "bit_rate": 444,
+ "maximum_bit_rate": null,
+ "content_type": "video/mp4",
+ "container": "mp4",
+ "url": "https://vid.tasty.co/output/215487/square_320/1631150838",
+ "name": "mp4_320x180",
+ "height": 180,
+ "duration": 353896,
+ "minimum_bit_rate": null,
+ "aspect": "landscape"
+ },
+ {
+ "width": 640,
+ "file_size": 46802727,
+ "bit_rate": 1058,
+ "maximum_bit_rate": null,
+ "aspect": "landscape",
+ "container": "mp4",
+ "url": "https://vid.tasty.co/output/215487/landscape_480/1631150838",
+ "height": 360,
+ "duration": 353896,
+ "minimum_bit_rate": null,
+ "content_type": "video/mp4",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/landscape_480/1631150838_00001.png",
+ "name": "mp4_640x360"
+ },
+ {
+ "height": 720,
+ "duration": 353896,
+ "file_size": 115083637,
+ "bit_rate": 2602,
+ "minimum_bit_rate": null,
+ "url": "https://vid.tasty.co/output/215487/landscape_720/1631150838",
+ "name": "mp4_1280x720",
+ "width": 1280,
+ "maximum_bit_rate": null,
+ "content_type": "video/mp4",
+ "aspect": "landscape",
+ "container": "mp4",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/landscape_720/1631150838_00001.png"
+ },
+ {
+ "content_type": "video/mp4",
+ "container": "mp4",
+ "name": "mp4_640x360",
+ "height": 360,
+ "width": 640,
+ "bit_rate": 1058,
+ "maximum_bit_rate": null,
+ "minimum_bit_rate": null,
+ "aspect": "landscape",
+ "url": "https://vid.tasty.co/output/215487/landscape_480/1631150838",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/landscape_480/1631150838_00001.png",
+ "duration": 353896,
+ "file_size": 46802727
+ },
+ {
+ "height": 1080,
+ "maximum_bit_rate": 4581,
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/1445289064805-h2exzu/1631150838_00001.png",
+ "content_type": "application/vnd.apple.mpegurl",
+ "aspect": "landscape",
+ "container": "ts",
+ "width": 1920,
+ "duration": 353896,
+ "file_size": null,
+ "bit_rate": null,
+ "minimum_bit_rate": 269,
+ "url": "https://vid.tasty.co/output/215487/hls24_1631150838.m3u8",
+ "name": "low"
+ },
+ {
+ "duration": 353896,
+ "minimum_bit_rate": 269,
+ "url": "https://vid.tasty.co/output/215487/hls24_1631150838.m3u8",
+ "aspect": "landscape",
+ "container": "ts",
+ "height": 1080,
+ "width": 1920,
+ "file_size": null,
+ "bit_rate": null,
+ "maximum_bit_rate": 4581,
+ "content_type": "application/vnd.apple.mpegurl",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/215487/1445289064805-h2exzu/1631150838_00001.png",
+ "name": "low"
+ }
+ ],
+ "canonical_id": "recipe:8110",
+ "country": "US",
+ "is_app_only": false,
+ "num_servings": 4,
+ "video_id": 135115,
+ "instructions": [
+ {
+ "id": 70664,
+ "display_text": "Preheat a nonstick electric griddle to 300°F (150°C). (Alternatively, heat a large nonstick skillet over medium-low heat.)",
+ "position": 1,
+ "start_time": 0,
+ "end_time": 0,
+ "temperature": 300,
+ "appliance": "oven"
+ },
+ {
+ "start_time": 113000,
+ "end_time": 125833,
+ "temperature": null,
+ "appliance": null,
+ "id": 70665,
+ "display_text": "In a large bowl, whisk the eggs until well combined. Add the milk, heavy cream, brown sugar, salt, cinnamon, and vanilla bean paste and whisk until completely combined.",
+ "position": 2
+ },
+ {
+ "position": 3,
+ "start_time": 169000,
+ "end_time": 172000,
+ "temperature": null,
+ "appliance": null,
+ "id": 70666,
+ "display_text": "Working in batches, dip each slice of challah in the custard, letting soak for 20–60 seconds on each side, until fully saturated but not soggy."
+ },
+ {
+ "id": 70667,
+ "display_text": "Lightly grease the griddle with butter. Once melted, add the soaked challah on the griddle and cook, without disturbing, until golden brown and crispy, about 2 minutes. Use a flat spatula to flip the bread and continue cooking until the other side is golden brown, 2 minutes more. Wipe the pan clean between batches and add more butter as needed.",
+ "position": 4,
+ "start_time": 192500,
+ "end_time": 208000,
+ "temperature": null,
+ "appliance": null
+ },
+ {
+ "position": 5,
+ "start_time": 307000,
+ "end_time": 317500,
+ "temperature": null,
+ "appliance": null,
+ "id": 70668,
+ "display_text": "Serve the French toast with a pat of butter, a drizzle of warm maple syrup, a sprinkle of flaky salt, and berries."
+ },
+ {
+ "temperature": null,
+ "appliance": null,
+ "id": 70669,
+ "display_text": "Enjoy!",
+ "position": 6,
+ "start_time": 325000,
+ "end_time": 328666
+ }
+ ],
+ "user_ratings": {
+ "count_positive": 336,
+ "count_negative": 8,
+ "score": 0.976744
+ }
+}
diff --git a/src/mirageServer/endpoints/recipes/list.json b/src/mirageServer/endpoints/recipes/list.json
index 5850043..0b1e419 100644
--- a/src/mirageServer/endpoints/recipes/list.json
+++ b/src/mirageServer/endpoints/recipes/list.json
@@ -1,6 +1,899 @@
{
"count": 1518,
"results": [
+ {
+ "aspect_ratio": "1:1",
+ "id": 3194,
+ "inspired_by_url": null,
+ "brand": null,
+ "show_id": 34,
+ "created_at": 1513281355,
+ "description": "This one-pan roasted chicken and sweet potatoes recipe is a delicious and easy way to enjoy a flavorful and filling meal. With tender chicken, sweet potatoes, and flavorful seasoning, this recipe is perfect for a busy weeknight.",
+ "seo_title": null,
+ "video_id": 39588,
+ "yields": "Servings: 1",
+ "user_ratings": {
+ "count_negative": 105,
+ "score": 0.927436,
+ "count_positive": 1342
+ },
+ "credits": [
+ {
+ "name": "Mercedes Sandoval",
+ "type": "internal"
+ }
+ ],
+ "show": {
+ "id": 34,
+ "name": "Goodful"
+ },
+ "is_subscriber_content": false,
+ "renditions": [
+ {
+ "file_size": 46589372,
+ "minimum_bit_rate": null,
+ "content_type": "video/mp4",
+ "url": "https://vid.tasty.co/output/68224/square_720/1513303098",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/68224/square_720/1513303098_00001.png",
+ "name": "mp4_720x720",
+ "height": 720,
+ "duration": 186522,
+ "maximum_bit_rate": null,
+ "aspect": "square",
+ "container": "mp4",
+ "width": 720,
+ "bit_rate": 1999
+ },
+ {
+ "maximum_bit_rate": null,
+ "minimum_bit_rate": null,
+ "aspect": "square",
+ "container": "mp4",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/68224/square_320/1513303098_00001.png",
+ "name": "mp4_320x320",
+ "duration": 186522,
+ "bit_rate": 691,
+ "file_size": 16090829,
+ "content_type": "video/mp4",
+ "url": "https://vid.tasty.co/output/68224/square_320/1513303098",
+ "height": 320,
+ "width": 320
+ },
+ {
+ "minimum_bit_rate": null,
+ "container": "mp4",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/68224/landscape_720/1513303098_00001.png",
+ "width": 720,
+ "duration": 186522,
+ "file_size": 46551633,
+ "bit_rate": 1997,
+ "maximum_bit_rate": null,
+ "content_type": "video/mp4",
+ "aspect": "square",
+ "url": "https://vid.tasty.co/output/68224/landscape_720/1513303098",
+ "height": 720,
+ "name": "mp4_720x720"
+ },
+ {
+ "minimum_bit_rate": null,
+ "aspect": "square",
+ "name": "mp4_480x480",
+ "width": 480,
+ "file_size": 26962093,
+ "bit_rate": 1157,
+ "maximum_bit_rate": null,
+ "content_type": "video/mp4",
+ "container": "mp4",
+ "url": "https://vid.tasty.co/output/68224/landscape_480/1513303098",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/68224/landscape_480/1513303098_00001.png",
+ "height": 480,
+ "duration": 186522
+ },
+ {
+ "file_size": null,
+ "minimum_bit_rate": 272,
+ "content_type": "application/vnd.apple.mpegurl",
+ "url": "https://vid.tasty.co/output/68224/hls24_1513303098.m3u8",
+ "name": "low",
+ "height": 1080,
+ "duration": 186478,
+ "maximum_bit_rate": 3631,
+ "aspect": "square",
+ "container": "ts",
+ "poster_url": "https://img.buzzfeed.com/video-transcoder-prod/output/68224/1445289064805-h2exzu/1513303098_00001.png",
+ "width": 1080,
+ "bit_rate": null
+ }
+ ],
+ "buzz_id": 4707416,
+ "promotion": "full",
+ "tags": [
+ {
+ "id": 64444,
+ "name": "north_american",
+ "display_name": "North American",
+ "type": "cuisine",
+ "root_tag_type": "cuisine"
+ },
+ {
+ "id": 64462,
+ "name": "comfort_food",
+ "display_name": "Comfort Food",
+ "type": "cooking_style",
+ "root_tag_type": "cooking_style"
+ },
+ {
+ "display_name": "Dairy-Free",
+ "type": "dietary",
+ "root_tag_type": "dietary",
+ "id": 64463,
+ "name": "dairy_free"
+ },
+ {
+ "name": "gluten_free",
+ "display_name": "Gluten-Free",
+ "type": "dietary",
+ "root_tag_type": "dietary",
+ "id": 64465
+ },
+ {
+ "name": "healthy",
+ "display_name": "Healthy",
+ "type": "healthy",
+ "root_tag_type": "healthy",
+ "id": 64466
+ },
+ {
+ "id": 64471,
+ "name": "easy",
+ "display_name": "Easy",
+ "type": "difficulty",
+ "root_tag_type": "difficulty"
+ },
+ {
+ "name": "under_30_minutes",
+ "display_name": "Under 30 Minutes",
+ "type": "difficulty",
+ "root_tag_type": "difficulty",
+ "id": 64472
+ },
+ {
+ "root_tag_type": "meal",
+ "id": 64486,
+ "name": "dinner",
+ "display_name": "Dinner",
+ "type": "meal"
+ },
+ {
+ "type": "meal",
+ "root_tag_type": "meal",
+ "id": 64489,
+ "name": "lunch",
+ "display_name": "Lunch"
+ },
+ {
+ "id": 64492,
+ "name": "baking",
+ "display_name": "Baking",
+ "type": "appliance",
+ "root_tag_type": "appliance"
+ },
+ {
+ "root_tag_type": "meal",
+ "id": 64505,
+ "name": "weeknight",
+ "display_name": "Weeknight",
+ "type": "dinner"
+ },
+ {
+ "id": 65855,
+ "name": "one_pot_or_pan",
+ "display_name": "One-Pot or Pan",
+ "type": "cooking_style",
+ "root_tag_type": "cooking_style"
+ },
+ {
+ "type": "equipment",
+ "root_tag_type": "equipment",
+ "id": 1247775,
+ "name": "oven_mitts",
+ "display_name": "Oven Mitts"
+ },
+ {
+ "id": 1247780,
+ "name": "parchment_paper",
+ "display_name": "Parchment Paper",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "display_name": "Pyrex",
+ "type": "equipment",
+ "root_tag_type": "equipment",
+ "id": 1247785,
+ "name": "pyrex"
+ },
+ {
+ "id": 1247790,
+ "name": "tongs",
+ "display_name": "Tongs",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "id": 1280500,
+ "name": "baking_pan",
+ "display_name": "Baking Pan",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "id": 1280501,
+ "name": "chefs_knife",
+ "display_name": "Chef's Knife",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "id": 1280503,
+ "name": "cutting_board",
+ "display_name": "Cutting Board",
+ "type": "equipment",
+ "root_tag_type": "equipment"
+ },
+ {
+ "root_tag_type": "feature_page",
+ "id": 6986107,
+ "name": "mccormick_ugc_one_pot_others",
+ "display_name": "McCormick UGC One Pot Others",
+ "type": "feature_page"
+ },
+ {
+ "type": "healthy",
+ "root_tag_type": "healthy",
+ "id": 8091917,
+ "name": "high_protein",
+ "display_name": "High-Protein"
+ },
+ {
+ "name": "low_sugar",
+ "display_name": "Low-Sugar",
+ "type": "healthy",
+ "root_tag_type": "healthy",
+ "id": 8091918
+ },
+ {
+ "name": "low_fat",
+ "display_name": "Low-Fat",
+ "type": "healthy",
+ "root_tag_type": "healthy",
+ "id": 8091919
+ },
+ {
+ "name": "high_fiber",
+ "display_name": "High-Fiber",
+ "type": "healthy",
+ "root_tag_type": "healthy",
+ "id": 8091920
+ },
+ {
+ "id": 10089785,
+ "name": "walmart_meal_planning_october",
+ "display_name": "Walmart Meal Planning October",
+ "type": "business_tags",
+ "root_tag_type": "business_tags"
+ }
+ ],
+ "beauty_url": "https://img.buzzfeed.com/video-api-prod/assets/711a5ab95e2a49609b992bc5834da2cd/BFV34175_One-PanMealsUnder500Calories-BeautyShot.jpg",
+ "is_one_top": false,
+ "language": "eng",
+ "brand_id": null,
+ "servings_noun_plural": "servings",
+ "slug": "one-pan-roasted-chicken-and-sweet-potatoes",
+ "is_app_only": false,
+ "name": "One-Pan Roasted Chicken And Sweet Potatoes",
+ "seo_path": "9295813,64486,64505",
+ "servings_noun_singular": "serving",
+ "thumbnail_alt_text": "",
+ "approved_at": 1513564124,
+ "sections": [
+ {
+ "components": [
+ {
+ "extra_comment": "diced",
+ "position": 1,
+ "measurements": [
+ {
+ "id": 727563,
+ "quantity": "1",
+ "unit": {
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none",
+ "name": "",
+ "abbreviation": ""
+ }
+ }
+ ],
+ "ingredient": {
+ "name": "small sweet potato",
+ "display_singular": "small sweet potato",
+ "display_plural": "small sweet potatoes",
+ "created_at": 1496365459,
+ "updated_at": 1509035206,
+ "id": 1139
+ },
+ "id": 30504,
+ "raw_text": "1 small sweet potato, diced"
+ },
+ {
+ "id": 30505,
+ "raw_text": "1 lemon, sliced, seeds removed",
+ "extra_comment": "sliced, seeds removed",
+ "position": 2,
+ "measurements": [
+ {
+ "id": 727570,
+ "quantity": "1",
+ "unit": {
+ "name": "",
+ "abbreviation": "",
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none"
+ }
+ }
+ ],
+ "ingredient": {
+ "updated_at": 1509035282,
+ "id": 155,
+ "name": "lemon",
+ "display_singular": "lemon",
+ "display_plural": "lemons",
+ "created_at": 1493906426
+ }
+ },
+ {
+ "position": 3,
+ "measurements": [
+ {
+ "unit": {
+ "display_singular": "cup",
+ "display_plural": "cups",
+ "system": "imperial",
+ "name": "cup",
+ "abbreviation": "c"
+ },
+ "id": 727567,
+ "quantity": "1"
+ },
+ {
+ "id": 727566,
+ "quantity": "360",
+ "unit": {
+ "name": "gram",
+ "abbreviation": "g",
+ "display_singular": "g",
+ "display_plural": "g",
+ "system": "metric"
+ }
+ }
+ ],
+ "ingredient": {
+ "id": 374,
+ "name": "green beans",
+ "display_singular": "green bean",
+ "display_plural": "green beans",
+ "created_at": 1494976031,
+ "updated_at": 1509035266
+ },
+ "id": 30506,
+ "raw_text": "1 cup green beans, trimmed",
+ "extra_comment": "trimmed"
+ },
+ {
+ "id": 30507,
+ "raw_text": "1 tablespoon olive oil",
+ "extra_comment": "",
+ "position": 4,
+ "measurements": [
+ {
+ "id": 727564,
+ "quantity": "1",
+ "unit": {
+ "name": "tablespoon",
+ "abbreviation": "tbsp",
+ "display_singular": "tablespoon",
+ "display_plural": "tablespoons",
+ "system": "imperial"
+ }
+ }
+ ],
+ "ingredient": {
+ "id": 4,
+ "name": "olive oil",
+ "display_singular": "olive oil",
+ "display_plural": "olive oils",
+ "created_at": 1493306183,
+ "updated_at": 1509035290
+ }
+ },
+ {
+ "id": 30508,
+ "raw_text": "1 tablespoon rosemary, chopped",
+ "extra_comment": "",
+ "position": 5,
+ "measurements": [
+ {
+ "id": 727560,
+ "quantity": "1",
+ "unit": {
+ "display_singular": "tablespoon",
+ "display_plural": "tablespoons",
+ "system": "imperial",
+ "name": "tablespoon",
+ "abbreviation": "tbsp"
+ }
+ }
+ ],
+ "ingredient": {
+ "name": "chopped fresh rosemary leaves",
+ "display_singular": "chopped fresh rosemary leaf",
+ "display_plural": "chopped fresh rosemary leaves",
+ "created_at": 1678823278,
+ "updated_at": 1678823278,
+ "id": 10849
+ }
+ },
+ {
+ "raw_text": "1 tablespoon thyme, chopped",
+ "extra_comment": "",
+ "position": 6,
+ "measurements": [
+ {
+ "id": 727569,
+ "quantity": "1",
+ "unit": {
+ "name": "tablespoon",
+ "abbreviation": "tbsp",
+ "display_singular": "tablespoon",
+ "display_plural": "tablespoons",
+ "system": "imperial"
+ }
+ }
+ ],
+ "ingredient": {
+ "name": "chopped fresh thyme leaves",
+ "display_singular": "chopped fresh thyme leaf",
+ "display_plural": "chopped fresh thyme leaves",
+ "created_at": 1678823272,
+ "updated_at": 1678823272,
+ "id": 10848
+ },
+ "id": 30509
+ },
+ {
+ "id": 30510,
+ "raw_text": "1 clove garlic, minced",
+ "extra_comment": "minced",
+ "position": 7,
+ "measurements": [
+ {
+ "id": 727559,
+ "quantity": "1",
+ "unit": {
+ "name": "clove",
+ "abbreviation": "clove",
+ "display_singular": "clove",
+ "display_plural": "cloves",
+ "system": "none"
+ }
+ }
+ ],
+ "ingredient": {
+ "created_at": 1493744766,
+ "updated_at": 1509035285,
+ "id": 95,
+ "name": "garlic",
+ "display_singular": "garlic",
+ "display_plural": "garlics"
+ }
+ },
+ {
+ "ingredient": {
+ "updated_at": 1509035289,
+ "id": 11,
+ "name": "kosher salt",
+ "display_singular": "kosher salt",
+ "display_plural": "kosher salts",
+ "created_at": 1493307153
+ },
+ "id": 30511,
+ "raw_text": "½ teaspoon salt, plus more to season",
+ "extra_comment": "plus more to taste",
+ "position": 8,
+ "measurements": [
+ {
+ "id": 727568,
+ "quantity": "½",
+ "unit": {
+ "display_plural": "teaspoons",
+ "system": "imperial",
+ "name": "teaspoon",
+ "abbreviation": "tsp",
+ "display_singular": "teaspoon"
+ }
+ }
+ ]
+ },
+ {
+ "extra_comment": "plus more to taste",
+ "position": 9,
+ "measurements": [
+ {
+ "id": 727561,
+ "quantity": "¼",
+ "unit": {
+ "abbreviation": "tsp",
+ "display_singular": "teaspoon",
+ "display_plural": "teaspoons",
+ "system": "imperial",
+ "name": "teaspoon"
+ }
+ }
+ ],
+ "ingredient": {
+ "id": 166,
+ "name": "freshly ground black pepper",
+ "display_singular": "freshly ground black pepper",
+ "display_plural": "freshly ground black peppers",
+ "created_at": 1493925438,
+ "updated_at": 1509035282
+ },
+ "id": 30512,
+ "raw_text": "¼ teaspoon ground black pepper, plus more to season"
+ },
+ {
+ "id": 30513,
+ "raw_text": "1 boneless, skinless chicken breast",
+ "extra_comment": "",
+ "position": 10,
+ "measurements": [
+ {
+ "id": 727562,
+ "quantity": "1",
+ "unit": {
+ "display_singular": "",
+ "display_plural": "",
+ "system": "none",
+ "name": "",
+ "abbreviation": ""
+ }
+ }
+ ],
+ "ingredient": {
+ "display_singular": "boneless, skinless chicken breast",
+ "display_plural": "boneless, skinless chicken breasts",
+ "created_at": 1493390997,
+ "updated_at": 1509035287,
+ "id": 33,
+ "name": "boneless, skinless chicken breast"
+ }
+ },
+ {
+ "position": 11,
+ "measurements": [
+ {
+ "unit": {
+ "display_singular": "teaspoon",
+ "display_plural": "teaspoons",
+ "system": "imperial",
+ "name": "teaspoon",
+ "abbreviation": "tsp"
+ },
+ "id": 727565,
+ "quantity": "¼"
+ }
+ ],
+ "ingredient": {
+ "display_plural": "paprikas",
+ "created_at": 1493430149,
+ "updated_at": 1509035286,
+ "id": 42,
+ "name": "paprika",
+ "display_singular": "paprika"
+ },
+ "id": 30514,
+ "raw_text": "¼ teaspoon paprika",
+ "extra_comment": ""
+ }
+ ],
+ "name": null,
+ "position": 1
+ }
+ ],
+ "facebook_posts": [],
+ "nutrition": {
+ "sugar": 20,
+ "fiber": 33,
+ "updated_at": "2023-03-25T07:08:24+01:00",
+ "calories": 536,
+ "carbohydrates": 52,
+ "fat": 18,
+ "protein": 39
+ },
+ "tips_and_ratings_enabled": true,
+ "updated_at": 1683237600,
+ "compilations": [
+ {
+ "draft_status": "published",
+ "is_shoppable": false,
+ "language": "eng",
+ "promotion": "full",
+ "slug": "one-pan-meals-under-500-calories",
+ "canonical_id": "compilation:342",
+ "created_at": 1513281206,
+ "name": "One-Pan Meals Under 500 Calories",
+ "video_url": "https://vid.tasty.co/output/68224/hls24_1513303098.m3u8",
+ "show": [
+ {
+ "id": 34,
+ "name": "Goodful"
+ }
+ ],
+ "approved_at": 1513564174,
+ "beauty_url": "https://img.buzzfeed.com/video-api-prod/assets/711a5ab95e2a49609b992bc5834da2cd/BFV34175_One-PanMealsUnder500Calories-BeautyShot.jpg",
+ "country": "US",
+ "thumbnail_url": "https://img.buzzfeed.com/thumbnailer-prod-us-east-1/video-api/assets/124164.jpg",
+ "video_id": 39588,
+ "facebook_posts": [],
+ "aspect_ratio": "1:1",
+ "buzz_id": 4707419,
+ "description": null,
+ "id": 342,
+ "keywords": null,
+ "thumbnail_alt_text": ""
+ },
+ {
+ "promotion": "full",
+ "approved_at": 1541000496,
+ "buzz_id": null,
+ "created_at": 1541000315,
+ "id": 712,
+ "is_shoppable": true,
+ "keywords": null,
+ "language": "eng",
+ "aspect_ratio": "1:1",
+ "draft_status": "published",
+ "canonical_id": "compilation:712",
+ "show": [
+ {
+ "id": 34,
+ "name": "Goodful"
+ }
+ ],
+ "video_url": "https://vid.tasty.co/output/113684/hls24_1540919176.m3u8",
+ "beauty_url": null,
+ "country": "US",
+ "description": "We’re all looking for ways to eat a bit healthier and let’s face it: it’s not always easy. But that’s why we’re here to help. These quick and easy dinners under 500 calories promise you explosions of flavor, courtesy of some strong, herbaceous notes and the freshest of ingredients. From a single-pan roasted chicken to a one-pot chickpea curry , we’re here to help you eat healthy, happy, and flavor-laden. \n",
+ "slug": "dinners-under-500-calories",
+ "thumbnail_alt_text": "",
+ "thumbnail_url": "https://img.buzzfeed.com/video-api-prod/assets/e83de47984b74d7f81e491aabe00e836/4_Healthy_Weeknight_Dinners_Under_300_Calories.jpg",
+ "video_id": 22965,
+ "facebook_posts": [],
+ "name": "Dinners Under 500 Calories"
+ },
+ {
+ "approved_at": 1557454609,
+ "aspect_ratio": "1:1",
+ "slug": "sweet-potato-recipes-under-500-calories",
+ "draft_status": "published",
+ "id": 938,
+ "video_url": "https://vid.tasty.co/output/132144/hls24_1557379812.m3u8",
+ "language": "eng",
+ "promotion": "full",
+ "canonical_id": "compilation:938",
+ "buzz_id": null,
+ "country": "US",
+ "description": null,
+ "is_shoppable": false,
+ "keywords": null,
+ "video_id": 82921,
+ "facebook_posts": [],
+ "show": [
+ {
+ "name": "Goodful",
+ "id": 34
+ }
+ ],
+ "beauty_url": null,
+ "created_at": 1557379588,
+ "name": "Sweet Potato Recipes Under 500 Calories ",
+ "thumbnail_alt_text": "",
+ "thumbnail_url": "https://img.buzzfeed.com/thumbnailer-prod-us-east-1/video-api/assets/215305.jpg"
+ },
+ {
+ "is_shoppable": true,
+ "name": "5 Sheet Pan Dinners For The Easiest Week Ever",
+ "video_url": "https://vid.tasty.co/output/149413/hls24_1571895435.m3u8",
+ "buzz_id": null,
+ "description": null,
+ "id": 1203,
+ "slug": "5-sheet-pan-dinners-for-the-easiest-week-ever",
+ "facebook_posts": [],
+ "created_at": 1571895323,
+ "draft_status": "published",
+ "language": "eng",
+ "keywords": null,
+ "thumbnail_alt_text": "",
+ "thumbnail_url": "https://img.buzzfeed.com/thumbnailer-prod-us-east-1/video-api/assets/239826.jpg",
+ "video_id": 93739,
+ "canonical_id": "compilation:1203",
+ "approved_at": 1577892431,
+ "aspect_ratio": "1:1",
+ "country": "US",
+ "beauty_url": null,
+ "promotion": "full",
+ "show": [
+ {
+ "name": "Tasty",
+ "id": 17
+ }
+ ]
+ },
+ {
+ "show": [
+ {
+ "id": 17,
+ "name": "Tasty"
+ }
+ ],
+ "aspect_ratio": "1:1",
+ "name": "Kickstart Your New Year With These Low Calorie Meal Recipes",
+ "thumbnail_alt_text": "",
+ "canonical_id": "compilation:1320",
+ "approved_at": 1576162826,
+ "country": "US",
+ "promotion": "full",
+ "id": 1320,
+ "language": "eng",
+ "video_id": 97160,
+ "video_url": "https://vid.tasty.co/output/154794/hls24_1576141088.m3u8",
+ "facebook_posts": [],
+ "beauty_url": null,
+ "buzz_id": null,
+ "description": null,
+ "keywords": null,
+ "slug": "kickstart-your-new-year-with-these-low-calorie-meal-recipes",
+ "thumbnail_url": "https://img.buzzfeed.com/thumbnailer-prod-us-east-1/video-api/assets/247447.jpg",
+ "created_at": 1576141044,
+ "draft_status": "published",
+ "is_shoppable": false
+ },
+ {
+ "draft_status": "published",
+ "is_shoppable": false,
+ "video_url": "https://vid.tasty.co/output/160298/hls24_1581588059.m3u8",
+ "facebook_posts": [],
+ "approved_at": 1581602162,
+ "beauty_url": null,
+ "created_at": 1581587786,
+ "language": "eng",
+ "name": "5 Low Calorie Sweet Potato Recipes",
+ "promotion": "full",
+ "thumbnail_url": "https://img.buzzfeed.com/thumbnailer-prod-us-east-1/video-api/assets/255244.jpg",
+ "country": "US",
+ "video_id": 100181,
+ "show": [
+ {
+ "name": "Goodful",
+ "id": 34
+ }
+ ],
+ "canonical_id": "compilation:1401",
+ "aspect_ratio": "1:1",
+ "buzz_id": null,
+ "description": null,
+ "id": 1401,
+ "keywords": null,
+ "slug": "5-low-calorie-sweet-potato-recipes",
+ "thumbnail_alt_text": ""
+ }
+ ],
+ "draft_status": "published",
+ "video_url": "https://vid.tasty.co/output/68224/hls24_1513303098.m3u8",
+ "cook_time_minutes": null,
+ "original_video_url": "https://s3.amazonaws.com/video-api-prod/assets/99e7a28d1cfd4b228ffbf7d64be39064/BFV34175_One-PanMealsUnder500Calories-FB1080SQ.mp4",
+ "video_ad_content": "none",
+ "canonical_id": "recipe:3194",
+ "price": {
+ "total": 1550,
+ "portion": 1550,
+ "consumption_total": 650,
+ "consumption_portion": 650,
+ "updated_at": "2023-11-09T06:07:13+01:00"
+ },
+ "topics": [
+ {
+ "slug": "easy-dinner",
+ "name": "Easy Dinner"
+ },
+ {
+ "name": "Healthy Eating",
+ "slug": "healthy"
+ },
+ {
+ "slug": "one-pot",
+ "name": "One-Pot Recipes"
+ },
+ {
+ "name": "Romantic Dinners",
+ "slug": "romantic-dinners"
+ },
+ {
+ "name": "Lunch",
+ "slug": "lunch"
+ },
+ {
+ "name": "Dinner",
+ "slug": "dinner"
+ },
+ {
+ "name": "American",
+ "slug": "american"
+ }
+ ],
+ "prep_time_minutes": null,
+ "keywords": null,
+ "total_time_minutes": null,
+ "instructions": [
+ {
+ "display_text": "Preheat the oven to 375˚F (190˚C). Line a baking sheet with parchment paper.",
+ "position": 1,
+ "start_time": 0,
+ "end_time": 0,
+ "temperature": 375,
+ "appliance": "oven",
+ "id": 26813
+ },
+ {
+ "id": 26814,
+ "display_text": "Add the sweet potatoes, green beans, lemon, olive oil, salt, pepper, rosemary, thyme, and garlic to the prepared baking sheet and toss until fully coated. Spread on either side of the baking sheet, leaving space in the middle. Place the chicken at the center of the pan and season with salt, pepper, and the paprika.",
+ "position": 2,
+ "start_time": 113350,
+ "end_time": 131690,
+ "temperature": null,
+ "appliance": null
+ },
+ {
+ "id": 26817,
+ "display_text": "Bake until the vegetables are tender and chicken is cooked through, about 20 minutes.",
+ "position": 3,
+ "start_time": 0,
+ "end_time": 0,
+ "temperature": null,
+ "appliance": null
+ },
+ {
+ "id": 26818,
+ "display_text": "Enjoy!",
+ "position": 4,
+ "start_time": 158650,
+ "end_time": 173230,
+ "temperature": null,
+ "appliance": null
+ }
+ ],
+ "is_shoppable": true,
+ "thumbnail_url": "https://img.buzzfeed.com/thumbnailer-prod-us-east-1/cb5bf201eeef4702bc44b4bff81b9630/BFV34175_One-PanMealsUnder500Calories-FB1080SQ.jpg",
+ "country": "US",
+ "num_servings": 1,
+ "total_time_tier": {
+ "tier": "under_30_minutes",
+ "display_tier": "Under 30 minutes"
+ },
+ "nutrition_visibility": "auto"
+ },
{
"is_one_top": false,
"cook_time_minutes": 10,
diff --git a/src/mirageServer/endpoints/tips/tips.json b/src/mirageServer/endpoints/tips/tips.json
new file mode 100644
index 0000000..6f19289
--- /dev/null
+++ b/src/mirageServer/endpoints/tips/tips.json
@@ -0,0 +1,108 @@
+{
+ "count": 73,
+ "results": [
+ {
+ "author_avatar_url": "https://img.buzzfeed.com/tasty-app-user-assets-prod-us-east-1/avatars/default/strawberry.png",
+ "author_name": "Tania Miah",
+ "author_rating": 1,
+ "author_user_id": 13481921,
+ "author_username": "",
+ "author_is_verified": 0,
+ "is_flagged": false,
+ "recipe_id": 8110,
+ "status_id": 1,
+ "comment_id": 1572180254,
+ "comment_count": 7,
+ "tip_body": "I think this recipe was amazing all I can say is to make more. Lol but a little less brown sugar. B.T.W I am 10 and I did it by myself.",
+ "tip_id": 545606,
+ "tip_photo": {
+ "height": 960,
+ "url": "https://img.buzzfeed.com/tasty-app-user-assets-prod-us-east-1/tips/1a3331b1463b468f9bbd8ea228ea5b09.jpeg",
+ "width": 1280
+ },
+ "created_at": null,
+ "updated_at": 1646591815,
+ "upvotes_total": 210
+ },
+ {
+ "author_avatar_url": "https://lh3.googleusercontent.com/a/AATXAJyPiaxPSAm8qZYtP-ui-FGoyXXSNY4bcscTrw5Y=s96-c",
+ "author_name": "MR B",
+ "author_rating": 1,
+ "author_user_id": 20258536,
+ "author_username": "",
+ "author_is_verified": 0,
+ "is_flagged": false,
+ "recipe_id": 8110,
+ "status_id": 1,
+ "comment_id": 1572181722,
+ "comment_count": 1,
+ "tip_body": "my 4 yr old loves French toast so dad had to make it pretty for her",
+ "tip_id": 547215,
+ "tip_photo": {
+ "height": 1024,
+ "url": "https://img.buzzfeed.com/tasty-app-user-assets-prod-us-east-1/tips/26cdd6af8c374c90b6462512d3c3d673.jpeg",
+ "width": 768
+ },
+ "created_at": null,
+ "updated_at": 1647322814,
+ "upvotes_total": 91
+ },
+ {
+ "author_avatar_url": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=349284286255262&height=200&ext=1602241954&hash=AeSj-TffKVK7zqyM",
+ "author_name": "Abdullah Nadeem",
+ "author_rating": 1,
+ "author_user_id": 17450212,
+ "author_username": "",
+ "author_is_verified": 0,
+ "is_flagged": false,
+ "recipe_id": 8110,
+ "status_id": 1,
+ "comment_id": 1572177412,
+ "comment_count": 0,
+ "tip_body": "I used white bread and added a bit cocoa powder and boom love it ❤️ also make pakistani dishes. Love u tasty ❤️\nLove from pakistan ❤️ 🇵🇰 ❤️",
+ "tip_id": 542464,
+ "tip_photo": null,
+ "created_at": null,
+ "updated_at": 1645188619,
+ "upvotes_total": 35
+ },
+ {
+ "author_avatar_url": "https://img.buzzfeed.com/tasty-app-user-assets-prod-us-east-1/avatars/default/broccoli.png",
+ "author_name": "",
+ "author_rating": 1,
+ "author_user_id": 19826065,
+ "author_username": "racercat2000",
+ "author_is_verified": 0,
+ "is_flagged": false,
+ "recipe_id": 8110,
+ "status_id": 1,
+ "comment_id": 1572177373,
+ "comment_count": 0,
+ "tip_body": "Yaaas so good I used white bread totally fine I also used vanilla extract still super good",
+ "tip_id": 542420,
+ "tip_photo": null,
+ "created_at": null,
+ "updated_at": 1645151508,
+ "upvotes_total": 31
+ },
+ {
+ "author_avatar_url": "https://img.buzzfeed.com/tasty-app-user-assets-prod-us-east-1/avatars/default/donut.png",
+ "author_name": "Aviv Mosayov",
+ "author_rating": 1,
+ "author_user_id": 19520069,
+ "author_username": "bakingbookworm",
+ "author_is_verified": 0,
+ "is_flagged": false,
+ "recipe_id": 8110,
+ "status_id": 1,
+ "comment_id": 1572177536,
+ "comment_count": 0,
+ "tip_body": "If your using challah bread (which is best btw) you should really try making your own it will elevate it from eh to YEAHHH",
+ "tip_id": 542594,
+ "tip_photo": null,
+ "created_at": null,
+ "updated_at": 1645234414,
+ "upvotes_total": 27
+ }
+ ]
+}
diff --git a/src/mirageServer/server.js b/src/mirageServer/server.js
index 6584f9a..483f601 100644
--- a/src/mirageServer/server.js
+++ b/src/mirageServer/server.js
@@ -3,6 +3,8 @@ import jsonPlaceHolderData from "./endpoints/jsonPlaceHolder.json";
import recipesListData from "./endpoints/recipes/list.json";
import recipesListSimilaritiesData from "./endpoints/recipes/listSimilarities.json";
import recipesAutocomplete from "./endpoints/recipes/autocomplete.json";
+//import recipeGetMoreInfo from "./endpoints/recipes/get-more-info.json";
+import recipeTips from "./endpoints/tips/tips.json";
export default function () {
const jsonPlaceholderAPIRoot = "https://jsonplaceholder.typicode.com";
@@ -33,7 +35,8 @@ export default function () {
}
function matchSearchId(q, data) {
- const id = q.match(/(?<=id:)[0-9]+/i);
+ const idMatch = q.match(/(?:id:)([0-9]+)/i);
+ const id = idMatch && idMatch[1];
if (!id) return false;
const result = data.results.find((recipe) => recipe.id === +id);
if (!result) return false;
@@ -82,6 +85,18 @@ export default function () {
timing: 1200,
},
);
+ this.get(
+ `${tastyAPIRoot}/recipes/get-more-info`,
+ (schema, request) => {
+ const id = request.queryParams.id;
+ const data = recipesListData;
+ return data.results.find((recipe) => recipe.id === +id);
+ },
+ {
+ timing: 1200,
+ },
+ );
+ this.get(`${tastyAPIRoot}/tips/list`, () => recipeTips, { timing: 1200 });
},
});
}
diff --git a/src/pages/About.jsx b/src/pages/About.jsx
index d8553fe..a1e8ac1 100644
--- a/src/pages/About.jsx
+++ b/src/pages/About.jsx
@@ -1,40 +1,35 @@
import { FaLinkedin } from "react-icons/fa";
import { FaGithubSquare } from "react-icons/fa";
import { Heading } from "@/features/ui";
-import homepage from "@/assets/about/homepage.png";
import SvgComponent from "@/assets/brand/swooshes/svgWave";
import { team } from "@/constants";
+import usePageTitle from "../hooks/usePageTitle";
+import ChinguRectangle from "../assets/about/chingu-rectangle.png";
+import Yumi from "@/assets/about/yumi-with-blue-apron-holding-brown-salad-bowl.png";
+import YumiWithBasket from "@/assets/brand/yumi-with-basket/YumiWithBasket";
const About = () => {
- return (
-
- {/*
About */}
-
- About
-
+ usePageTitle("About");
- {/* START Project section */}
+ return (
+
-
- This Project
-
-
-
-
-
- We developed a unique web app with a distinctive brand and a
- whimsical, animated design. YumYumYes! is intended to make recipe
- browsing fun! Colorful mascots Yumi and Chef Carrots welcome
- visitors to our pages, and delightful visual motifs including text
- gradients, 3d colored balls, dashed lines and gentle curves are
- intended to make the app feel more alive and engaging.
-
-
- <>
+
+
+
+ About YumYumYes!
+
+
+ YumYumYes! is intended to make recipe browsing fun!
+
+
+ We developed a unique web app with a distinctive brand and a
+ whimsical, animated design. Colorful mascots Yumi and Chef Carrots
+ welcome visitors to our pages, and delightful visual motifs
+ including text gradients, 3d colored balls, dashed lines and
+ gentle curves are intended to make the app feel more alive and
+ engaging.
+
YumYumYes! allows users to search for recipes by ingredient and
filter by useful tags such as difficulty, occasion, etc. The app
@@ -47,37 +42,47 @@ const About = () => {
care to make it intuitive for all users, and accessible to users
with different abilities.
- >
+
+
+
+
{/* Display top of wave between sections */}
-
+
{/* END Project section */}
{/* START Team section */}
-
+
- Our Team
+ Meet the YumYumTeam!
-
+
+ These are the exceptional team members that made YumYumYes! possible.
+
+
+
{team.map(({ name, role, summary, image, socials }, index) => (
-
+
{name}
{role}
@@ -85,7 +90,7 @@ const About = () => {
@@ -109,13 +114,14 @@ const About = () => {
{summary}
+
{
// Display top of wave
// but don't display top of wave after last team member
index !== team.length - 1 && (
-
+
)
}
@@ -123,7 +129,7 @@ const About = () => {
// Display bottom of wave
// but don't display bottom of wave before first team member
index !== 0 && (
-
+
)
}
@@ -131,9 +137,48 @@ const About = () => {
{/* Display bottom of wave in between sections */}
-
+
+
+ {/* Display Yumi_2 image between sections */}
+
+
{/* END Team section */}
+
+
+
+
+ About Chingu
+
+
+
+ {`Chingu is a community of developers who are passionate about
+ learning, helping others, and collaborating on projects. It is a
+ remote, self-organizing, project-based learning platform. Chingu
+ helps you to build your portfolio and network while collaborating on
+ real-world projects with remote team members. "Chingus" are
+ motivated learners who are self-directed and take responsibility for
+ their own learning.`}
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/pages/Favorites.jsx b/src/pages/Favorites.jsx
new file mode 100644
index 0000000..2730664
--- /dev/null
+++ b/src/pages/Favorites.jsx
@@ -0,0 +1,123 @@
+import { useEffect, useState, Fragment } from "react";
+import { Dialog, Transition } from "@headlessui/react";
+import { Button, Heading } from "@/features/ui";
+import { RecipeCard } from "@/features/recipes/components/recipe-card";
+import usePageTitle from "../hooks/usePageTitle";
+
+const Favorites = () => {
+ usePageTitle("Favorite Recipes");
+
+ const [favorites, setFavorites] = useState([]);
+ const [isOpen, setIsOpen] = useState(false);
+
+ useEffect(() => {
+ const savedFavorites = localStorage.getItem("favorites");
+ if (savedFavorites) {
+ // Parse the saved JSON data
+ setFavorites(JSON.parse(savedFavorites));
+ }
+ }, []);
+
+ const hasFavorites = favorites.length > 0;
+
+ return (
+ <>
+
+ Favorite Recipes
+
+
+
+ {
+ hasFavorites ? setIsOpen(true) : null;
+ }}
+ >
+ {hasFavorites
+ ? `Clear Favorites ${hasFavorites ? `(${favorites.length})` : null}`
+ : "No Favorites Saved Yet"}
+
+
+
+
+ {favorites.map((recipeId) => (
+
+ ))}
+
+
+
+ setIsOpen(false)} className="relative z-50">
+
+ {/* The backdrop, rendered as a fixed sibling to the panel container */}
+
+
+
+ {/* Full-screen container to center the panel */}
+
+
+ {/* The actual dialog panel */}
+
+
+ Clear Favorites
+
+
+
+
+ This will permanently remove your favorite recipes.
+
+
+
+ Are you sure you want to clear your favorites?
+
+
+
+
+
+ {
+ localStorage.removeItem("favorites");
+ setFavorites([]);
+ setIsOpen(false);
+ }}
+ className="w-full place-content-center"
+ >
+ Yes
+
+ setIsOpen(false)}
+ className="w-full place-content-center"
+ >
+ No
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Favorites;
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 5c8613d..38da79f 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -1,8 +1,11 @@
import { HeroSection } from "@/features/ui";
import { SearchSection } from "@/features/ui";
import { TopRecipes } from "@/features/ui";
+import usePageTitle from "../hooks/usePageTitle";
const Home = () => {
+ usePageTitle(null);
+
return (
diff --git a/src/pages/PageNotFound.jsx b/src/pages/PageNotFound.jsx
new file mode 100644
index 0000000..0a04ba3
--- /dev/null
+++ b/src/pages/PageNotFound.jsx
@@ -0,0 +1,13 @@
+import { HeroSectionPageNotFound } from "@/features/ui";
+import usePageTitle from "../hooks/usePageTitle";
+
+const PageNotFound = () => {
+ usePageTitle("Not Found");
+ return (
+
+
+
+ );
+};
+
+export default PageNotFound;
diff --git a/src/pages/RecipeDetails.jsx b/src/pages/RecipeDetails.jsx
index aa63f9a..b6a0c08 100644
--- a/src/pages/RecipeDetails.jsx
+++ b/src/pages/RecipeDetails.jsx
@@ -1,29 +1,25 @@
import { useParams } from "react-router-dom";
import { useSessionStorage } from "@/features/recipes/hooks";
import { Heading, LoadingState } from "@/features/ui";
-import { FetchRecipeById } from "@/features/recipes/api";
+import { FetchRecipeDetailsById } from "@/features/recipes/api";
import RecipeDetails from "@/features/recipes/components/recipe-details/RecipeDetails";
+import PageNotFound from "./PageNotFound";
const RecipesDetailPage = () => {
- let { recipeId } = useParams();
-
+ const { recipeId } = useParams();
const [cachedRecipesList] = useSessionStorage("recipes", []);
- console.log(cachedRecipesList);
-
+ let recipe = null;
// Attempt to retrieve recipe from session storage.
- const cachedRecipe = cachedRecipesList.results?.find(
- (recipe) => recipe.id === recipeId,
- );
-
- let recipe = {};
- let query = null;
-
- if (cachedRecipe) recipe = cachedRecipe;
- else query = `id:${recipeId}`;
-
- const { data, isLoading, isError, error } = FetchRecipeById(query);
+ if (cachedRecipesList.length > 0) {
+ recipe = cachedRecipesList.results.find(
+ (result) => result.id === +recipeId,
+ );
+ }
- if (data) recipe = data.results[0];
+ // Call API to fetch recipe details if not found in cache.
+ const { data, isLoading, isError, error } = recipe
+ ? { data: recipe[0], isLoading: false, isError: false, error: null }
+ : FetchRecipeDetailsById(recipeId);
// Quit gracefully if no id is available
if (!recipeId) {
@@ -44,7 +40,9 @@ const RecipesDetailPage = () => {
return
Error: {error}
;
}
- return
;
+ if (!data?.name) return
;
+
+ return
;
};
export default RecipesDetailPage;
diff --git a/src/pages/Search.jsx b/src/pages/Search.jsx
index 503071e..bdd989e 100644
--- a/src/pages/Search.jsx
+++ b/src/pages/Search.jsx
@@ -1,12 +1,15 @@
import YumiWithSpoon from "@/assets/brand/yumi-with-spoon/YumiWithSpoon.jsx";
import BellPeppers from "@/assets/brand/bell-peppers/BellPeppers";
import SvgComponent from "@/assets/brand/swooshes/svgWave";
-import { RecipeList } from "@/features/recipes";
+import { RecipeList, FeatureOfTheDay } from "@/features/recipes";
import { Heading, SearchBox } from "@/features/ui";
import debounce from "lodash/debounce";
import { useSearchParams } from "react-router-dom";
+import usePageTitle from "../hooks/usePageTitle";
const Search = () => {
+ usePageTitle("Search Recipes");
+
const [search, setSearch] = useSearchParams();
const searchTerm = search.get("q") || "";
const debouncedSetSearchParams = debounce(setSearch, 300);
@@ -21,17 +24,20 @@ const Search = () => {
return (
- {/*
*/}
+
+ YumYum Time!!
+
-
-
- YumYum Time!!
-
-
-
+
+
+
-
+
Add Ingredients Here and We Will Do Our Magic!
@@ -39,7 +45,7 @@ const Search = () => {
-
-
-
+ {searchTerm !== "" ? (
+
+ ) : (
+
+ )}
+
+ {/* Yellow Ball 1 */}
+
+ {/* Orange Ball */}
+
+ {/* Green Ball */}
+
+
);
diff --git a/src/pages/index.js b/src/pages/index.js
index 3cfc26e..bed8e85 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -3,5 +3,6 @@ import Search from "./Search";
import RecipeDetails from "./RecipeDetails";
import About from "./About";
import Components from "./Components";
+import PageNotFound from "./PageNotFound";
-export { Home, About, Search, RecipeDetails, Components };
+export { Home, About, Search, RecipeDetails, Components, PageNotFound };
diff --git a/src/routes/index.jsx b/src/routes/index.jsx
index f543fde..bbedd00 100644
--- a/src/routes/index.jsx
+++ b/src/routes/index.jsx
@@ -1,11 +1,23 @@
-import { Outlet } from "react-router-dom";
+import { useEffect } from "react";
+import { Outlet, useLocation } from "react-router-dom";
import { Header, Footer } from "@/features/ui";
+function ScrollToTop() {
+ const { pathname } = useLocation();
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [pathname]);
+
+ return null;
+}
+
export default function Spa() {
return (
<>
+
diff --git a/src/styles/global.css b/src/styles/global.css
index 6e99062..484b3ec 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -103,16 +103,37 @@
}
.text-shadow {
- text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25);
+ filter: brightness(1.25);
}
.text-shadow-sm {
- text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25);
+ text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25);
+ filter: brightness(1.25);
}
+
.text-stroke {
- -webkit-text-stroke: 1.5px var(--color-light-cream);
+ -webkit-text-stroke: 0.75px var(--color-light-cream);
}
.text-stroke-sm {
- -webkit-text-stroke: 0.75px var(--color-light-cream);
+ -webkit-text-stroke: 0.5px var(--color-light-cream);
+}
+
+@media screen and (min-width: 1080px) {
+ .text-shadow {
+ text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ filter: brightness(1.25);
+ }
+ .text-shadow-sm {
+ text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25);
+ filter: brightness(1.25);
+ }
+
+ .text-stroke {
+ -webkit-text-stroke: 1px var(--color-light-cream);
+ }
+ .text-stroke-sm {
+ -webkit-text-stroke: 0.75px var(--color-light-cream);
+ }
}
body,
@@ -137,3 +158,16 @@ h6,
h6.font-display {
font-family: var(--font-open-sans);
}
+.text-no-stroke {
+ -webkit-text-stroke: 0px;
+}
+
+.y3-shadow-md-inner {
+ box-shadow:
+ inset 0 4px 6px -1px rgb(0 0 0 / 0.1),
+ 0 2px 4px -2px rgb(0 0 0 / 0.1);
+}
+
+.balance {
+ text-wrap: balance;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index e1967aa..ae865b3 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -135,6 +135,7 @@ export default {
RedBall: "url('/src/assets/HomePage/RedPlasticBall.png')",
YellowBall: "url('/src/assets/HomePage/YellowPlasticBall.png')",
OrangeBall: "url('/src/assets/HomePage/OrangePlasticBall.png')",
+ GreenBall: "url('/src/assets/HomePage/GreenPlasticBall.png')",
"title-cutout":
"url('/src/assets/brand/swooshes/title-background-cutout.svg')",
},