-
-
Save abenteuerzeit/5eb60648a50bc55f2288f9d52edfc2b3 to your computer and use it in GitHub Desktop.
Favorite Recipes App
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect } from 'react'; | |
import { useSelector, useDispatch } from 'react-redux'; | |
import { addRecipe } from '../favoriteRecipes/favoriteRecipesSlice.js'; | |
import { loadData, selectFilteredAllRecipes } from './allRecipesSlice.js'; | |
import FavoriteButton from "../../components/FavoriteButton"; | |
import Recipe from "../../components/Recipe"; | |
const favoriteIconURL = 'https://static-assets.codecademy.com/Courses/Learn-Redux/Recipes-App/icons/favorite.svg'; | |
export const AllRecipes = () => { | |
const allRecipes = useSelector(selectFilteredAllRecipes); | |
const dispatch = useDispatch(); | |
const onFirstRender = () => { | |
dispatch(loadData()); | |
} | |
useEffect(onFirstRender, []); | |
const onAddRecipeHandler = (recipe) => { | |
dispatch(addRecipe(recipe)); | |
}; | |
return ( | |
<div className="recipes-container"> | |
{allRecipes.map((recipe) => ( | |
<Recipe recipe={recipe} key={recipe.id}> | |
<FavoriteButton | |
onClickHandler={() => onAddRecipeHandler(recipe)} | |
icon={favoriteIconURL} | |
> | |
Add to Favorites | |
</FavoriteButton> | |
</Recipe> | |
))} | |
</div> | |
); | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import allRecipesData from '../../../data.js' | |
import { selectSearchTerm } from '../searchTerm/searchTermSlice.js'; | |
export const loadData = () => { | |
return { | |
type: 'allRecipes/loadData', | |
payload: allRecipesData | |
} | |
} | |
const initialState = []; | |
export const allRecipesReducer = (allRecipes = initialState, action) => { | |
switch (action.type) { | |
case 'allRecipes/loadData': | |
return action.payload; | |
case 'favoriteRecipes/addRecipe': | |
return allRecipes.filter(recipe => recipe.id !== action.payload.id); | |
case 'favoriteRecipes/removeRecipe': | |
return [...allRecipes, action.payload] | |
default: | |
return allRecipes; | |
} | |
} | |
export const selectAllRecipes = (state) => state.allRecipes; | |
export const selectFilteredAllRecipes = (state) => { | |
const allRecipes = selectAllRecipes(state); | |
const searchTerm = selectSearchTerm(state); | |
return allRecipes.filter((recipe) => | |
recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const allRecipesData = [ | |
{ id: 0, name: 'Biscuits', img: 'img/biscuits.jpg'}, | |
{ id: 1, name: 'Bulgogi', img: 'img/bulgogi.jpg'}, | |
{ id: 2, name: 'Calamari', img: 'img/calamari.jpg'}, | |
{ id: 3, name: 'Ceviche', img: 'img/ceviche.jpg'}, | |
{ id: 4, name: 'Cheeseburger', img: 'img/cheeseburger.jpg'}, | |
{ id: 5, name: 'Churrasco', img: 'img/churrasco.jpg'}, | |
{ id: 6, name: 'Dumplings', img: 'img/dumplings.jpg'}, | |
{ id: 7, name: 'Fish & Chips', img: 'img/fishnchips.jpg'}, | |
{ id: 8, name: 'Hummus', img: 'img/hummus.jpg'}, | |
{ id: 9, name: 'Masala Dosa', img: 'img/masaladosa.jpg'}, | |
{ id: 10, name: 'Pad Thai', img: 'img/padthai.jpg'}, | |
]; | |
export default allRecipesData; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { useSelector, useDispatch } from 'react-redux'; | |
import { removeRecipe, selectFilteredFavoriteRecipes } from './favoriteRecipesSlice.js'; | |
import FavoriteButton from "../../components/FavoriteButton"; | |
import Recipe from "../../components/Recipe"; | |
const unfavoriteIconUrl = 'https://static-assets.codecademy.com/Courses/Learn-Redux/Recipes-App/icons/unfavorite.svg'; | |
export const FavoriteRecipes = () =>{ | |
const favoriteRecipes = useSelector(selectFilteredFavoriteRecipes); | |
const dispatch = useDispatch(); | |
const onRemoveRecipeHandler = (recipe) => { | |
dispatch(removeRecipe(recipe)); | |
}; | |
return ( | |
<div className="recipes-container"> | |
{favoriteRecipes.map(createRecipeComponent)} | |
</div> | |
); | |
// Helper Function | |
function createRecipeComponent(recipe) { | |
return ( | |
<Recipe recipe={recipe} key={recipe.id}> | |
<FavoriteButton | |
onClickHandler={() => onRemoveRecipeHandler(recipe)} | |
icon={unfavoriteIconUrl} | |
> | |
Remove Favorite | |
</FavoriteButton> | |
</Recipe> | |
) | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { selectSearchTerm } from '../searchTerm/searchTermSlice.js'; | |
const initialState = []; | |
export const favoriteRecipesReducer = (favoriteRecipes = initialState, action) => { | |
switch (action.type) { | |
case 'favoriteRecipes/addRecipe': | |
return [...favoriteRecipes, action.payload] | |
case 'favoriteRecipes/removeRecipe': | |
return favoriteRecipes.filter(recipe => recipe.id !== action.payload.id) | |
default: | |
return favoriteRecipes; | |
} | |
} | |
export function addRecipe(recipe) { | |
return { | |
type: 'favoriteRecipes/addRecipe', | |
payload: recipe | |
} | |
} | |
export function removeRecipe(recipe) { | |
return { | |
type: 'favoriteRecipes/removeRecipe', | |
payload: recipe | |
} | |
} | |
export const selectFavoriteRecipes = (state) => state.favoriteRecipes; | |
export const selectFilteredFavoriteRecipes = (state) => { | |
const favoriteRecipes = selectFavoriteRecipes(state); | |
const searchTerm = selectSearchTerm(state); | |
return favoriteRecipes.filter((recipe) => | |
recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import url("https://fonts.googleapis.com/css2?family=Oxygen:wght@400;700&display=swap"); | |
html, | |
body, | |
#root, | |
#app { | |
height: 100%; | |
margin: 15px; | |
font-family: "Oxygen", sans-serif; | |
} | |
#app { | |
display: flex; | |
flex-direction: column; | |
color: #141c3a; | |
} | |
#search-container { | |
padding: 33px 36px 0px 36px; | |
position: relative; | |
} | |
#search { | |
width: 100%; | |
padding: 9px 25px 9px 40px; | |
border-radius: 16.5px; | |
border: solid 1px #141c3a; | |
background-color: #ffffff; | |
outline: none; | |
box-sizing: border-box; | |
} | |
#search-icon { | |
opacity: 0.5; | |
position: absolute; | |
top: 42px; | |
left: 53px; | |
} | |
#search::placeholder { | |
font-size: 14px; | |
font-weight: bold; | |
color: #8a8e9d; | |
} | |
#search-clear-button { | |
position: absolute; | |
right: 42px; | |
top: 36px; | |
bottom: 3px; | |
border: 0; | |
background: none; | |
color: #141c3a; | |
margin: 0; | |
padding: 0 10px; | |
border-radius: 100px; | |
z-index: 2; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
} | |
.header { | |
font-size: 32px; | |
font-weight: bold; | |
color: #141c3a; | |
margin: 0; | |
margin-bottom: 16px; | |
} | |
.recipe { | |
padding: 16px 13px 16px 14px; | |
border-radius: 6px; | |
border: solid 1px #141c3a; | |
background-color: none; | |
text-align: left; | |
display: grid; | |
grid-template-columns: 1fr; | |
grid-template-rows: 1fr; | |
} | |
.recipe:hover, | |
.recipe:focus, | |
.recipe:focus-within { | |
background: #141c3a; | |
color: #ffffff; | |
} | |
.recipe:hover > .favorite-button, | |
.recipe:focus > .favorite-button, | |
.recipe:focus-within > .favorite-button { | |
display: flex; | |
} | |
.recipe:hover .recipe-image, | |
.recipe:focus .recipe-image, | |
.recipe:focus-within .recipe-image { | |
opacity: 0.7; | |
} | |
.recipe-name { | |
font-size: 20px; | |
font-weight: bold; | |
margin: 0; | |
margin-bottom: 8px; | |
} | |
.recipes-container { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | |
grid-column-gap: 12px; | |
grid-row-gap: 12px; | |
} | |
.recipe-container { | |
grid-row-start: 1; | |
grid-column-start: 1; | |
} | |
#recipes-wrapper { | |
flex-grow: 1; | |
display: flex; | |
flex-direction: column; | |
} | |
.recipes-section { | |
padding: 32px 36px; | |
} | |
#favorite-recipes { | |
background-color: #f2f2f2; | |
flex-grow: 1; | |
} | |
.favorite-button { | |
padding: 5px 7px; | |
border-radius: 4px; | |
background-color: #efd9ca; | |
font-weight: bold; | |
color: #141c3a; | |
font-size: 12px; | |
border: none; | |
cursor: pointer; | |
display: none; | |
align-items: center; | |
grid-row-start: 1; | |
grid-column-start: 1; | |
margin: auto; | |
z-index: 1; | |
} | |
.heart-icon { | |
color: #fd4d3f; | |
margin-right: 5px; | |
} | |
.spinner { | |
display: block; | |
margin: 0 auto; | |
animation: rotation 2s infinite linear; | |
} | |
@keyframes rotation { | |
from { | |
transform: rotate(0deg); | |
} | |
to { | |
transform: rotate(359deg); | |
} | |
} | |
#error-wrapper { | |
text-align: center; | |
} | |
.image-container { | |
width: 100%; | |
height: 72px; | |
overflow: hidden; | |
} | |
.recipe-image { | |
width: 100%; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { Provider } from 'react-redux'; | |
import { App } from './app/App.js'; | |
import { store } from './app/store.js'; | |
ReactDOM.render( | |
<Provider store={store}> | |
<App /> | |
</Provider>, | |
document.getElementById('root') | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
export default function Recipe({ recipe, children }) { | |
return ( | |
<div key={recipe.id} className="recipe" tabIndex={0}> | |
<span className="recipe-container"> | |
<h3 className="recipe-name">{recipe.name}</h3> | |
<div className="image-container"> | |
<img src={recipe.img} alt="" className="recipe-image" /> | |
</div> | |
</span> | |
{children} | |
</div> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { useSelector, useDispatch } from 'react-redux'; | |
import { setSearchTerm, clearSearchTerm, selectSearchTerm } from './searchTermSlice.js'; | |
const searchIconUrl = 'https://static-assets.codecademy.com/Courses/Learn-Redux/Recipes-App/icons/search.svg'; | |
const clearIconUrl = 'https://static-assets.codecademy.com/Courses/Learn-Redux/Recipes-App/icons/clear.svg'; | |
export const SearchTerm = () => { | |
const searchTerm = useSelector(selectSearchTerm); | |
const dispatch = useDispatch(); | |
const onSearchTermChangeHandler = (e) => { | |
const userInput = e.target.value; | |
dispatch(setSearchTerm(userInput)); | |
}; | |
const onClearSearchTermHandler = () => { | |
dispatch(clearSearchTerm()); | |
}; | |
return ( | |
<div id="search-container"> | |
<img id="search-icon" alt="" src={searchIconUrl} /> | |
<input | |
id="search" | |
type="text" | |
value={searchTerm} | |
onChange={onSearchTermChangeHandler} | |
placeholder="Search recipes" | |
/> | |
{searchTerm.length > 0 && ( | |
<button | |
onClick={onClearSearchTermHandler} | |
type="button" | |
id="search-clear-button" | |
> | |
<img src={clearIconUrl} alt="" /> | |
</button> | |
)} | |
</div> | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const initialState = '' | |
export const searchTermReducer = (state = initialState, action) => { | |
switch (action.type) { | |
case 'searchTerm/setSearchTerm': | |
return action.payload; | |
case 'searchTerm/clearSearchTerm': | |
return ''; | |
default: | |
return state; | |
} | |
} | |
export function setSearchTerm(term) { | |
return { | |
type: 'searchTerm/setSearchTerm', | |
payload: term | |
} | |
} | |
export function clearSearchTerm() { | |
return { | |
type: 'searchTerm/clearSearchTerm' | |
} | |
} | |
export const selectSearchTerm = (state) => state.searchTerm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createStore, combineReducers } from 'redux'; | |
import { favoriteRecipesReducer } from '../features/favoriteRecipes/favoriteRecipesSlice.js'; | |
import { searchTermReducer } from '../features/searchTerm/searchTermSlice.js'; | |
import { allRecipesReducer } from '../features/allRecipes/allRecipesSlice.js'; | |
export const store = createStore(combineReducers({ | |
favoriteRecipes: favoriteRecipesReducer, | |
searchTerm: searchTermReducer, | |
allRecipes: allRecipesReducer | |
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
console.log = function() {}; | |
const { expect } = require('chai'); | |
const rewire = require('rewire'); | |
describe('', function() { | |
it('', function() { | |
let appModule; | |
try { | |
appModule = rewire('../searchSlice.js'); | |
} catch (e) { | |
expect(true, 'Try checking your code again. You likely have a syntax error.').to.equal(false); | |
} | |
let varLearnerDeclares; | |
let learnerVariableName = 'selectSearchTerm'; | |
try { | |
varLearnerDeclares = appModule.__get__(learnerVariableName); | |
} catch (e) { | |
expect(true, `Did you declare the \`${learnerVariableName}\` selector?`).to.equal(false); | |
} | |
// These are just two possible examples, you can use the whole Chai expect API | |
expect(varLearnerDeclares, `Did you declare \`${learnerVariableName}\` to be a function?`).to.be.a('function'); | |
expect(varLearnerDeclares({searchTerm: 'Hello'}), 'Did you return the state\'s `searchTerm` value?').to.deep.equal('Hello'); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment