Skip to content

Instantly share code, notes, and snippets.

@codecademydev
Created August 18, 2022 15:02
Show Gist options
  • Save codecademydev/4534eb9a35b44228e4de3a23ebf079c7 to your computer and use it in GitHub Desktop.
Save codecademydev/4534eb9a35b44228e4de3a23ebf079c7 to your computer and use it in GitHub Desktop.
Codecademy export
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())
);
};
import React from 'react';
import { AllRecipes } from '../features/allRecipes/AllRecipes.js';
import { SearchTerm } from '../features/searchTerm/SearchTerm.js';
import { FavoriteRecipes } from '../features/favoriteRecipes/FavoriteRecipes.js';
export function App() {
return (
<main>
<section>
<SearchTerm />
</section>
<section>
<h2>Favorite Recipes</h2>
<FavoriteRecipes />
</section>
<hr />
<section>
<h2>All Recipes</h2>
<AllRecipes />
</section>
</main>
)
}
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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment