Skip to content

Instantly share code, notes, and snippets.

@abenteuerzeit
Forked from codecademydev/AllRecipes.js
Last active January 12, 2023 10:20
Show Gist options
  • Save abenteuerzeit/5eb60648a50bc55f2288f9d52edfc2b3 to your computer and use it in GitHub Desktop.
Save abenteuerzeit/5eb60648a50bc55f2288f9d52edfc2b3 to your computer and use it in GitHub Desktop.
Favorite Recipes App
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>
);
};
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())
);
};
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;
import React from "react";
export default function FavoriteButton({ children, onClickHandler, icon }) {
return (
<button className="favorite-button" onClick={onClickHandler}>
<img className="heart-icon" alt="" src={icon} />
{children}
</button>
);
}
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>
)
}
};
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())
);
};
@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%;
}
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')
);
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>
);
}
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>
);
};
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;
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
}));
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