Skip to content

Instantly share code, notes, and snippets.

@derektypist
Created August 13, 2021 17:52
Show Gist options
  • Save derektypist/d907549b205ac2784c9c95e03d30ea0d to your computer and use it in GitHub Desktop.
Save derektypist/d907549b205ac2784c9c95e03d30ea0d to your computer and use it in GitHub Desktop.
Recipe Box
<div id="app"></div>

Recipe Box

The purpose of this project is to provide a recipe box. Allows you to create, read, update and delete recipes as well as its ingredients and directions. Taken from https://codepen.io/freeCodeCamp/pen/dNVazZ, accessed on August 2021. Recipes taken from Easy Peasy Meals (2015) - Pavilion Books, except Fish and Vegetables.

A Pen by Derek Dhammaloka on CodePen.

License.

// Local Storage Key
const localStorageKey = "_derektypist_recipes";
// Create Local Storage Manager Class
class LocalStorageManager {
set(obj) {
let currentState = JSON.stringify(obj);
localStorage.setItem("_derektypist_recipes", currentState);
}
get() {
let currentState = localStorage.getItem("_derektypist_recipes");
return JSON.parse(currentState);
}
}
// Create Recipe Index
const recipeIndex = [
{
recipe: "Penne Puttanesca",
ingredients: [
"350g Dried Penne Pasta",
"1 tbsp olive oil",
"1 onion, finely chopped",
"400g can chopped tomatoes",
"2 tsp dried oregano",
"120g can boneless and skinless sardine fillets, drained",
"50g Black Olives, pitted",
"A small handful of curly parsley, chopped",
"Salt and Freshly Ground Black Pepper"
],
directions: [
"Bring a large pan of salted water to the boil and cook the pasta according to the pack instructions. Drain well, keeping a cupful of the cooking water to one side.",
"Meanwhile, heat the oil in a large pan and fry the onion for 10 minutes until softened but not coloured. Add the tomatoes and oregano, then bring the mixture to the boil, reduce the heat and simmer for 15 minutes until thickened. Stir in the sardines and olives - the stirring should help break up the fish slightly.",
"Add the pasta to the sauce and toss well to combine. Add a little of the pasta water if the mixture looks too dry. Check the seasoning, then divide among four bowls. Garnish with parsley and serve immediately."
]
},
{
recipe: "Fish and Vegetables",
ingredients: ["Fish", "Potatoes", "Green Beans", "Carrots"],
directions: [
"Wash the potatoes, green beans and carrots",
"Chop the green beans and carrots into smaller pieces",
"Preheat the oven to 220 °C",
"Put the fish in the oven and bake for 25 minutes",
"Put the potatoes in boiling water, bring to boil and simmer for 20 minutes. Drain well.",
"Put the carrots in boiling water, bring to boil and simmer for 8 minutes. Drain well.",
"Put the green beans in boiling water, bring to boil and simmer for 6 minutes. Drain well."
]
},
{
recipe: "Walnut and Creamy Blue Cheese",
ingredients: [
"1 tsp olive oil",
"1 crushed garlic clove",
"25g Toasted Walnut Pieces",
"100g Cubed Gorgonzola",
"150ml Single Cream",
"Ground Black Pepper"
],
directions: [
"Heat 1 tsp olive oil in a small pan.",
"Add 1 crushed garlic clove and 25g Toasted Walnut Pieces. Cook for 1 minute.",
"Add 100g Cubed Gorgonzola and 150ml Single Cream.",
"Season with Ground Black Pepper."
]
},
{
recipe: "Luscious Lemon Passion Pots",
ingredients: [
"150g Condensed Milk",
"50ml Double Cream",
"Grated Zest and Juice of 1 large lemon",
"1 passion fruit"
],
directions: [
"Put the condensed milk, double cream and lemon zest and juice into a medium bowl and whisk until thick and fluffy. Spoon into small ramekins or coffee cups and chill until needed - or carry on with the recipe if you cannot wait",
"To serve, halve the passion fruit, scoop out the seeds and use to decorate the lemon pots."
]
},
{
recipe: "Chocolate Sauce",
ingredients: ["Plain Chocolate", "Water"],
directions: [
"Chop plain chocolate (at least 70% cocoa solids) and put it in a saucepan with 50ml water per 100g chocolate.",
"Heat slowly allowing the chocolate to melt, then stir until the sauce is smooth."
]
}
];
// Create Instance of Local Storage Manager
const LSM = new LocalStorageManager();
if (LSM.get("_derektypist_recipes") === null) {
LSM.set([
recipeIndex[0],
recipeIndex[1],
recipeIndex[2],
recipeIndex[3],
recipeIndex[4]
]);
}
// App
class App extends React.Component {
constructor() {
super();
this.state = {
showDialog: false,
recipes: LSM.get("_derektypist_recipes"),
dialogType: ""
};
this.getRecipeList = this.getRecipeList.bind(this);
this.showOnClick = this.showOnClick.bind(this);
this.showRecipe = this.showRecipe.bind(this);
this.defineDialogType = this.defineDialogType.bind(this);
this.addRecipe = this.addRecipe.bind(this);
this.editRecipe = this.editRecipe.bind(this);
this.deleteRecipe = this.deleteRecipe.bind(this);
this.populateFormData = this.populateFormData.bind(this);
this.toggleDialogDisplay = this.toggleDialogDisplay.bind(this);
}
componentDidMount() {
let recipes = LSM.get("_derektypist_recipes");
let recipe = recipes[0].recipe.toLowerCase();
this.setState({
currentRecipe: recipe
});
}
// Get Recipe List
getRecipeList() {
let allRecipes = [];
let recipes = LSM.get("_derektypist_recipes");
recipes.map((recipe) => {
allRecipes.push(recipe.recipe.toLowerCase());
});
return allRecipes;
}
// Show Recipe on Click
showOnClick(e) {
let currentRecipe = e.target.innerText;
currentRecipe = currentRecipe.toLowerCase().split(" ").join("-");
this.showRecipe(currentRecipe);
}
// Show Recipe
showRecipe(recipe) {
this.setState({
currentRecipe: recipe
});
}
// Define Dialog Type
defineDialogType(e) {
if (this.state.dialogType === "Add Recipe") {
this.addRecipe();
} else {
this.editRecipe();
}
}
// Add Recipe
addRecipe() {
let dialogIDs =
this.state.dialogType === "Add Recipe"
? ["add-recipe-name", "add-ingredients", "add-directions"]
: ["edit-recipe-name", "edit-ingredients", "edit-directions"];
let recipeName = document
.getElementById(dialogIDs[0])
.value.replace(/\s+/g, "-");
if (recipeName.endsWith("-")) recipeName = recipeName.slice(0, -1);
let myNewRecipe = {
recipe: recipeName,
ingredients: document.getElementById(dialogIDs[1]).value.split("\\"),
directions: document.getElementById(dialogIDs[2]).value.split("\\")
};
let recipes = LSM.get("_derektypist_recipes");
let recipeList = this.getRecipeList();
if (myNewRecipe.recipe === "") {
alert("Your recipe must have a name!");
} else if (recipeList.indexOf(recipeName.toLowerCase()) !== -1) {
recipeName = recipeName.replace("-", " ");
alert(`${recipeName} has already been added to the Recipe Box!`);
} else {
recipes.push(myNewRecipe);
LSM.set(recipes);
setTimeout(() => {
this.showRecipe(myNewRecipe.recipe.toLowerCase());
}, 10);
this.setState({
recipes: recipes,
showDialog: !this.state.showDialog
});
}
}
// Edit Recipe
editRecipe() {
let recipes = LSM.get("_derektypist_recipes");
recipes = recipes.filter((obj) => {
return obj.recipe.toLowerCase() !== this.state.editThis;
});
LSM.set(recipes);
this.addRecipe();
}
// Delete Recipe
deleteRecipe(e) {
if (
confirm(
`Are you sure you want to delete ${e.currentTarget.value.replace(
"-",
" "
)} from the recipe box?`
)
) {
// Handle Tab Focus after Delete
let tabToFocus;
let recipeList = this.getRecipeList();
recipeList.indexOf(e.currentTarget.value.toLowerCase()) >= 1
? tabToFocus =
recipeList[
recipeList.indexOf(e.currentTarget.value.toLowerCase()) - 1
]
: tabToFocus = recipeList[1];
this.showRecipe(tabToFocus);
// Delete the recipe
let recipes = LSM.get("_derektypist_recipes");
recipes = recipes.filter((obj) => {
return obj.recipe !== e.currentTarget.value;
});
// Reset Storage & State
LSM.set(recipes);
this.setState({
recipes: recipes
});
}
}
// Populate Form Data
populateFormData(str) {
// If dialog is being closed or opened by add button, do nothing
if (str === "") return;
else {
// If dialog is being opened to edit, find the correct recipe
let recipe;
for (let i = 0; i < this.state.recipes.length; i++) {
if (this.state.recipes[i].recipe === str) {
recipe = this.state.recipes[i];
}
}
// Wait for dialog to open then populate data
setTimeout(() => {
document.getElementById(
"edit-recipe-name"
).value = recipe.recipe.replace(/-/g, " ");
document.getElementById(
"edit-ingredients"
).value = recipe.ingredients.join(" \\ ");
document.getElementById(
"edit-directions"
).value = recipe.directions.join(" \\\n\n");
this.setState({ editThis: recipe.recipe.toLowerCase() });
}, 10);
}
}
// Toggle Dialog Display
toggleDialogDisplay(e) {
let indicator =
e.currentTarget.title !== undefined ? e.currentTarget.title : "";
this.setState({
dialogType: indicator,
showDialog: !this.state.showDialog
});
let val = e.currentTarget.value === undefined ? "" : e.currentTarget.value;
this.populateFormData(val);
}
render() {
let dialogText =
this.state.dialogType === "Add Recipe"
? ["Add a Recipe", "Add"]
: ["Edit Recipe", "Save"];
let dialogIDs =
this.state.dialogType === "Add Recipe"
? [
"add-recipe-name",
"add-ingredients",
"add-directions",
"add-submit",
"add-close"
]
: [
"edit-recipe-name",
"edit-ingredients",
"edit-directions",
"edit-submit",
"edit-close"
];
let dialogBox;
if (this.state.showDialog) {
dialogBox = (
<div className="dialog-box dialog-wrap">
<Dialog
dialogType={dialogText[0]}
buttonType={dialogText[1]}
nameID={dialogIDs[0]}
ingredientsID={dialogIDs[1]}
directionsID={dialogIDs[2]}
submitID={dialogIDs[3]}
closeID={dialogIDs[4]}
handleSubmit={this.defineDialogType}
handleClose={this.toggleDialogDisplay}
/>
</div>
);
}
return (
<div className="recipe-box-wrapper">
<div className="heading">
<i className="fab fa-free-code-camp"></i>
<div>Recipe Box</div>
</div>
{dialogBox}
<IndexView
handleClick={this.showOnClick}
contents={this.state.recipes}
/>
<RecipePane
contents={this.state.recipes}
displayRecipe={this.state.currentRecipe}
handleDelete={this.deleteRecipe}
handleEdit={this.toggleDialogDisplay}
/>
<div className="add-button">
<button
id="add-recipe"
title="Add Recipe"
onClick={this.toggleDialogDisplay}
>
<i className="fas fa-plus"></i>
</button>
</div>
</div>
);
}
}
// Dialog
class Dialog extends React.Component {
render() {
return (
<div className="dialog-box">
<h2>{this.props.dialogType}</h2>
<div className="input-title">Recipe</div>
<textarea
rows="1"
id={this.props.nameID}
placeholder="Recipe Name (e.g. Noodles)"
></textarea>
<div className="input-title">Ingredients</div>
<textarea
id={this.props.ingredientsID}
placeholder={`Separate each ingredient with a \\: \n\n1 Packet Noodles \n300ml Boiling Water `}
/>
<br />
<div className="input-title">Directions</div>
<textarea
id={this.props.directionsID}
placeholder={'Separate each step with a "\\": \n\nAdd the noodles, boiling water and the flavouring in a pan' + '\nBring to boil \nReduce to moderate heat and stir frequently for 4 minutes'}
/>
<br />
<button onClick={this.props.handleClose} className="corner-close">
<i className="fas fa-times" />
</button>
<button id={this.props.submitID} onClick={this.props.handleSubmit}>
{this.props.buttonType}
</button>
<button id={this.props.closeID} onClick={this.props.handleClose}>
Close
</button>
</div>
);
}
}
// Index View
class IndexView extends React.Component {
render() {
let recipes = this.props.contents;
let items = recipes.map((recipe, i) => (
<div
onClick={this.props.handleClick}
key={i}
id={i}
className="index-view-item"
id={"view-" + recipe.recipe.toLowerCase()}
>
{recipe.recipe.replace(/-/g, " ")}
</div>
));
return (
<div id="index-view">{items}</div>
)
}
}
// Recipe Pane
class RecipePane extends React.Component {
render() {
let recipes = this.props.contents;
let displayRecipe;
for (let i = 0; i < recipes.length; i++) {
if (recipes[i].recipe.toLowerCase() === this.props.displayRecipe) {
displayRecipe = (
<div id={recipes[i].recipe.toLowerCase()} className="recipe-view">
<div className="recipe-title">
<div className="recipe-view-name title-row">
{recipes[i].recipe.replace(/-/g, " ")}
</div>
<div className="title-row">
<button
id={"delete-" + recipes[i].recipe.toLowerCase()}
onClick={this.props.handleDelete}
title="Delete Recipe"
value={recipes[i].recipe}
>
<i className="fas fa-trash" />
</button>
<button
id={"edit-" + recipes[i].recipe.toLowerCase()}
onClick={this.props.handleEdit}
title="Edit Recipe"
value={recipes[i].recipe}
>
<i className="fas fa-edit" />
</button>
</div>
</div>
<div className="recipe-body">
<h4>Ingredients:</h4>
<ul className="ingredient list">
{recipes[i].ingredients.map((ingredient, j) => (
<li key={j}>{ingredient}</li>
))}
</ul>
<h4>Directions:</h4>
<ol className="directions list">
{recipes[i].directions.map((step, k) => (
<li key={k}>{step}</li>
))}
</ol>
</div>
</div>
);
}
}
return (
<div>
{displayRecipe}
</div>);
}
}
ReactDOM.render(<App />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
/* Variables */
:root {
--background: #009b77;
--dialog-main: #926aa6;
--body: #4b5335;
--titles: #34568b;
}
/* Body */
body {
background: var(--background);
color: white;
}
/* Recipe Title */
.recipe-title,
h4,
.index-view-item,
.dialog-box {
font-family: "Special Elite", cursive;
}
.recipe-box-wrapper {
margin: 20px 15%;
}
/* Heading */
.heading {
font-size: 40px;
text-align: center;
margin-bottom: 20px;
font-family: "Bebas Neue", cursive;
}
.heading div, i {
display: inline-block;
cursor: pointer;
letter-spacing: 0.1em;
}
/* Buttons */
button:hover {
cursor: pointer;
}
.unstyle-button {
background: none;
border: none;
outline: none;
font-size: 32px;
}
.unstyle-button:hover {
color: var(--background);
}
#index-view {
height: 95px;
overflow: auto;
margin-bottom: 5px;
border: 5px solid var(--titles);
border-bottom: 10px solid var(--titles);
border-top: 10px solid var(--titles);
}
#index-view .index-view-item {
padding: 7px;
cursor: pointer;
}
#index-view .index-view-item:nth-child(odd) {
background: var(--titles);
color: var(--background);
}
#index-view .index-view-item:hover {
color: var(--body);
}
.active {
background: var(--titles) !important;
border: 2px solid var(--titles) !important;
color: var(--background) !important;
}
.add-button {
background: var(--titles);
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
padding: 5px;
}
.add-button button .unstyle-button {
background: none;
border: none;
outline: none;
font-size: 32px;
}
/* Recipe Views and Titles */
.recipe-view .list {
max-width: 600px;
}
.recipe-view .recipe-body {
background: var(--body);
height: 330px;
overflow-y: scroll;
padding: 0 10px;
font-family: "Roboto", Arial, Verdana, sans-serif;
}
.recipe-view .recipe-body h4 {
letter-spacing: 0.1em;
}
.recipe-view .recipe-title {
background: var(--titles);
padding: 10px;
border-top-right-radius: 10px;
border-top-left-radius: 10px;
}
.recipe-view .recipe-title .title-row {
display: inline-block;
}
.recipe-view .recipe-title .unstyle-button {
background: none;
border: none;
outline: none;
font-size: 32px;
}
.recipe-view .recipe-title .fa-edit {
vertical-align: bottom;
}
.recipe-view .recipe-title div:first-of-type {
margin-right: 10px;
font-size: 30px;
font-weight: bold;
}
/* Dialog */
.dialog-wrap {
box-shadow: 2px 10px 10px 5000px rgba(0, 0, 0, 0.6) !important;
}
.dialog-box {
text-align: center;
position: fixed;
width: 500px;
min-width: 300px;
top: 50%;
left: 50%;
margin-left: -250px;
margin-top: -200px;
box-shadow: 3px 3px 10px black;
border-radius: 15px;
background: var(--dialog-main);
z-index: 1000;
}
.dialog-box .input-title {
margin: 5px;
}
.dialog-box textarea {
margin-bottom: 10px;
background: lightblue;
font-family: "Roboto", Arial, Verdana, sans-serif;
min-width: 300px;
min-height: 60px;
resize: vertical;
color: #22395d;
font-weight: bold;
font-size: 13px;
overflow: auto;
}
.dialog-box textarea:focus {
box-shadow: 0 0 5px var(--background);
outline: 1px solid var(--background);
border: 1px solid var(--background);
}
.dialog-box textarea:nth-of-type(3) {
min-height: 100px;
}
.dialog-box textarea:first-of-type {
resize: none;
min-height: 15px !important;
}
.dialog-box button:not(:first-of-type) {
margin-bottom: 15px;
}
.dialog-box button {
background: #294470;
border: none;
outline: none;
font-size: 15px;
font-family: "Bebas Neue", cursive;
margin-right: 8px;
border-radius: 5px;
}
.dialog-box .corner-close {
background: none;
border: none;
outline: none;
font-size: 24px !important;
color: #22395d;
position: absolute;
top: 5px;
right: 5px;
}
/* Media Queries */
@media screen and (max-width: 750px) {
.recipe-box-wrapper {
margin: 5%;
}
.heading {
font-size: 30px;
margin-bottom: 5px;
margin-top: 5px;
}
}
@media screen and (max-width: 350px) {
.dialog-box {
width: 100%;
height: 100%;
top: 0;
left: 0;
margin-left: 0;
margin-top: 0;
box-shadow: none;
border-radius: 0;
}
.recipe-box-wrapper {
margin: 0;
}
.heading {
font-size: 24px;
margin-bottom: 5px;
margin-top: 5px;
}
.recipe-body {
max-height: 200px;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment