├── Procfile ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json ├── index.html └── db.json ├── src ├── pages │ ├── index.js │ ├── Home.jsx │ └── Cart.jsx ├── assets │ └── img │ │ ├── empty-cart.png │ │ ├── grey-arrow-left.svg │ │ ├── arrow-top.svg │ │ ├── plus.svg │ │ ├── trash.svg │ │ ├── cart.svg │ │ └── pizza-logo.svg ├── scss │ ├── fonts │ │ ├── ProximaNova-Bold.eot │ │ ├── ProximaNova-Bold.ttf │ │ ├── ProximaNova-Black.eot │ │ ├── ProximaNova-Black.ttf │ │ ├── ProximaNova-Black.woff │ │ ├── ProximaNova-Bold.woff │ │ ├── ProximaNova-Extrabld.eot │ │ ├── ProximaNova-Extrabld.ttf │ │ ├── ProximaNova-Extrabld.woff │ │ ├── ProximaNova-Regular.eot │ │ ├── ProximaNova-Regular.ttf │ │ ├── ProximaNova-Regular.woff │ │ ├── ProximaNova-Semibold.eot │ │ ├── ProximaNova-Semibold.ttf │ │ └── ProximaNova-Semibold.woff │ ├── components │ │ ├── _all.scss │ │ ├── _header.scss │ │ ├── _categories.scss │ │ ├── _sort.scss │ │ ├── _pizza-block.scss │ │ └── _button.scss │ ├── _variables.scss │ ├── libs │ │ └── _normalize.scss │ ├── _fonts.scss │ └── app.scss ├── redux │ ├── actions │ │ ├── filters.js │ │ ├── cart.js │ │ └── pizzas.js │ ├── reducers │ │ ├── index.js │ │ ├── filters.js │ │ ├── pizzas.js │ │ └── cart.js │ └── store.js ├── components │ ├── index.js │ ├── Button.jsx │ ├── PizzaBlock │ │ ├── LoadingBlock.jsx │ │ └── index.jsx │ ├── Categories.jsx │ ├── SortPopup.jsx │ ├── Header.jsx │ └── CartItem.jsx ├── index.js └── App.js ├── config ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── pnpTs.js ├── getHttpsConfig.js ├── paths.js ├── env.js ├── modules.js ├── webpackDevServer.config.js └── webpack.config.js ├── .gitignore ├── server.js ├── README.md ├── scripts ├── test.js ├── start.js └── build.js └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as Home } from './Home'; 2 | export { default as Cart } from './Cart'; 3 | -------------------------------------------------------------------------------- /src/assets/img/empty-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/assets/img/empty-cart.png -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Bold.eot -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Bold.ttf -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Black.eot -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Black.ttf -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Black.woff -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Bold.woff -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Extrabld.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Extrabld.eot -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Extrabld.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Extrabld.ttf -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Extrabld.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Extrabld.woff -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Regular.eot -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Regular.ttf -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Regular.woff -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Semibold.eot -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Semibold.ttf -------------------------------------------------------------------------------- /src/scss/fonts/ProximaNova-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/react-pizza/HEAD/src/scss/fonts/ProximaNova-Semibold.woff -------------------------------------------------------------------------------- /src/scss/components/_all.scss: -------------------------------------------------------------------------------- 1 | @import './header'; 2 | @import './button'; 3 | @import './categories'; 4 | @import './sort'; 5 | @import './pizza-block'; 6 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $black: #232323; 2 | $background: #ffdf8c; 3 | $gray-line: #f6f6f6; 4 | $orange: #fe5f1e; 5 | 6 | $container-width: 90%; 7 | 8 | $duration: 0.15s; 9 | -------------------------------------------------------------------------------- /src/assets/img/grey-arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/redux/actions/filters.js: -------------------------------------------------------------------------------- 1 | export const setSortBy = ({ type, order }) => ({ 2 | type: 'SET_SORT_BY', 3 | payload: { type, order }, 4 | }); 5 | 6 | export const setCategory = (catIndex) => ({ 7 | type: 'SET_CATEGORY', 8 | payload: catIndex, 9 | }); 10 | -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import filters from './filters'; 4 | import pizzas from './pizzas'; 5 | import cart from './cart'; 6 | 7 | const rootReducer = combineReducers({ 8 | filters, 9 | pizzas, 10 | cart, 11 | }); 12 | 13 | export default rootReducer; 14 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import rootReducer from './reducers'; 5 | 6 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 7 | 8 | const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk))); 9 | 10 | export default store; 11 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button'; 2 | export { default as Header } from './Header'; 3 | export { default as Categories } from './Categories'; 4 | export { default as SortPopup } from './SortPopup'; 5 | export { default as CartItem } from './CartItem'; 6 | export { default as PizzaBlock } from './PizzaBlock'; 7 | export { default as PizzaLoadingBlock } from './PizzaBlock/LoadingBlock'; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const jsonServer = require('json-server'); 2 | const server = jsonServer.create(); 3 | const router = jsonServer.router('./public/db.json'); 4 | const middlewares = jsonServer.defaults({ 5 | static: './build', 6 | }); 7 | 8 | const PORT = process.env.PORT || 3001; 9 | 10 | server.use(middlewares); 11 | server.use(router); 12 | 13 | server.listen(PORT, () => { 14 | console.log('Server is running'); 15 | }); 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Интернет-магазин по заказу пиццы - **React Pizza** 2 | 3 | - [Плейлист с полным курсом на YouTube](https://www.youtube.com/watch?v=bziVFvq8cLQ&list=PL0FGkDGJQjJFMRmP7wZ771m1Nx-m2_qXq) 4 | - [Как задеплоить React Pizza на бесплатный хостинг](https://www.youtube.com/watch?v=-pJN9faoa8E&t=1951s) 5 | 6 | **Stack:** 7 | 8 | - ReactJS + хуки 9 | - React Router 10 | - Redux 11 | - Redux thunk 12 | - json-server 13 | - Axios 14 | - classnames 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import store from './redux/store'; 7 | 8 | import './scss/app.scss'; 9 | import App from './App'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root'), 18 | ); 19 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './components'; 4 | import { Home, Cart } from './pages'; 5 | import { Route } from 'react-router-dom'; 6 | 7 | function App() { 8 | return ( 9 |
10 |
11 |
12 | 13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/assets/img/arrow-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | const Button = ({ onClick, className, outline, children }) => { 6 | return ( 7 | 14 | ); 15 | }; 16 | 17 | Button.propTypes = { 18 | onClick: PropTypes.func, 19 | }; 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /src/redux/actions/cart.js: -------------------------------------------------------------------------------- 1 | export const addPizzaToCart = (pizzaObj) => ({ 2 | type: 'ADD_PIZZA_CART', 3 | payload: pizzaObj, 4 | }); 5 | 6 | export const clearCart = () => ({ 7 | type: 'CLEAR_CART', 8 | }); 9 | 10 | export const removeCartItem = (id) => ({ 11 | type: 'REMOVE_CART_ITEM', 12 | payload: id, 13 | }); 14 | 15 | export const plusCartItem = (id) => ({ 16 | type: 'PLUS_CART_ITEM', 17 | payload: id, 18 | }); 19 | 20 | export const minusCartItem = (id) => ({ 21 | type: 'MINUS_CART_ITEM', 22 | payload: id, 23 | }); 24 | -------------------------------------------------------------------------------- /src/redux/reducers/filters.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | category: null, 3 | sortBy: { 4 | type: 'popular', 5 | order: 'desc', 6 | }, 7 | }; 8 | 9 | const filters = (state = initialState, action) => { 10 | if (action.type === 'SET_SORT_BY') { 11 | return { 12 | ...state, 13 | sortBy: action.payload, 14 | }; 15 | } 16 | if (action.type === 'SET_CATEGORY') { 17 | return { 18 | ...state, 19 | category: action.payload, 20 | }; 21 | } 22 | return state; 23 | }; 24 | 25 | export default filters; 26 | -------------------------------------------------------------------------------- /src/redux/reducers/pizzas.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | items: [], 3 | isLoaded: false, 4 | }; 5 | 6 | const pizzas = (state = initialState, action) => { 7 | switch (action.type) { 8 | case 'SET_PIZZAS': 9 | return { 10 | ...state, 11 | items: action.payload, 12 | isLoaded: true, 13 | }; 14 | 15 | case 'SET_LOADED': 16 | return { 17 | ...state, 18 | isLoaded: action.payload, 19 | }; 20 | 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export default pizzas; 27 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/redux/actions/pizzas.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const setLoaded = (payload) => ({ 4 | type: 'SET_LOADED', 5 | payload, 6 | }); 7 | 8 | export const fetchPizzas = (sortBy, category) => (dispatch) => { 9 | dispatch({ 10 | type: 'SET_LOADED', 11 | payload: false, 12 | }); 13 | 14 | axios 15 | .get( 16 | `/pizzas?${category !== null ? `category=${category}` : ''}&_sort=${sortBy.type}&_order=${ 17 | sortBy.order 18 | }`, 19 | ) 20 | .then(({ data }) => { 21 | dispatch(setPizzas(data)); 22 | }); 23 | }; 24 | 25 | export const setPizzas = (items) => ({ 26 | type: 'SET_PIZZAS', 27 | payload: items, 28 | }); 29 | -------------------------------------------------------------------------------- /src/assets/img/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /config/pnpTs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolveModuleName } = require('ts-pnp'); 4 | 5 | exports.resolveModuleName = ( 6 | typescript, 7 | moduleName, 8 | containingFile, 9 | compilerOptions, 10 | resolutionHost 11 | ) => { 12 | return resolveModuleName( 13 | moduleName, 14 | containingFile, 15 | compilerOptions, 16 | resolutionHost, 17 | typescript.resolveModuleName 18 | ); 19 | }; 20 | 21 | exports.resolveTypeReferenceDirective = ( 22 | typescript, 23 | moduleName, 24 | containingFile, 25 | compilerOptions, 26 | resolutionHost 27 | ) => { 28 | return resolveModuleName( 29 | moduleName, 30 | containingFile, 31 | compilerOptions, 32 | resolutionHost, 33 | typescript.resolveTypeReferenceDirective 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/scss/components/_header.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .header { 4 | border-bottom: 1px solid $gray-line; 5 | padding: 40px 0; 6 | 7 | .container { 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | @media (max-width: 768px) { 12 | flex-direction: column; 13 | } 14 | } 15 | 16 | @media (max-width: 768px) { 17 | &__cart { 18 | margin-top: 30px; 19 | } 20 | } 21 | 22 | &__logo { 23 | display: flex; 24 | 25 | img { 26 | margin-right: 15px; 27 | } 28 | 29 | h1 { 30 | color: #181818; 31 | font-size: 24px; 32 | letter-spacing: 1%; 33 | text-transform: uppercase; 34 | font-weight: 800; 35 | } 36 | 37 | p { 38 | color: #7b7b7b; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/PizzaBlock/LoadingBlock.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContentLoader from 'react-content-loader'; 3 | 4 | function LoadingBlock() { 5 | return ( 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default LoadingBlock; 24 | -------------------------------------------------------------------------------- /src/scss/components/_categories.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .categories { 4 | flex: 1; 5 | max-width: 80%; 6 | overflow: auto; 7 | 8 | @media (max-width: 768px) { 9 | max-width: 100%; 10 | margin-bottom: 30px; 11 | } 12 | 13 | ul { 14 | display: flex; 15 | 16 | li { 17 | background-color: #f9f9f9; 18 | padding: 13px 30px; 19 | border-radius: 30px; 20 | margin-right: 10px; 21 | font-weight: bold; 22 | cursor: pointer; 23 | transition: background-color 0.1s ease-in-out; 24 | @include noselect(); 25 | 26 | &:hover { 27 | background-color: darken(#f9f9f9, 2%); 28 | } 29 | 30 | &:active { 31 | background-color: darken(#f9f9f9, 5%); 32 | } 33 | 34 | &.active { 35 | background-color: #282828; 36 | color: #fff; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/assets/img/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/img/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Categories.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Categories = React.memo(function Categories({ activeCategory, items, onClickCategory }) { 5 | return ( 6 |
7 | 23 |
24 | ); 25 | }); 26 | 27 | Categories.propTypes = { 28 | // activeCategory: PropTypes.oneOf([PropTypes.number, null]), 29 | items: PropTypes.arrayOf(PropTypes.string).isRequired, 30 | onClickCategory: PropTypes.func.isRequired, 31 | }; 32 | 33 | Categories.defaultProps = { activeCategory: null, items: [] }; 34 | 35 | export default Categories; 36 | -------------------------------------------------------------------------------- /src/scss/libs/_normalize.scss: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | list-style: none; 5 | outline: none; 6 | font-family: 'Proxima Nova', Roboto, system-ui, Tahoma, sans-serif; 7 | box-sizing: border-box; 8 | } 9 | 10 | html { 11 | -ms-text-size-adjust: 100%; 12 | -webkit-text-size-adjust: 100%; 13 | } 14 | 15 | body { 16 | -moz-osx-font-smoothing: grayscale; 17 | -webkit-font-smoothing: antialiased; 18 | color: $black; 19 | } 20 | 21 | a, 22 | span, 23 | p, 24 | b, 25 | h1, 26 | h2, 27 | h3, 28 | h4, 29 | h5 { 30 | color: $black; 31 | } 32 | 33 | h1 { 34 | font-size: 48px; 35 | } 36 | 37 | h2 { 38 | font-weight: 600; 39 | font-size: 28px; 40 | line-height: 30px; 41 | } 42 | 43 | a { 44 | text-decoration: none; 45 | } 46 | 47 | @mixin noselect { 48 | -webkit-touch-callout: none; /* iOS Safari */ 49 | -webkit-user-select: none; /* Safari */ 50 | -khtml-user-select: none; /* Konqueror HTML */ 51 | -moz-user-select: none; /* Old versions of Firefox */ 52 | -ms-user-select: none; /* Internet Explorer/Edge */ 53 | user-select: none; /* Non-prefixed version, currently 54 | supported by Chrome, Opera and Firefox */ 55 | } 56 | -------------------------------------------------------------------------------- /src/scss/components/_sort.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .sort { 4 | margin-left: 20px; 5 | position: relative; 6 | &__label { 7 | display: flex; 8 | align-items: center; 9 | 10 | svg { 11 | margin-right: 8px; 12 | transform: rotate(180deg); 13 | 14 | &.rotated { 15 | transform: rotate(0); 16 | } 17 | } 18 | 19 | b { 20 | margin-right: 8px; 21 | } 22 | 23 | span { 24 | color: $orange; 25 | border-bottom: 1px dashed $orange; 26 | cursor: pointer; 27 | } 28 | } 29 | 30 | &__popup { 31 | position: absolute; 32 | right: 0; 33 | margin-top: 15px; 34 | background: #ffffff; 35 | box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.09); 36 | border-radius: 10px; 37 | overflow: hidden; 38 | padding: 10px 0; 39 | width: 160px; 40 | 41 | ul { 42 | overflow: hidden; 43 | li { 44 | padding: 12px 20px; 45 | cursor: pointer; 46 | 47 | &.active, 48 | &:hover { 49 | background: rgba(254, 95, 30, 0.05); 50 | } 51 | 52 | &.active { 53 | font-weight: bold; 54 | color: $orange; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/scss/components/_pizza-block.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .pizza-block { 4 | width: 100%; 5 | text-align: center; 6 | margin-bottom: 65px; 7 | 8 | @media (max-width: 1090px) { 9 | margin-bottom: 35px; 10 | } 11 | 12 | @media (max-width: 768px) { 13 | &:not(:last-of-type) { 14 | border-bottom: 1px solid #efefef; 15 | margin-bottom: 25px; 16 | padding-bottom: 45px; 17 | } 18 | } 19 | 20 | &__image { 21 | width: 260px; 22 | } 23 | 24 | &__title { 25 | font-size: 20px; 26 | font-weight: 900; 27 | letter-spacing: 1%; 28 | margin-bottom: 20px; 29 | } 30 | 31 | &__selector { 32 | display: flex; 33 | background-color: #f3f3f3; 34 | border-radius: 10px; 35 | flex-direction: column; 36 | padding: 6px; 37 | 38 | ul { 39 | display: flex; 40 | flex: 1; 41 | 42 | &:first-of-type { 43 | margin-bottom: 6px; 44 | } 45 | 46 | li { 47 | padding: 8px; 48 | flex: 1; 49 | cursor: pointer; 50 | font-weight: 600; 51 | font-size: 14px; 52 | @include noselect(); 53 | &.active { 54 | background: #ffffff; 55 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.04); 56 | border-radius: 5px; 57 | cursor: auto; 58 | } 59 | &.disabled { 60 | opacity: 0.4; 61 | pointer-events: none; 62 | } 63 | } 64 | } 65 | } 66 | 67 | &__bottom { 68 | display: flex; 69 | align-items: center; 70 | justify-content: space-between; 71 | margin-top: 20px; 72 | } 73 | 74 | &__price { 75 | font-weight: bold; 76 | font-size: 22px; 77 | line-height: 27px; 78 | letter-spacing: 0.015em; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Proxima Nova'; 3 | src: url('./fonts/ProximaNova-Black.eot'); 4 | src: local('./fonts/Proxima Nova Black'), local('ProximaNova-Black'), 5 | url('./fonts/ProximaNova-Black.eot?#iefix') format('embedded-opentype'), 6 | url('./fonts/ProximaNova-Black.woff') format('woff'), 7 | url('./fonts/ProximaNova-Black.ttf') format('truetype'); 8 | font-weight: 900; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: 'Proxima Nova'; 14 | src: url('./fonts/ProximaNova-Bold.eot'); 15 | src: local('./fonts/Proxima Nova Bold'), local('ProximaNova-Bold'), 16 | url('./fonts/ProximaNova-Bold.eot?#iefix') format('embedded-opentype'), 17 | url('./fonts/ProximaNova-Bold.woff') format('woff'), 18 | url('./fonts/ProximaNova-Bold.ttf') format('truetype'); 19 | font-weight: bold; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'Proxima Nova'; 25 | src: url('./fonts/ProximaNova-Regular.eot'); 26 | src: local('./fonts/Proxima Nova Regular'), local('ProximaNova-Regular'), 27 | url('./fonts/ProximaNova-Regular.eot?#iefix') format('embedded-opentype'), 28 | url('./fonts/ProximaNova-Regular.woff') format('woff'), 29 | url('./fonts/ProximaNova-Regular.ttf') format('truetype'); 30 | font-weight: normal; 31 | font-style: normal; 32 | } 33 | 34 | @font-face { 35 | font-family: 'Proxima Nova'; 36 | src: url('./fonts/ProximaNova-Extrabld.eot'); 37 | src: local('./fonts/Proxima Nova Extrabold'), local('ProximaNova-Extrabld'), 38 | url('./fonts/ProximaNova-Extrabld.eot?#iefix') format('embedded-opentype'), 39 | url('./fonts/ProximaNova-Extrabld.woff') format('woff'), 40 | url('./fonts/ProximaNova-Extrabld.ttf') format('truetype'); 41 | font-weight: 800; 42 | font-style: normal; 43 | } 44 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right