commit
472fee2c5c
32 changed files with 17667 additions and 0 deletions
-
23.gitignore
-
35README.md
-
16397package-lock.json
-
44package.json
-
1public/_redirects
-
BINpublic/apple-touch-icon.png
-
BINpublic/favicon.ico
-
39public/index.html
-
BINpublic/logo192.png
-
BINpublic/logo512.png
-
25public/manifest.json
-
3public/robots.txt
-
34src/App.js
-
50src/components/Footer.js
-
47src/components/Navbar.js
-
61src/components/RecipeCard.js
-
13src/components/RecipeList.js
-
32src/components/SearchBar.js
-
40src/components/SearchResults.js
-
37src/components/useAxios.js
-
46src/contexts/BookmarkContext.js
-
1src/images/bg-hero.svg
-
BINsrc/images/img-not-found.png
-
10src/images/logo.svg
-
1src/images/no-data.svg
-
1src/images/page-not-found.svg
-
520src/index.css
-
12src/index.js
-
33src/pages/Bookmark.js
-
100src/pages/Detail.js
-
44src/pages/Home.js
-
18src/pages/NotFound.js
@ -0,0 +1,23 @@ |
|||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
||||
|
|
||||
|
# dependencies |
||||
|
/node_modules |
||||
|
/.pnp |
||||
|
.pnp.js |
||||
|
|
||||
|
# testing |
||||
|
/coverage |
||||
|
|
||||
|
# production |
||||
|
/build |
||||
|
|
||||
|
# misc |
||||
|
.DS_Store |
||||
|
.env.local |
||||
|
.env.development.local |
||||
|
.env.test.local |
||||
|
.env.production.local |
||||
|
|
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
@ -0,0 +1,35 @@ |
|||||
|
# Gorecipe |
||||
|
|
||||
|
Aplikasi web serderhana untuk mencari resep masakan khas indonesia yang dibuat menggunakan ReactJS. |
||||
|
|
||||
|
<p align="center"> |
||||
|
<br> |
||||
|
<img src="https://i.ibb.co/fD4gkJ8/home.png" border="0" width="350"> |
||||
|
</p> |
||||
|
|
||||
|
## Setup |
||||
|
|
||||
|
1. Download ZIP / Clone repo gorecipe |
||||
|
2. Buka Appnya di text editor vscode / lainnya |
||||
|
3. Buka terminal, pastikan arah pointernya mengarah ke folder gorecipe yg telah didownload |
||||
|
4. kemudian jalankan perintah `npm install` untuk menginstall semua dependencies |
||||
|
5. jalankan perintah `npm start` untuk menjalankan Aplikasinya di mode development |
||||
|
6. Buka [http://localhost:3000](http://localhost:3000) untuk melihat hasil aplikasi webnya. |
||||
|
|
||||
|
## Fitur |
||||
|
|
||||
|
- Cari resep |
||||
|
- Toggle save resep |
||||
|
- Routing halaman |
||||
|
|
||||
|
## Dibuat menggunakan |
||||
|
|
||||
|
- HTML |
||||
|
- CSS |
||||
|
- JavaScript |
||||
|
- ReactJS |
||||
|
- React Router |
||||
|
|
||||
|
### Thanks to |
||||
|
|
||||
|
Terima kasih untuk [farizdotid](https://github.com/farizdotid) & [tomorisakura](https://github.com/tomorisakura) telah menyediakan API Resep masakan indonesia |
||||
16397
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,44 @@ |
|||||
|
{ |
||||
|
"name": "recipes-app", |
||||
|
"version": "0.1.0", |
||||
|
"private": true, |
||||
|
"dependencies": { |
||||
|
"@testing-library/jest-dom": "^5.14.1", |
||||
|
"@testing-library/react": "^11.2.7", |
||||
|
"@testing-library/user-event": "^12.8.3", |
||||
|
"axios": "^0.21.1", |
||||
|
"react": "^17.0.2", |
||||
|
"react-dom": "^17.0.2", |
||||
|
"react-icons": "^4.2.0", |
||||
|
"react-router-dom": "^5.2.0", |
||||
|
"react-scripts": "4.0.3", |
||||
|
"web-vitals": "^1.1.2" |
||||
|
}, |
||||
|
"scripts": { |
||||
|
"start": "react-scripts start", |
||||
|
"build": "react-scripts build", |
||||
|
"test": "react-scripts test", |
||||
|
"eject": "react-scripts eject" |
||||
|
}, |
||||
|
"eslintConfig": { |
||||
|
"extends": [ |
||||
|
"react-app", |
||||
|
"react-app/jest" |
||||
|
] |
||||
|
}, |
||||
|
"browserslist": { |
||||
|
"production": [ |
||||
|
">0.2%", |
||||
|
"not dead", |
||||
|
"not op_mini all" |
||||
|
], |
||||
|
"development": [ |
||||
|
"last 1 chrome version", |
||||
|
"last 1 firefox version", |
||||
|
"last 1 safari version" |
||||
|
] |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/react-router-dom": "^5.1.8" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
/* /index.html 200 |
||||
|
After Width: 180 | Height: 180 | Size: 7.8 KiB |
@ -0,0 +1,39 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="utf-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||
|
<meta name="theme-color" content="#000000" /> |
||||
|
<meta |
||||
|
name="description" |
||||
|
content="Website yang menyediakan berbagai macam resep makanan indonesia" |
||||
|
/> |
||||
|
<!-- Favicons --> |
||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> |
||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" /> |
||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||
|
<!-- Google Fonts --> |
||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" /> |
||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
||||
|
<link |
||||
|
href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap" |
||||
|
rel="stylesheet" |
||||
|
/> |
||||
|
|
||||
|
<title>Gorecipe</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||
|
<div id="root"></div> |
||||
|
<!-- |
||||
|
This HTML file is a template. |
||||
|
If you open it directly in the browser, you will see an empty page. |
||||
|
|
||||
|
You can add webfonts, meta tags, or analytics to this file. |
||||
|
The build step will place the bundled scripts into the <body> tag. |
||||
|
|
||||
|
To begin the development, run `npm start` or `yarn start`. |
||||
|
To create a production bundle, use `npm run build` or `yarn build`. |
||||
|
--> |
||||
|
</body> |
||||
|
</html> |
||||
|
After Width: 192 | Height: 192 | Size: 8.4 KiB |
|
After Width: 512 | Height: 512 | Size: 23 KiB |
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"short_name": "Recipes App", |
||||
|
"name": "Aplikasi web sederhana untuk mencari resep makanan indonesia", |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "favicon.ico", |
||||
|
"sizes": "64x64 32x32 24x24 16x16", |
||||
|
"type": "image/x-icon" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "logo192.png", |
||||
|
"type": "image/png", |
||||
|
"sizes": "192x192" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "logo512.png", |
||||
|
"type": "image/png", |
||||
|
"sizes": "512x512" |
||||
|
} |
||||
|
], |
||||
|
"start_url": ".", |
||||
|
"display": "standalone", |
||||
|
"theme_color": "#000000", |
||||
|
"background_color": "#ffffff" |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
# https://www.robotstxt.org/robotstxt.html |
||||
|
User-agent: * |
||||
|
Disallow: |
||||
@ -0,0 +1,34 @@ |
|||||
|
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; |
||||
|
import Navbar from "./components/Navbar"; |
||||
|
import Footer from "./components/Footer"; |
||||
|
import Home from "./pages/Home"; |
||||
|
import Detail from "./pages/Detail"; |
||||
|
import Bookmark from "./pages/Bookmark"; |
||||
|
import NotFound from "./pages/NotFound"; |
||||
|
|
||||
|
const App = () => { |
||||
|
return ( |
||||
|
<Router> |
||||
|
<Navbar /> |
||||
|
<main> |
||||
|
<Switch> |
||||
|
<Route exact path="/"> |
||||
|
<Home /> |
||||
|
</Route> |
||||
|
<Route path="/recipe/:key"> |
||||
|
<Detail /> |
||||
|
</Route> |
||||
|
<Route path="/bookmark"> |
||||
|
<Bookmark /> |
||||
|
</Route> |
||||
|
<Route path="*"> |
||||
|
<NotFound /> |
||||
|
</Route> |
||||
|
</Switch> |
||||
|
</main> |
||||
|
<Footer /> |
||||
|
</Router> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default App; |
||||
@ -0,0 +1,50 @@ |
|||||
|
import { FaFacebook, FaInstagram, FaGithub } from "react-icons/fa"; |
||||
|
import logo from "../images/logo.svg"; |
||||
|
|
||||
|
const Footer = () => { |
||||
|
return ( |
||||
|
<footer> |
||||
|
<a href="/" className="brand"> |
||||
|
<img src={logo} alt="Logo" className="brand-img" /> |
||||
|
gorecipe |
||||
|
</a> |
||||
|
<ul className="scm-list"> |
||||
|
<li> |
||||
|
<a |
||||
|
className="scm-icon" |
||||
|
href="https://facebook.com/people/Indra-Adi-Kusuma/100009019826862/" |
||||
|
rel="noopener noreferrer" |
||||
|
target="_blank" |
||||
|
> |
||||
|
<FaFacebook /> |
||||
|
</a> |
||||
|
</li> |
||||
|
<li> |
||||
|
<a |
||||
|
className="scm-icon" |
||||
|
href="https://www.instagram.com/mrx.indra/" |
||||
|
rel="noopener noreferrer" |
||||
|
target="_blank" |
||||
|
> |
||||
|
<FaInstagram /> |
||||
|
</a> |
||||
|
</li> |
||||
|
<li> |
||||
|
<a |
||||
|
className="scm-icon" |
||||
|
href="https://github.com/indraAK" |
||||
|
rel="noopener noreferrer" |
||||
|
target="_blank" |
||||
|
> |
||||
|
<FaGithub /> |
||||
|
</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
<p className="copyright-text"> |
||||
|
<small>© Copyright 2021 gorecipe</small> |
||||
|
</p> |
||||
|
</footer> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Footer; |
||||
@ -0,0 +1,47 @@ |
|||||
|
import { useContext } from "react"; |
||||
|
import { NavLink } from "react-router-dom"; |
||||
|
import logo from "../images/logo.svg"; |
||||
|
import { BookmarkContext } from "../contexts/BookmarkContext"; |
||||
|
|
||||
|
const Navbar = () => { |
||||
|
const { recipes } = useContext(BookmarkContext); |
||||
|
|
||||
|
return ( |
||||
|
<header> |
||||
|
<div className="container"> |
||||
|
<nav className="navbar"> |
||||
|
<NavLink to="/" className="brand"> |
||||
|
<img src={logo} alt="Logo" className="brand-img" /> |
||||
|
gorecipe |
||||
|
</NavLink> |
||||
|
<ul className="nav-list"> |
||||
|
<li className="nav-item"> |
||||
|
<NavLink |
||||
|
to="/" |
||||
|
activeClassName="selected" |
||||
|
exact |
||||
|
className="nav-link" |
||||
|
> |
||||
|
Resep |
||||
|
</NavLink> |
||||
|
</li> |
||||
|
<li className="nav-item nav-item-group"> |
||||
|
<NavLink |
||||
|
to="/bookmark" |
||||
|
activeClassName="selected" |
||||
|
className="nav-link" |
||||
|
> |
||||
|
Disimpan |
||||
|
</NavLink> |
||||
|
<span className="favorite-counter"> |
||||
|
{recipes.length && recipes.length} |
||||
|
</span> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
</header> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Navbar; |
||||
@ -0,0 +1,61 @@ |
|||||
|
import { useContext } from "react"; |
||||
|
import { Link, useRouteMatch } from "react-router-dom"; |
||||
|
import { BiBarChartAlt, BiDish, BiTime, BiCheck } from "react-icons/bi"; |
||||
|
import { BookmarkContext } from "../contexts/BookmarkContext"; |
||||
|
|
||||
|
const RecipeCard = ({ recipe }) => { |
||||
|
const { dispatch, isSaved } = useContext(BookmarkContext); |
||||
|
const routeMatch = useRouteMatch("/bookmark"); |
||||
|
|
||||
|
return ( |
||||
|
<li> |
||||
|
<div className="recipe recipe-card"> |
||||
|
<img src={recipe.thumb} alt="" /> |
||||
|
<div className="recipe-body"> |
||||
|
<Link |
||||
|
to={`/recipe/${recipe.key}`} |
||||
|
className="recipe-title recipe-link" |
||||
|
> |
||||
|
{recipe.title} |
||||
|
</Link> |
||||
|
<div className="recipe-info"> |
||||
|
<div className="icon-group"> |
||||
|
<BiBarChartAlt className="icon" /> |
||||
|
<p>{recipe.dificulty ? recipe.dificulty : recipe.difficulty}</p> |
||||
|
</div> |
||||
|
<div className="icon-group"> |
||||
|
<BiDish className="icon" /> |
||||
|
<p> |
||||
|
{recipe.portion && recipe.portion} |
||||
|
{recipe.serving && recipe.serving} |
||||
|
{recipe.servings && recipe.servings} |
||||
|
</p> |
||||
|
</div> |
||||
|
<div className="icon-group"> |
||||
|
<BiTime className="icon" /> |
||||
|
<p>{recipe.times}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
{routeMatch && ( |
||||
|
<button |
||||
|
className="btn btn-primary" |
||||
|
style={{ marginTop: "4rem" }} |
||||
|
onClick={() => dispatch({ type: "TOGGLE_SAVE", key: recipe.key })} |
||||
|
> |
||||
|
{isSaved(recipe.key) ? ( |
||||
|
<span style={{ display: "flex", alignItems: "center" }}> |
||||
|
<BiCheck fontSize="20px" style={{ marginRight: "5px" }} />{" "} |
||||
|
Disimpan |
||||
|
</span> |
||||
|
) : ( |
||||
|
<span>Simpan Resep</span> |
||||
|
)} |
||||
|
</button> |
||||
|
)} |
||||
|
</div> |
||||
|
</div> |
||||
|
</li> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default RecipeCard; |
||||
@ -0,0 +1,13 @@ |
|||||
|
import RecipeCard from "./RecipeCard"; |
||||
|
|
||||
|
const RecipeList = ({ recipes }) => { |
||||
|
return ( |
||||
|
<ul className="recipe-list"> |
||||
|
{recipes.map((recipe, idx) => ( |
||||
|
<RecipeCard recipe={recipe} key={idx} /> |
||||
|
))} |
||||
|
</ul> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default RecipeList; |
||||
@ -0,0 +1,32 @@ |
|||||
|
import { useState } from "react"; |
||||
|
import { BiSearch } from "react-icons//bi"; |
||||
|
import { useHistory } from "react-router-dom"; |
||||
|
|
||||
|
const SearchBar = ({ value }) => { |
||||
|
const [term, setTerm] = useState(value); |
||||
|
const history = useHistory(); |
||||
|
|
||||
|
const handleSubmit = (event) => { |
||||
|
event.preventDefault(); |
||||
|
history.push(`?search=${term}`); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<form onSubmit={handleSubmit}> |
||||
|
<div className="input-group"> |
||||
|
<BiSearch className="search-icon" /> |
||||
|
<input |
||||
|
type="search" |
||||
|
placeholder="Hari ini mau resep apa?" |
||||
|
value={term} |
||||
|
onChange={(e) => setTerm(e.target.value)} |
||||
|
/> |
||||
|
<button type="submit" className="btn btn-search"> |
||||
|
<BiSearch color="#fff" size="1.7rem" /> |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default SearchBar; |
||||
@ -0,0 +1,40 @@ |
|||||
|
import React, { useRef, useMemo } from "react"; |
||||
|
import useAxios from "./useAxios"; |
||||
|
import RecipeList from "./RecipeList"; |
||||
|
|
||||
|
const SearchResults = ({ keyword }) => { |
||||
|
const PROXY_URL = "https://cors.bridged.cc/"; |
||||
|
const searchResultsRef = useRef(); |
||||
|
const { |
||||
|
data: recipes, |
||||
|
isLoading, |
||||
|
error, |
||||
|
} = useAxios( |
||||
|
PROXY_URL + |
||||
|
`https://masak-apa.tomorisakura.vercel.app/api/search/?q=${keyword}` |
||||
|
); |
||||
|
|
||||
|
useMemo(() => { |
||||
|
if (keyword && recipes) { |
||||
|
const offsetTop = searchResultsRef.current.offsetTop; |
||||
|
window.scrollTo({ top: offsetTop, behavior: "smooth" }); |
||||
|
} |
||||
|
}, [keyword, recipes]); |
||||
|
|
||||
|
return ( |
||||
|
<section className="recipes recipes-search-results" ref={searchResultsRef}> |
||||
|
<div className="container"> |
||||
|
<h2 className="heading-2"> |
||||
|
Hasil pencarian untuk{" "} |
||||
|
<span style={{ color: "#51C273" }}>{keyword} </span> |
||||
|
{recipes && <span className="amount">({recipes.length})</span>} |
||||
|
</h2> |
||||
|
{error && <p>{error}</p>} |
||||
|
{isLoading && <p>Tunggu sebentar...</p>} |
||||
|
{recipes && <RecipeList recipes={recipes} />} |
||||
|
</div> |
||||
|
</section> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default SearchResults; |
||||
@ -0,0 +1,37 @@ |
|||||
|
import { useState, useEffect } from "react"; |
||||
|
import axios from "axios"; |
||||
|
|
||||
|
const useAxios = (url) => { |
||||
|
const [data, setData] = useState(null); |
||||
|
const [isLoading, setIsLoading] = useState(false); |
||||
|
const [error, setError] = useState(null); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const cancelToken = axios.CancelToken.source(); |
||||
|
setTimeout(() => { |
||||
|
const fetchRecieps = async () => { |
||||
|
try { |
||||
|
setIsLoading(true); |
||||
|
const response = await axios.get(url, { |
||||
|
cancelToken: cancelToken.token, |
||||
|
}); |
||||
|
const results = response.data.results; |
||||
|
setData(results); |
||||
|
} catch (err) { |
||||
|
setError(err.message); |
||||
|
} finally { |
||||
|
setIsLoading(false); |
||||
|
} |
||||
|
}; |
||||
|
fetchRecieps(); |
||||
|
}, 1000); |
||||
|
|
||||
|
return () => { |
||||
|
cancelToken.cancel(); |
||||
|
}; |
||||
|
}, [url]); |
||||
|
|
||||
|
return { data, isLoading, error }; |
||||
|
}; |
||||
|
|
||||
|
export default useAxios; |
||||
@ -0,0 +1,46 @@ |
|||||
|
import { createContext, useReducer, useEffect } from "react"; |
||||
|
|
||||
|
const BookmarkContext = createContext(); |
||||
|
|
||||
|
const recipeIsSaved = (key, recipes) => { |
||||
|
return recipes.some((recipe) => recipe.key === key); |
||||
|
}; |
||||
|
|
||||
|
const reducer = (recipes, action) => { |
||||
|
switch (action.type) { |
||||
|
case "TOGGLE_SAVE": |
||||
|
let newRecipes = [...recipes]; |
||||
|
if (!recipeIsSaved(action.key, recipes)) { |
||||
|
newRecipes.push(action.payload); |
||||
|
} else { |
||||
|
newRecipes = recipes.filter((recipe) => recipe.key !== action.key); |
||||
|
} |
||||
|
return newRecipes; |
||||
|
default: |
||||
|
return recipes; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const BookmarkContextProvider = (props) => { |
||||
|
const [recipes, dispatch] = useReducer(reducer, [], () => { |
||||
|
const localData = localStorage.getItem("recipes"); |
||||
|
|
||||
|
return localData ? JSON.parse(localData) : []; |
||||
|
}); |
||||
|
const isSaved = (key) => { |
||||
|
return recipeIsSaved(key, recipes); |
||||
|
}; |
||||
|
|
||||
|
// simpan data recipe ke localStorage
|
||||
|
useEffect(() => { |
||||
|
localStorage.setItem("recipes", JSON.stringify(recipes)); |
||||
|
}, [recipes]); |
||||
|
|
||||
|
return ( |
||||
|
<BookmarkContext.Provider value={{ recipes, dispatch, isSaved }}> |
||||
|
{props.children} |
||||
|
</BookmarkContext.Provider> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export { BookmarkContext, BookmarkContextProvider }; |
||||
1
src/images/bg-hero.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
|
After Width: 580 | Height: 435 | Size: 10 KiB |
@ -0,0 +1,10 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<svg viewBox="60.019 2.659 19.2 19.2" xmlns="http://www.w3.org/2000/svg"> |
||||
|
<g id="logo/24/text/color/gofood" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="matrix(1, 0, 0, 1, 60.018517, 0.25926)"> |
||||
|
<g id="Group" transform="translate(0.000000, 1.461720)"> |
||||
|
<path d="M8.91328,9.77892 C8.86928,10.87012 8.29008,11.80612 7.45008,12.31732 C7.04608,12.56292 6.75968,12.97812 6.73648,13.47172 L6.66208,15.09092 C6.64208,15.52852 6.30128,15.87172 5.88928,15.87172 C5.47568,15.87172 5.13488,15.52772 5.11568,15.08932 L5.04048,13.41252 C5.01888,12.92052 4.74288,12.49812 4.34448,12.24452 C3.47248,11.68932 2.89328,10.70372 2.86128,9.54292 C2.83088,8.45732 2.90368,7.35412 3.07728,6.26612 C3.14048,5.86452 3.49568,5.58852 3.87648,5.66372 C4.25408,5.73012 4.50848,6.11092 4.44448,6.51172 C4.31808,7.31172 4.25808,7.80292 4.25168,8.78292 C4.24928,9.06132 4.46128,9.28612 4.72288,9.28612 C4.98288,9.28612 5.20128,9.06212 5.20128,8.78612 L5.19728,5.94052 C5.19728,5.49412 5.57328,5.13972 6.00768,5.21492 C6.34928,5.27412 6.58528,5.61412 6.58528,5.98052 L6.58928,8.78612 C6.58928,9.06212 6.79968,9.28612 7.06048,9.28612 C7.32208,9.28612 7.53248,9.06212 7.53088,8.78372 C7.52768,7.80372 7.46528,7.31252 7.33808,6.51172 C7.27408,6.11092 7.52928,5.73012 7.90608,5.66372 C8.27648,5.59012 8.64208,5.86452 8.70528,6.26612 C8.89248,7.43332 8.96208,8.61492 8.91328,9.77892 Z M12.8016,5.20468 C14.6304,5.20468 15.5456,7.97108 15.5456,9.79828 C15.5456,10.78708 15.0104,11.63908 14.2768,12.14228 C13.924,12.38388 13.68,12.76548 13.6592,13.21028 L13.572,15.09028 C13.552,15.52708 13.2136,15.87028 12.8016,15.87188 C12.3896,15.87028 12.0512,15.52708 12.0304,15.09028 L11.944,13.21028 C11.9232,12.76548 11.6792,12.38388 11.3264,12.14228 C10.5928,11.63908 10.0576,10.78708 10.0576,9.79828 C10.0576,7.97108 10.972,5.20468 12.8016,5.20468 Z" id="Shape" fill="#FFFFFF"/> |
||||
|
<circle id="Oval" fill="#EE2737" fill-rule="nonzero" cx="9.6" cy="10.53828" r="9.6"/> |
||||
|
<path d="M9.313488,9.778528 C9.28090011,10.8161796 8.73180649,11.7688684 7.85028,12.317224 C7.43417512,12.5583178 7.16637749,12.9916957 7.136896,13.471696 L7.062064,15.0904 C7.05429729,15.5155245 6.71381508,15.8595011 6.288792,15.871608 C5.86321342,15.8594528 5.52250794,15.5146377 5.515456,15.088944 L5.440752,13.412264 C5.41394879,12.9326491 5.15396528,12.4965639 4.7448,12.244904 C3.83024194,11.6483079 3.27368272,10.6347045 3.26112,9.542832 C3.23204946,8.44642386 3.30431645,7.34967416 3.477,6.26656 C3.49861681,6.07852411 3.59641821,5.9075862 3.74756691,5.79366143 C3.89871561,5.67973667 4.08997908,5.63279875 4.276696,5.663808 C4.66442771,5.74603244 4.91625903,6.12195301 4.844776,6.511808 C4.71266948,7.26150388 4.64787805,8.02151276 4.651176,8.782752 C4.64646935,8.91216473 4.693597,9.03810575 4.78210453,9.13263734 C4.87061207,9.22716894 4.99318185,9.2824754 5.122624,9.286288 C5.39227814,9.27887733 5.60582229,9.05597189 5.601664,8.786248 L5.597752,5.940928 C5.59034724,5.73096209 5.67653456,5.52854358 5.83302731,5.38836323 C5.98952006,5.24818288 6.20016768,5.18470776 6.408056,5.215088 C6.75852828,5.29978584 7.00019394,5.62048651 6.985008,5.980728 L6.988864,8.786248 C6.97852893,8.96159222 7.06636284,9.12818532 7.21688012,9.21872236 C7.36739741,9.3092594 7.55572645,9.30877996 7.70578081,9.21747772 C7.85583516,9.12617548 7.94281971,8.95913732 7.931592,8.783848 C7.9360476,8.0222077 7.87139596,7.26173359 7.738432,6.511776 C7.66694897,6.12192101 7.91878029,5.74600044 8.306512,5.663776 C8.49289008,5.63473765 8.68308133,5.68245049 8.8336822,5.79602598 C8.98428308,5.90960146 9.08244268,6.07934809 9.10576,6.266528 C9.29129235,7.42740976 9.36087766,8.60386935 9.313488,9.778528 L9.313488,9.778528 Z M13.20148,5.20496 C15.030752,5.20496 15.945384,7.971168 15.945384,9.798216 C15.9335113,10.7406833 15.459066,11.6170829 14.676408,12.14228 C14.3139521,12.3810403 14.0851956,12.7766388 14.059064,13.20988 L13.972144,15.09096 C13.9757229,15.3686586 13.829623,15.626808 13.5897077,15.7667007 C13.3497924,15.9065934 13.0531676,15.9065934 12.8132523,15.7667007 C12.573337,15.626808 12.4272371,15.3686586 12.430816,15.09096 L12.343896,13.20988 C12.3177675,12.7766399 12.0890138,12.3810411 11.72656,12.14228 C10.9438927,11.6170765 10.4694463,10.7406627 10.457584,9.798184 C10.457584,7.971168 11.372216,5.20496 13.20148,5.20496 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"/> |
||||
|
</g> |
||||
|
</g> |
||||
|
</svg> |
||||
@ -0,0 +1 @@ |
|||||
|
<svg id="b21613c9-2bf0-4d37-bef0-3b193d34fc5d" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#51c273"/><circle cx="190.15351" cy="24.95465" r="20" fill="#51c273"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#51c273"/><circle cx="433.63626" cy="105.17383" r="20" fill="#51c273"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg> |
||||
1
src/images/page-not-found.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,520 @@ |
|||||
|
:root { |
||||
|
--bg-color: hsl(0, 0%, 98%); |
||||
|
--primary-color: hsl(138, 56%, 44%); |
||||
|
--secondary-color: hsl(138, 48%, 54%); |
||||
|
--white: hsl(0, 0%, 100%); |
||||
|
--body-text: hsl(210, 6%, 34%); |
||||
|
--title-color: hsl(210, 8%, 16%); |
||||
|
} |
||||
|
|
||||
|
*, |
||||
|
*::before, |
||||
|
*::after { |
||||
|
box-sizing: border-box; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
font-family: "Nunito", sans-serif; |
||||
|
} |
||||
|
|
||||
|
html { |
||||
|
font-size: 62.5%; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
background-color: var(--bg-color); |
||||
|
color: var(--body-text); |
||||
|
min-height: 100vh; |
||||
|
font-size: 1.6rem; |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
|
||||
|
#root { |
||||
|
min-height: 100vh; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
main { |
||||
|
flex: 1 0 0; |
||||
|
} |
||||
|
|
||||
|
img { |
||||
|
display: block; |
||||
|
line-height: 0; |
||||
|
} |
||||
|
|
||||
|
a { |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
|
||||
|
a:hover { |
||||
|
color: var(--primary-color); |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
list-style: none; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
width: 90vw; |
||||
|
margin: 0 auto; |
||||
|
padding: 0 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
display: inline-flex; |
||||
|
border: none; |
||||
|
outline: none; |
||||
|
cursor: pointer; |
||||
|
padding: 1rem 1.2rem; |
||||
|
background-color: var(--primary-color); |
||||
|
color: var(--white); |
||||
|
border-radius: 3px; |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.btn-primary { |
||||
|
font-weight: 600; |
||||
|
font-size: 1.4rem; |
||||
|
padding: 1rem 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.btn-primary:hover { |
||||
|
background-color: var(--secondary-color); |
||||
|
color: var(--white); |
||||
|
} |
||||
|
|
||||
|
.heading-2 { |
||||
|
line-height: 1.3; |
||||
|
} |
||||
|
|
||||
|
.amount { |
||||
|
color: #fbaf18; |
||||
|
font-size: 1.7rem; |
||||
|
} |
||||
|
|
||||
|
/* NAVBAR */ |
||||
|
header { |
||||
|
background-color: var(--white); |
||||
|
min-height: 8rem; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.navbar { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.brand { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
color: hsl(210, 8%, 16%); |
||||
|
font-size: 2.3rem; |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.brand-img { |
||||
|
width: 4.2rem; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.navbar .nav-list { |
||||
|
display: flex; |
||||
|
align-items: baseline; |
||||
|
} |
||||
|
|
||||
|
.navbar .nav-item { |
||||
|
margin-left: 4rem; |
||||
|
} |
||||
|
|
||||
|
.navbar .nav-item-group { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.nav-link { |
||||
|
color: var(--body-text); |
||||
|
} |
||||
|
|
||||
|
.nav-link.selected { |
||||
|
color: var(--primary-color); |
||||
|
} |
||||
|
|
||||
|
.navbar .favorite-counter { |
||||
|
background-color: var(--primary-color); |
||||
|
color: var(--white); |
||||
|
font-size: 1.4rem; |
||||
|
font-weight: 600; |
||||
|
width: 2rem; |
||||
|
height: 2rem; |
||||
|
border-radius: 50%; |
||||
|
display: inline-flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
|
||||
|
/* HOMEPAGE - HERO */ |
||||
|
.hero { |
||||
|
text-align: center; |
||||
|
padding: 5rem 0; |
||||
|
background-color: var(--white); |
||||
|
min-height: calc(100vh - 8rem); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background-color: var(--white); |
||||
|
background-image: url(./images/bg-hero.svg); |
||||
|
background-size: 40rem; |
||||
|
background-position: center; |
||||
|
} |
||||
|
|
||||
|
.hero h1 { |
||||
|
line-height: 1.3; |
||||
|
font-size: clamp(2.5rem, 5vw, 3.2rem); |
||||
|
max-width: 60rem; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
/* HOMEPAGE - SEARCHBAR */ |
||||
|
form { |
||||
|
max-width: 50rem; |
||||
|
margin: 5rem auto 0; |
||||
|
} |
||||
|
|
||||
|
.input-group { |
||||
|
position: relative; |
||||
|
min-height: 5rem; |
||||
|
border-radius: 5rem; |
||||
|
} |
||||
|
|
||||
|
.input-group input { |
||||
|
display: block; |
||||
|
background-color: hsl(0, 0%, 100%); |
||||
|
color: var(--body-text); |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 0 5rem; |
||||
|
border: 1px solid hsl(216, 5%, 90%); |
||||
|
box-shadow: 6px 6px 12px rgba(0, 0, 0, 0.06); |
||||
|
border-radius: inherit; |
||||
|
} |
||||
|
|
||||
|
.input-group input:focus { |
||||
|
outline: none; |
||||
|
border-color: hsl(138, 48%, 54%); |
||||
|
box-shadow: 0 0 0 2px hsl(138, 48%, 90%); |
||||
|
} |
||||
|
|
||||
|
.input-group input::placeholder { |
||||
|
color: hsl(0, 0%, 55%); |
||||
|
} |
||||
|
|
||||
|
.input-group .search-icon { |
||||
|
position: absolute; |
||||
|
left: 2rem; |
||||
|
top: 50%; |
||||
|
transform: translateY(-50%); |
||||
|
font-size: 2rem; |
||||
|
color: hsl(210, 4%, 64%); |
||||
|
z-index: 1; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.btn-search { |
||||
|
display: inline-flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
width: 4rem; |
||||
|
height: 4rem; |
||||
|
border-radius: 50%; |
||||
|
background-color: var(--secondary-color); |
||||
|
position: absolute; |
||||
|
top: 5px; |
||||
|
right: 7px; |
||||
|
} |
||||
|
|
||||
|
.btn-search:hover { |
||||
|
background-color: var(--primary-color); |
||||
|
} |
||||
|
|
||||
|
/* HOMEPAGE - RECIEPS */ |
||||
|
.recipes { |
||||
|
padding: 7rem 0 0; |
||||
|
} |
||||
|
|
||||
|
.recipe-list { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(3, 1fr); |
||||
|
gap: 3rem 2rem; |
||||
|
margin-top: 2.5rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-card { |
||||
|
background-color: var(--white); |
||||
|
border-radius: 5px; |
||||
|
overflow: hidden; |
||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.recipe-card img { |
||||
|
width: 100%; |
||||
|
height: 27rem; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
|
||||
|
.recipe-card .recipe-body { |
||||
|
padding: 2rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-card .recipe-title { |
||||
|
color: var(--title-color); |
||||
|
font-size: 2rem; |
||||
|
font-weight: 600; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.recipe-card .recipe-link:hover { |
||||
|
color: var(--primary-color); |
||||
|
} |
||||
|
|
||||
|
.recipe-info { |
||||
|
display: flex; |
||||
|
align-items: flex-end; |
||||
|
margin-top: 3rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-info .icon-group { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-right: 2rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-info .icon { |
||||
|
font-size: 2rem; |
||||
|
color: var(--secondary-color); |
||||
|
} |
||||
|
|
||||
|
.recipe-info p { |
||||
|
font-size: 1.2rem; |
||||
|
line-height: 1; |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
|
||||
|
/* DETAIL */ |
||||
|
.detail { |
||||
|
margin-top: 3.5rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-detail { |
||||
|
max-width: 75rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-detail .recipe-body { |
||||
|
padding: 2rem 2rem 3rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-detail .recipe-title { |
||||
|
font-size: 2.2rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-by { |
||||
|
margin-top: 1rem; |
||||
|
font-size: 1.3rem; |
||||
|
color: hsl(0, 0%, 55%); |
||||
|
} |
||||
|
|
||||
|
.recipe-detail .recipe-info { |
||||
|
margin-top: 3rem; |
||||
|
margin-bottom: 4rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-detail .ingredients, |
||||
|
.recipe-detail .steps { |
||||
|
margin-top: 3rem; |
||||
|
} |
||||
|
|
||||
|
.recipe-detail .heading-4 { |
||||
|
font-size: 1.8rem; |
||||
|
} |
||||
|
|
||||
|
.list-group li { |
||||
|
padding: 5px 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.list-group .icon { |
||||
|
display: inline-block; |
||||
|
margin-right: 5px; |
||||
|
} |
||||
|
|
||||
|
.list-group-border li { |
||||
|
padding: 1.2rem 0; |
||||
|
} |
||||
|
|
||||
|
.list-group-border li:not(:last-child) { |
||||
|
border-bottom: 1px solid hsl(216, 5%, 90%); |
||||
|
} |
||||
|
|
||||
|
/* BOOKMARKS */ |
||||
|
.bookmarks { |
||||
|
margin-top: 3rem; |
||||
|
} |
||||
|
|
||||
|
.bookmarks .illustration { |
||||
|
width: 20rem; |
||||
|
margin: 5rem auto 0; |
||||
|
} |
||||
|
|
||||
|
/* NOT FOUND */ |
||||
|
.not-found { |
||||
|
min-height: calc(100vh - 8rem); |
||||
|
padding: 3rem 0; |
||||
|
text-align: center; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.not-found .illustration { |
||||
|
width: 100%; |
||||
|
max-width: 40rem; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.not-found h4 { |
||||
|
font-size: 2.5rem; |
||||
|
margin: 4rem 0 3rem; |
||||
|
} |
||||
|
|
||||
|
/* FOOTER */ |
||||
|
footer { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
background-color: var(--white); |
||||
|
border-top: 1px solid hsl(0, 0%, 93%); |
||||
|
padding: 4rem 2rem 2.5rem; |
||||
|
text-align: center; |
||||
|
margin-top: 15rem; |
||||
|
} |
||||
|
|
||||
|
.scm-list { |
||||
|
display: flex; |
||||
|
align-items: flex-start; |
||||
|
margin-top: 3rem; |
||||
|
} |
||||
|
|
||||
|
.scm-list li { |
||||
|
margin: 0 1rem; |
||||
|
} |
||||
|
|
||||
|
.scm-icon { |
||||
|
display: inline-block; |
||||
|
font-size: 2.2rem; |
||||
|
color: hsl(210, 4%, 64%); |
||||
|
} |
||||
|
|
||||
|
.copyright-text { |
||||
|
font-size: 1.8rem; |
||||
|
margin-top: 2rem; |
||||
|
} |
||||
|
|
||||
|
/* MOUSE SCROLL ANIM */ |
||||
|
.mouse-scroll-anim { |
||||
|
width: 3rem; |
||||
|
height: 5rem; |
||||
|
border-radius: 2.5rem; |
||||
|
border: 1px solid hsl(0, 0%, 55%); |
||||
|
margin: 5rem auto 0; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.mouse-scroll-anim::after { |
||||
|
content: ""; |
||||
|
display: inline-block; |
||||
|
width: 5px; |
||||
|
height: 1rem; |
||||
|
border-radius: 3rem; |
||||
|
background-color: hsl(0, 0%, 55%); |
||||
|
position: absolute; |
||||
|
top: 5px; |
||||
|
left: calc(50% - 2.5px); |
||||
|
animation: scroll 1.8s ease-in infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes scroll { |
||||
|
from { |
||||
|
opacity: 1; |
||||
|
transform: translateY(0); |
||||
|
} |
||||
|
|
||||
|
to { |
||||
|
opacity: 0; |
||||
|
transform: translateY(3rem); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: 992px) { |
||||
|
.container { |
||||
|
width: 97vw; |
||||
|
} |
||||
|
|
||||
|
.recipe-list { |
||||
|
grid-template-columns: repeat(2, 1fr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: 768px) { |
||||
|
.recipe-list { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (min-width: 600px) { |
||||
|
.recipe-detail img { |
||||
|
height: 38rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: 414px) { |
||||
|
.recipe-card .recipe-title { |
||||
|
font-size: 1.8rem; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.brand { |
||||
|
font-size: 1.8rem; |
||||
|
} |
||||
|
|
||||
|
.brand-img { |
||||
|
width: 3.2rem; |
||||
|
} |
||||
|
|
||||
|
.navbar .nav-item { |
||||
|
margin-left: 2.5rem; |
||||
|
} |
||||
|
|
||||
|
header .nav-link { |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
header { |
||||
|
min-height: 7.3rem; |
||||
|
} |
||||
|
|
||||
|
.bookmarks .illustration { |
||||
|
width: 15rem; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
import React from "react"; |
||||
|
import ReactDOM from "react-dom"; |
||||
|
import "./index.css"; |
||||
|
import App from "./App"; |
||||
|
import { BookmarkContextProvider } from "./contexts/BookmarkContext"; |
||||
|
|
||||
|
ReactDOM.render( |
||||
|
<BookmarkContextProvider> |
||||
|
<App /> |
||||
|
</BookmarkContextProvider>, |
||||
|
document.getElementById("root") |
||||
|
); |
||||
@ -0,0 +1,33 @@ |
|||||
|
import { useContext } from "react"; |
||||
|
import { BookmarkContext } from "../contexts/BookmarkContext"; |
||||
|
import RecipeList from "../components/RecipeList"; |
||||
|
import noDataImg from "../images/no-data.svg"; |
||||
|
|
||||
|
const Bookmark = () => { |
||||
|
const { recipes } = useContext(BookmarkContext); |
||||
|
|
||||
|
return ( |
||||
|
<section className="bookmarks"> |
||||
|
<div className="container"> |
||||
|
{recipes.length > 0 ? ( |
||||
|
<> |
||||
|
<h2 className="heading-2"> |
||||
|
Resep yang kamu simpan{" "} |
||||
|
<span className="amount">({recipes.length})</span> |
||||
|
</h2> |
||||
|
<RecipeList recipes={recipes} /> |
||||
|
</> |
||||
|
) : ( |
||||
|
<div style={{ textAlign: "center", marginTop: "10rem" }}> |
||||
|
<img src={noDataImg} className="illustration" alt="" /> |
||||
|
<h3 style={{ marginTop: "3rem" }}> |
||||
|
Belum ada resep yang Disimpan! |
||||
|
</h3> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
</section> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Bookmark; |
||||
@ -0,0 +1,100 @@ |
|||||
|
import { useContext } from "react"; |
||||
|
import { useParams } from "react-router-dom"; |
||||
|
import { BiBarChartAlt, BiDish, BiTime, BiCheck } from "react-icons/bi"; |
||||
|
import useAxios from "../components/useAxios"; |
||||
|
import notFoundImg from "../images/img-not-found.png"; |
||||
|
import { BookmarkContext } from "../contexts/BookmarkContext"; |
||||
|
|
||||
|
const Detail = () => { |
||||
|
const { key } = useParams(); |
||||
|
const { data, isLoading } = useAxios( |
||||
|
`https://cors.bridged.cc/https://masak-apa.tomorisakura.vercel.app/api/recipe/${key}` |
||||
|
); |
||||
|
let recipe; |
||||
|
|
||||
|
if (data) { |
||||
|
recipe = { ...data, key }; |
||||
|
} |
||||
|
|
||||
|
const { dispatch, isSaved } = useContext(BookmarkContext); |
||||
|
|
||||
|
return ( |
||||
|
<section className="detail"> |
||||
|
<div className="container"> |
||||
|
{isLoading && <p>Tunggu sebentar...</p>} |
||||
|
{recipe && ( |
||||
|
<div className="recipe recipe-detail recipe-card"> |
||||
|
<img src={recipe.thumb ? recipe.thumb : notFoundImg} alt="" /> |
||||
|
<div className="recipe-body"> |
||||
|
<h3 className="recipe-title">{recipe.title}</h3> |
||||
|
<p className="recipe-by"> |
||||
|
{recipe.author.user} - |
||||
|
<time> {recipe.author.datePublished}</time> |
||||
|
</p> |
||||
|
<div className="recipe-info"> |
||||
|
<div className="icon-group"> |
||||
|
<BiBarChartAlt className="icon" /> |
||||
|
<p>{recipe.dificulty}</p> |
||||
|
</div> |
||||
|
<div className="icon-group"> |
||||
|
<BiDish className="icon" /> |
||||
|
<p>{recipe.servings}</p> |
||||
|
</div> |
||||
|
<div className="icon-group"> |
||||
|
<BiTime className="icon" /> |
||||
|
<p>{recipe.times}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
{/* bahan2 */} |
||||
|
<div className="ingredients"> |
||||
|
<h4 className="heading-4">Bahan - Bahan :</h4> |
||||
|
<ul className="list-group"> |
||||
|
{recipe.ingredient.map((ingredient, idx) => ( |
||||
|
<li key={idx}> |
||||
|
<BiCheck |
||||
|
color="hsl(138, 48%, 54%)" |
||||
|
size="20px" |
||||
|
className="icon" |
||||
|
/>{" "} |
||||
|
{ingredient} |
||||
|
</li> |
||||
|
))} |
||||
|
</ul> |
||||
|
</div> |
||||
|
{/* langkah2 */} |
||||
|
<div className="steps"> |
||||
|
<h4 className="heading-4">Langkah pembuatan :</h4> |
||||
|
<ul className="list-group list-group-border"> |
||||
|
{recipe.step.map((step, idx) => ( |
||||
|
<li key={idx}>{step}</li> |
||||
|
))} |
||||
|
</ul> |
||||
|
</div> |
||||
|
{/* tombol simpan resep */} |
||||
|
<div style={{ textAlign: "center" }}> |
||||
|
<button |
||||
|
className="btn btn-primary" |
||||
|
style={{ margin: "4rem auto 0" }} |
||||
|
onClick={() => |
||||
|
dispatch({ type: "TOGGLE_SAVE", payload: recipe, key }) |
||||
|
} |
||||
|
> |
||||
|
{isSaved(key) ? ( |
||||
|
<span style={{ display: "flex", alignItems: "center" }}> |
||||
|
<BiCheck fontSize="20px" style={{ marginRight: "5px" }} />{" "} |
||||
|
Disimpan |
||||
|
</span> |
||||
|
) : ( |
||||
|
<span>Simpan Resep</span> |
||||
|
)} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
</section> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Detail; |
||||
@ -0,0 +1,44 @@ |
|||||
|
import SearchBar from "../components/SearchBar"; |
||||
|
import SearchResults from "../components/SearchResults"; |
||||
|
import useAxios from "../components/useAxios"; |
||||
|
import RecipeList from "../components/RecipeList"; |
||||
|
import { useLocation } from "react-router-dom"; |
||||
|
|
||||
|
const Home = () => { |
||||
|
const params = new URLSearchParams(useLocation().search); |
||||
|
const query = params.get("search"); |
||||
|
const { |
||||
|
data: recipes, |
||||
|
isLoading, |
||||
|
error, |
||||
|
} = useAxios( |
||||
|
"https://cors.bridged.cc/https://masak-apa.tomorisakura.vercel.app/api/recipes/" |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<section className="hero"> |
||||
|
<div className="container"> |
||||
|
<h1>Gorecipe menyediakan kumpulan resep masakan khas indonesia</h1> |
||||
|
<SearchBar value={query ? query : ""} /> |
||||
|
<div className="mouse-scroll-anim"></div> |
||||
|
</div> |
||||
|
</section> |
||||
|
{/* Kontent - Hasil pencarian resep */} |
||||
|
{query && <SearchResults keyword={query} />} |
||||
|
{ |
||||
|
/* Kontent - Resep untukmu hari ini */ |
||||
|
<section className="recipes recipes-today"> |
||||
|
<div className="container"> |
||||
|
<h2 className="heading-2">Resep untukmu hari ini</h2> |
||||
|
{error && <p>{error}</p>} |
||||
|
{isLoading && <p>Tunggu sebentar...</p>} |
||||
|
{recipes && <RecipeList recipes={recipes} />} |
||||
|
</div> |
||||
|
</section> |
||||
|
} |
||||
|
</> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Home; |
||||
@ -0,0 +1,18 @@ |
|||||
|
import illustrationImg from "../images/page-not-found.svg"; |
||||
|
import { Link } from "react-router-dom"; |
||||
|
|
||||
|
const NotFound = () => { |
||||
|
return ( |
||||
|
<section className="not-found"> |
||||
|
<div className="container"> |
||||
|
<img src={illustrationImg} className="illustration" alt="" /> |
||||
|
<h4 className="heading-4">Halaman tidak ditemukan!</h4> |
||||
|
<Link to="/" className="btn btn-primary"> |
||||
|
Kembali ke Home |
||||
|
</Link> |
||||
|
</div> |
||||
|
</section> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default NotFound; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue