├── .gitignore ├── .new-component-config.json ├── LICENSE.md ├── README.md ├── docs ├── exercise-1-solution.png ├── exercise-2-solution.png ├── exercise-3-solution.png ├── exercise-4a-solution.png ├── exercise-4b-solution.png ├── giant-sneaker.png └── nav-position.png ├── package-lock.json ├── package.json ├── public ├── assets │ ├── flyknit.jpg │ ├── joyride.jpg │ ├── lebron.jpg │ ├── legend-academy.jpg │ ├── metcon-5.jpg │ ├── pegasus.jpg │ ├── phantom-flyknit.jpg │ ├── phantom.jpg │ ├── react-infinity.jpg │ ├── react-vision.jpg │ ├── stefan-janoski.jpg │ └── tech-challenge.jpg ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── components ├── App │ ├── App.js │ └── index.js ├── Breadcrumbs │ ├── Breadcrumbs.js │ └── index.js ├── GlobalStyles │ ├── GlobalStyles.js │ └── index.js ├── Header │ ├── Header.js │ └── index.js ├── Icon │ ├── Icon.js │ └── index.js ├── Logo │ ├── Logo.js │ └── index.js ├── SearchInput │ ├── SearchInput.js │ └── index.js ├── Select │ ├── Select.js │ └── index.js ├── ShoeCard │ ├── ShoeCard.js │ └── index.js ├── ShoeGrid │ ├── ShoeGrid.js │ └── index.js ├── ShoeIndex │ ├── ShoeIndex.js │ └── index.js ├── ShoeSidebar │ ├── ShoeSidebar.js │ └── index.js ├── Spacer │ ├── Spacer.js │ └── index.js ├── SuperHeader │ ├── SuperHeader.js │ └── index.js ├── UnstyledButton │ ├── UnstyledButton.js │ └── index.js └── VisuallyHidden │ ├── VisuallyHidden.js │ └── index.js ├── constants.js ├── data.js ├── index.js └── utils.js /.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 | 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /.new-component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "functional", 3 | "prettierConfig": { 4 | "semi": true, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Josh's Course Materials License 2 | 3 | Version 1, November 2020 4 | Copyright (c) Josh Comeau, 2020 5 | 6 | The files in this repository are meant to be used as part of a paid course, and are not intended for public distribution. They're open-source because it's the simplest form of distribution, and provides the best experience for students enrolled in the course. 7 | 8 | All are welcome to create personal copies of this repository, and modify its contents for educational use. Please experiment with the code, and see what you can build! 9 | 10 | It is forbidden to use these contents in any sort of commercial endeavour, including but not limited to: 11 | 12 | • Reselling its contents as part of a different course 13 | • Incorporating the code into a pre-existing business or project 14 | • Selling your solution to students enrolled in the course 15 | 16 | Exemptions can be made, on a case-by-case basis. Contact Josh Comeau (me@joshwcomeau.com) for more information. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sole&Ankle — Module 4 workshop 2 | 3 | In this workshop, our goal is to finish building an e-commerce store! 4 | 5 | The good news is, most of our work is done already. We just need to write some additional CSS to construct the layout; things are a bit messy right now! 6 | 7 | - Access the Figma: https://www.figma.com/file/kAL3AumTUV11y1IqHhltB6/Sole-and-Ankle-%E2%80%94-Mockup 8 | 9 | This project uses Create React App. To get started, run the following terminal commands: 10 | 11 | - `npm install` 12 | - `npm run start` 13 | 14 | You can then visit the app in-browser; it defaults to http://localhost:3000. 15 | 16 | _Note that we're only focusing on the design._ The links and inputs don't do anything. 17 | 18 | > **Want a bigger challenge?** 19 | > 20 | > This workshop comes with a lot of starter code — we'll be adding 21 | > Flex-specific properties, but for the most part, we don't have a 22 | > ton of code to write. If you'd prefer, you can build the app from 23 | > scratch, to practice all the CSS we've learned so far! 24 | > 25 | > If you go that route, you can find the sneaker assets you need in 26 | > `/public/assets`, and their metadata in `/src/data.js`. Design 27 | > tokens can be found in `/src/constants`. The custom font is 28 | > Raleway, from Google Fonts. 29 | 30 | ## Troubleshooting 31 | 32 | If you run into problems running a local development server, check out our [Troubleshooting Guide](https://courses.joshwcomeau.com/troubleshooting) on the course platform. 33 | 34 | This guide addresses the common `Digital Envelope Routine` error you may have seen. 35 | 36 | --- 37 | 38 | ## Exercise 1: Superheader 39 | 40 | Let's build the “Superheader” a thin grey strip that runs along the top of the page: 41 | 42 | ![Close-up screenshot of the superheader](./docs/exercise-1-solution.png) 43 | 44 | Use Flexbox to correctly align the elements within `src/components/SuperHeader`. 45 | 46 | ## Exercise 2: Header 47 | 48 | Continuing on down, let's tackle the main header: 49 | 50 | ![Close-up screenshot of the header and superheader](./docs/exercise-2-solution.png) 51 | 52 | The trickiest part of this exercise is the _position of the main navigation_. We want it to be perfectly centered within the container: 53 | 54 | ![Screenshot showing the position of the navigation within the header](./docs/nav-position.png) 55 | 56 | This is a thorny problem, and it's not something we've explicitly seen in the course. Give it your best shot, but please don't be discouraged if you can't figure it out! 57 | 58 | ## Exercise 3: Shell 59 | 60 | Next up, we want to tackle the "framing" around the shoe grid — the sidebar and title/filter. 61 | 62 | ![Screenshot of the store, with everything except the sneaker grid](./docs/exercise-3-solution.png) 63 | 64 | _NOTE:_ To make life a bit easier, you may wish to comment out the `` component. We'll work on integrating it in the next exercise. 65 | 66 | ## Exercise 4: Shoe Grid 67 | 68 | This exercise features two mini-challenges. The second one is a chance to revisit some of the lessons learned in previous modules, and isn't as specific to Flexbox. 69 | 70 | ### 4A: Grid layout 71 | 72 | Time to tackle the main feature of this application, the shoes! 73 | 74 | Here's a screenshot of the final result: 75 | 76 | ![Screenshot of the store, with sneaker grid](./docs/exercise-4a-solution.png) 77 | 78 | This is a tricky problem to solve with Flexbox—CSS Grid is a better tool for this job! Nevertheless, it can be done using Flexbox, with one caveat: the last row may be oversized: 79 | 80 | ![Screenshot of the shoe grid with one enormous sneaker, spanning 4 typical columns](./docs/giant-sneaker.png) 81 | 82 | In a future module, we'll revisit this and see how CSS Grid can help us out :) 83 | 84 | ## 4B: Final touches 85 | 86 | Our sneaker store is in pretty good shape, but there's two small details missing: 87 | 88 | 1. The “Sale” and “Just Released” flags. 89 | 1. The crossed-out prices, for items that are on sale. 90 | 91 | Inside `ShoeCard.js`, you'll find a `variant` variable you can use to figure out which flag, if any, needs to be rendered. It's up to you to create the flag, using styled-components. 92 | 93 | For the crossed-out prices, you can use the `price` and `salePrice` props. 94 | 95 | ![Screenshot of the store, with the final details added](./docs/exercise-4b-solution.png) 96 | 97 | _NOTE:_ This exercise has minimal flexbox implications, and is mainly about revisiting lessons learned in the previous modules (including positioned layout and styled-components). Feel free to skip it if you'd prefer! 98 | 99 | ## To be continued! 100 | 101 | Our sneaker store can flex to support different screen sizes, but there isn't a proper mobile or tablet view. Don't fret — we will revisit this workshop in a future module! 102 | 103 | In the meantime, take a moment to congratulate yourself for making it through the Flexbox module!! 104 | -------------------------------------------------------------------------------- /docs/exercise-1-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/docs/exercise-1-solution.png -------------------------------------------------------------------------------- /docs/exercise-2-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/docs/exercise-2-solution.png -------------------------------------------------------------------------------- /docs/exercise-3-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/docs/exercise-3-solution.png -------------------------------------------------------------------------------- /docs/exercise-4a-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/docs/exercise-4a-solution.png -------------------------------------------------------------------------------- /docs/exercise-4b-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/docs/exercise-4b-solution.png -------------------------------------------------------------------------------- /docs/giant-sneaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/docs/giant-sneaker.png -------------------------------------------------------------------------------- /docs/nav-position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/docs/nav-position.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sole-and-ankle", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "date-fns": "2.16.1", 7 | "react": "17.0.1", 8 | "react-dom": "17.0.1", 9 | "react-feather": "2.0.9", 10 | "react-scripts": "4.0.1", 11 | "styled-components": "5.2.1" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": [ 21 | "react-app", 22 | "react-app/jest" 23 | ] 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/assets/flyknit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/flyknit.jpg -------------------------------------------------------------------------------- /public/assets/joyride.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/joyride.jpg -------------------------------------------------------------------------------- /public/assets/lebron.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/lebron.jpg -------------------------------------------------------------------------------- /public/assets/legend-academy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/legend-academy.jpg -------------------------------------------------------------------------------- /public/assets/metcon-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/metcon-5.jpg -------------------------------------------------------------------------------- /public/assets/pegasus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/pegasus.jpg -------------------------------------------------------------------------------- /public/assets/phantom-flyknit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/phantom-flyknit.jpg -------------------------------------------------------------------------------- /public/assets/phantom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/phantom.jpg -------------------------------------------------------------------------------- /public/assets/react-infinity.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/react-infinity.jpg -------------------------------------------------------------------------------- /public/assets/react-vision.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/react-vision.jpg -------------------------------------------------------------------------------- /public/assets/stefan-janoski.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/stefan-janoski.jpg -------------------------------------------------------------------------------- /public/assets/tech-challenge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/assets/tech-challenge.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | Sole&Ankle — CSS for JavaScript Developers 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/css-for-js/sole-and-ankle/8dbf156d266f5b0c32f09eafe6047c4c49eee7ee/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import Header from '../Header'; 5 | import ShoeIndex from '../ShoeIndex'; 6 | 7 | const App = () => { 8 | const [sortId, setSortId] = React.useState('newest'); 9 | 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 | 17 | ); 18 | }; 19 | 20 | const Main = styled.main` 21 | padding: 64px 32px; 22 | `; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './App'; 2 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs/Breadcrumbs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { COLORS } from '../../constants'; 5 | 6 | const Breadcrumbs = ({ children }) => { 7 | return {children}; 8 | }; 9 | 10 | Breadcrumbs.Crumb = ({ href, children, delegated }) => { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | }; 19 | 20 | const CrumbWrapper = styled.div` 21 | &:not(:first-of-type) { 22 | margin-left: 8px; 23 | 24 | &::before { 25 | content: '/'; 26 | margin-right: 8px; 27 | color: ${COLORS.gray[300]}; 28 | } 29 | } 30 | `; 31 | 32 | const CrumbLink = styled.a` 33 | color: ${COLORS.gray[700]}; 34 | text-decoration: none; 35 | 36 | &:hover { 37 | color: ${COLORS.gray[900]}; 38 | } 39 | `; 40 | 41 | const Wrapper = styled.nav` 42 | display: flex; 43 | font-size: 0.875rem; 44 | `; 45 | export default Breadcrumbs; 46 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Breadcrumbs'; 2 | -------------------------------------------------------------------------------- /src/components/GlobalStyles/GlobalStyles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyles = createGlobalStyle` 4 | /* http://meyerweb.com/eric/tools/css/reset/ 5 | v2.0 | 20110126 6 | License: none (public domain) 7 | */ 8 | html, body, div, span, applet, object, iframe, 9 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 10 | a, abbr, acronym, address, big, cite, code, 11 | del, dfn, em, img, ins, kbd, q, s, samp, 12 | small, strike, strong, sub, sup, tt, var, 13 | b, u, i, center, 14 | dl, dt, dd, ol, ul, li, 15 | fieldset, form, label, legend, 16 | table, caption, tbody, tfoot, thead, tr, th, td, 17 | article, aside, canvas, details, embed, 18 | figure, figcaption, footer, header, hgroup, 19 | menu, nav, output, ruby, section, summary, 20 | time, mark, audio, video { 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | font-size: 100%; 25 | vertical-align: baseline; 26 | } 27 | /* HTML5 display-role reset for older browsers */ 28 | article, aside, details, figcaption, figure, 29 | footer, header, hgroup, menu, nav, section { 30 | display: block; 31 | } 32 | ol, ul { 33 | list-style: none; 34 | } 35 | blockquote, q { 36 | quotes: none; 37 | } 38 | blockquote:before, blockquote:after, 39 | q:before, q:after { 40 | content: ''; 41 | content: none; 42 | } 43 | table { 44 | border-collapse: collapse; 45 | border-spacing: 0; 46 | } 47 | 48 | 49 | /* GLOBAL STYLES */ 50 | *, 51 | *:before, 52 | *:after { 53 | box-sizing: border-box; 54 | line-height: 1.45; 55 | font-family: 'Raleway', sans-serif; 56 | -webkit-font-smoothing: antialiased; 57 | -moz-osx-font-smoothing: auto; 58 | } 59 | 60 | #root { 61 | /* 62 | Create a stacking context, without a z-index. 63 | This ensures that all portal content (modals and tooltips) will 64 | float above the app. 65 | */ 66 | isolation: isolate; 67 | } 68 | 69 | html, body, #root { 70 | height: 100%; 71 | } 72 | `; 73 | 74 | export default GlobalStyles; 75 | -------------------------------------------------------------------------------- /src/components/GlobalStyles/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './GlobalStyles'; 2 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { COLORS, WEIGHTS } from '../../constants'; 5 | import Logo from '../Logo'; 6 | import SuperHeader from '../SuperHeader'; 7 | 8 | const Header = () => { 9 | // Our site features two visual headers, but they should be 10 | // grouped semantically as a single header. 11 | return ( 12 |
13 | 14 | 15 | 16 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | const MainHeader = styled.div` 30 | padding: 0 32px; 31 | border-bottom: 1px solid ${COLORS.gray[300]}; 32 | `; 33 | 34 | const Nav = styled.nav``; 35 | 36 | const NavLink = styled.a` 37 | font-size: 1.125rem; 38 | text-transform: uppercase; 39 | text-decoration: none; 40 | color: ${COLORS.gray[900]}; 41 | font-weight: ${WEIGHTS.medium}; 42 | 43 | &:first-of-type { 44 | color: ${COLORS.secondary}; 45 | } 46 | `; 47 | 48 | export default Header; 49 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Header'; 2 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | import { 4 | Search, 5 | Menu, 6 | ShoppingBag, 7 | ChevronDown, 8 | } from 'react-feather'; 9 | 10 | const icons = { 11 | search: Search, 12 | menu: Menu, 13 | 'shopping-bag': ShoppingBag, 14 | 'chevron-down': ChevronDown, 15 | }; 16 | 17 | const Icon = ({ id, color, size, strokeWidth, ...delegated }) => { 18 | const Component = icons[id]; 19 | 20 | if (!Component) { 21 | throw new Error(`No icon found for ID: ${id}`); 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | const Wrapper = styled.div` 32 | & > svg { 33 | display: block; 34 | stroke-width: ${(p) => p.strokeWidth}px; 35 | } 36 | `; 37 | 38 | export default Icon; 39 | -------------------------------------------------------------------------------- /src/components/Icon/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Icon'; 2 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { WEIGHTS } from '../../constants'; 5 | 6 | const Logo = (props) => { 7 | return ( 8 | 9 | Sole&Ankle 10 | 11 | ); 12 | }; 13 | 14 | const Link = styled.a` 15 | text-decoration: none; 16 | color: inherit; 17 | `; 18 | 19 | const Wrapper = styled.h1` 20 | font-size: 1.5rem; 21 | font-weight: ${WEIGHTS.bold}; 22 | `; 23 | 24 | export default Logo; 25 | -------------------------------------------------------------------------------- /src/components/Logo/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Logo'; 2 | -------------------------------------------------------------------------------- /src/components/SearchInput/SearchInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { COLORS } from '../../constants'; 5 | import VisuallyHidden from '../VisuallyHidden'; 6 | import Icon from '../Icon'; 7 | 8 | const SearchInput = ({ label, ...delegated }) => { 9 | return ( 10 | 15 | ); 16 | }; 17 | 18 | const Label = styled.label` 19 | position: relative; 20 | `; 21 | 22 | const Input = styled.input` 23 | border: none; 24 | background: transparent; 25 | border-bottom: 1px solid ${COLORS.gray[300]}; 26 | padding-left: 24px; 27 | font-size: 0.875rem; 28 | color: ${COLORS.gray[100]}; 29 | outline-offset: 4px; 30 | 31 | &::placeholder { 32 | color: ${COLORS.gray[500]}; 33 | } 34 | `; 35 | 36 | const SearchIcon = styled(Icon)` 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | bottom: 0; 41 | margin: auto; 42 | width: 16px; 43 | height: 16px; 44 | `; 45 | 46 | export default SearchInput; 47 | -------------------------------------------------------------------------------- /src/components/SearchInput/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SearchInput'; 2 | -------------------------------------------------------------------------------- /src/components/Select/Select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { COLORS, WEIGHTS } from '../../constants'; 5 | import Icon from '../Icon'; 6 | 7 | const Select = ({ label, value, children, ...delegated }) => { 8 | const childArray = React.Children.toArray(children); 9 | const selectedChild = childArray.find( 10 | (child) => child.props.value === value 11 | ); 12 | 13 | const displayedValue = selectedChild.props.children; 14 | 15 | return ( 16 | 17 | {label} 18 | 19 | 20 | {children} 21 | 22 | 23 | {displayedValue} 24 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | const Wrapper = styled.label``; 36 | 37 | const VisibleLabel = styled.span` 38 | color: ${COLORS.gray[700]}; 39 | margin-right: 16px; 40 | `; 41 | 42 | const SelectWrapper = styled.div` 43 | position: relative; 44 | `; 45 | 46 | const NativeSelect = styled.select` 47 | opacity: 0; 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | right: 0; 52 | bottom: 0; 53 | width: 100%; 54 | height: 100%; 55 | cursor: pointer; 56 | `; 57 | 58 | const DisplayedBit = styled.span` 59 | display: block; 60 | background: ${COLORS.gray[100]}; 61 | font-size: 1rem; 62 | font-weight: ${WEIGHTS.medium}; 63 | color: ${COLORS.gray[900]}; 64 | padding: 12px 42px 12px 16px; 65 | border-radius: 8px; 66 | pointer-events: none; 67 | 68 | ${NativeSelect}:focus ~ & { 69 | outline: 1px dotted #212121; 70 | outline: 5px auto -webkit-focus-ring-color; 71 | } 72 | `; 73 | 74 | const ChevronIcon = styled(Icon)` 75 | position: absolute; 76 | top: 0; 77 | right: 9px; 78 | bottom: 0; 79 | margin: auto; 80 | width: 24px; 81 | height: 24px; 82 | `; 83 | 84 | export default Select; 85 | -------------------------------------------------------------------------------- /src/components/Select/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Select'; 2 | -------------------------------------------------------------------------------- /src/components/ShoeCard/ShoeCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { COLORS, WEIGHTS } from '../../constants'; 5 | import { formatPrice, pluralize, isNewShoe } from '../../utils'; 6 | import Spacer from '../Spacer'; 7 | 8 | const ShoeCard = ({ 9 | slug, 10 | name, 11 | imageSrc, 12 | price, 13 | salePrice, 14 | releaseDate, 15 | numOfColors, 16 | }) => { 17 | // There are 3 variants possible, based on the props: 18 | // - new-release 19 | // - on-sale 20 | // - default 21 | // 22 | // Any shoe released in the last month will be considered 23 | // `new-release`. Any shoe with a `salePrice` will be 24 | // on-sale. In theory, it is possible for a shoe to be 25 | // both on-sale and new-release, but in this case, `on-sale` 26 | // will triumph and be the variant used. 27 | // prettier-ignore 28 | const variant = typeof salePrice === 'number' 29 | ? 'on-sale' 30 | : isNewShoe(releaseDate) 31 | ? 'new-release' 32 | : 'default' 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {name} 43 | {formatPrice(price)} 44 | 45 | 46 | {pluralize('Color', numOfColors)} 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | const Link = styled.a` 54 | text-decoration: none; 55 | color: inherit; 56 | `; 57 | 58 | const Wrapper = styled.article``; 59 | 60 | const ImageWrapper = styled.div` 61 | position: relative; 62 | `; 63 | 64 | const Image = styled.img``; 65 | 66 | const Row = styled.div` 67 | font-size: 1rem; 68 | `; 69 | 70 | const Name = styled.h3` 71 | font-weight: ${WEIGHTS.medium}; 72 | color: ${COLORS.gray[900]}; 73 | `; 74 | 75 | const Price = styled.span``; 76 | 77 | const ColorInfo = styled.p` 78 | color: ${COLORS.gray[700]}; 79 | `; 80 | 81 | const SalePrice = styled.span` 82 | font-weight: ${WEIGHTS.medium}; 83 | color: ${COLORS.primary}; 84 | `; 85 | 86 | export default ShoeCard; 87 | -------------------------------------------------------------------------------- /src/components/ShoeCard/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ShoeCard'; 2 | -------------------------------------------------------------------------------- /src/components/ShoeGrid/ShoeGrid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import SHOES from '../../data'; 5 | import ShoeCard from '../ShoeCard'; 6 | 7 | const ShoeGrid = () => { 8 | return ( 9 | 10 | {SHOES.map((shoe) => ( 11 | 12 | ))} 13 | 14 | ); 15 | }; 16 | 17 | const Wrapper = styled.div``; 18 | 19 | export default ShoeGrid; 20 | -------------------------------------------------------------------------------- /src/components/ShoeGrid/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ShoeGrid'; 2 | -------------------------------------------------------------------------------- /src/components/ShoeIndex/ShoeIndex.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { WEIGHTS } from '../../constants'; 5 | 6 | import Breadcrumbs from '../Breadcrumbs'; 7 | import Select from '../Select'; 8 | import Spacer from '../Spacer'; 9 | import ShoeSidebar from '../ShoeSidebar'; 10 | import ShoeGrid from '../ShoeGrid'; 11 | 12 | const ShoeIndex = ({ sortId, setSortId }) => { 13 | return ( 14 | 15 | 16 |
17 | Running 18 | 26 |
27 | 28 | 29 |
30 | 31 | 32 | Home 33 | Sale 34 | 35 | Shoes 36 | 37 | 38 | 39 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | const Wrapper = styled.div``; 46 | 47 | const LeftColumn = styled.div``; 48 | 49 | const MainColumn = styled.div``; 50 | 51 | const Header = styled.header``; 52 | 53 | const Title = styled.h2` 54 | font-size: 1.5rem; 55 | font-weight: ${WEIGHTS.medium}; 56 | `; 57 | 58 | export default ShoeIndex; 59 | -------------------------------------------------------------------------------- /src/components/ShoeIndex/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ShoeIndex'; 2 | -------------------------------------------------------------------------------- /src/components/ShoeSidebar/ShoeSidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | import { COLORS, WEIGHTS } from '../../constants'; 5 | 6 | const Sidebar = () => { 7 | return ( 8 | 9 | Lifestyle 10 | Jordan 11 | Running 12 | Basketball 13 | Training & Gym 14 | Football 15 | Skateboarding 16 | American Football 17 | Baseball 18 | Golf 19 | Tennis 20 | Athletics 21 | Walking 22 | 23 | ); 24 | }; 25 | 26 | const Wrapper = styled.aside``; 27 | 28 | const Link = styled.a` 29 | display: block; 30 | text-decoration: none; 31 | font-weight: ${WEIGHTS.medium}; 32 | color: ${COLORS.gray[900]}; 33 | line-height: 2; 34 | `; 35 | 36 | const ActiveLink = styled(Link)` 37 | color: ${COLORS.primary}; 38 | `; 39 | 40 | export default Sidebar; 41 | -------------------------------------------------------------------------------- /src/components/ShoeSidebar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ShoeSidebar'; 2 | -------------------------------------------------------------------------------- /src/components/Spacer/Spacer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | function getHeight({ axis, size }) { 4 | return axis === 'horizontal' ? 1 : size; 5 | } 6 | function getWidth({ axis, size }) { 7 | return axis === 'vertical' ? 1 : size; 8 | } 9 | 10 | const Spacer = styled.span` 11 | display: block; 12 | width: ${getWidth}px; 13 | min-width: ${getWidth}px; 14 | height: ${getHeight}px; 15 | min-height: ${getHeight}px; 16 | `; 17 | 18 | export default Spacer; 19 | -------------------------------------------------------------------------------- /src/components/Spacer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Spacer'; 2 | -------------------------------------------------------------------------------- /src/components/SuperHeader/SuperHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { COLORS } from '../../constants'; 5 | 6 | import SearchInput from '../SearchInput'; 7 | import UnstyledButton from '../UnstyledButton'; 8 | import Icon from '../Icon'; 9 | 10 | const SuperHeader = () => { 11 | return ( 12 | 13 | 14 | Free shipping on domestic orders over $75! 15 | 16 | 17 | Help 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | const Wrapper = styled.div` 26 | font-size: 0.875rem; 27 | color: ${COLORS.gray[300]}; 28 | background-color: ${COLORS.gray[900]}; 29 | `; 30 | 31 | const MarketingMessage = styled.span` 32 | color: ${COLORS.white}; 33 | `; 34 | 35 | const HelpLink = styled.a` 36 | color: inherit; 37 | text-decoration: none; 38 | outline-offset: 2px; 39 | 40 | &:not(:focus-visible) { 41 | outline: none; 42 | } 43 | `; 44 | 45 | export default SuperHeader; 46 | -------------------------------------------------------------------------------- /src/components/SuperHeader/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SuperHeader'; 2 | -------------------------------------------------------------------------------- /src/components/UnstyledButton/UnstyledButton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export default styled.button` 4 | display: ${(props) => props.display || 'block'}; 5 | margin: 0; 6 | padding: 0; 7 | border: none; 8 | background: transparent; 9 | cursor: pointer; 10 | text-align: left; 11 | font: inherit; 12 | color: inherit; 13 | 14 | &:focus { 15 | outline-offset: 2px; 16 | } 17 | 18 | &:focus:not(:focus-visible) { 19 | outline: none; 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/components/UnstyledButton/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UnstyledButton'; 2 | -------------------------------------------------------------------------------- /src/components/VisuallyHidden/VisuallyHidden.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | const VisuallyHidden = ({ children, ...delegated }) => { 5 | const [forceShow, setForceShow] = React.useState(false); 6 | 7 | React.useEffect(() => { 8 | if (process.env.NODE_ENV !== 'production') { 9 | const handleKeyDown = (ev) => { 10 | if (ev.key === 'Alt') { 11 | setForceShow(true); 12 | } 13 | }; 14 | 15 | const handleKeyUp = () => { 16 | setForceShow(false); 17 | }; 18 | 19 | window.addEventListener('keydown', handleKeyDown); 20 | window.addEventListener('keyup', handleKeyUp); 21 | 22 | return () => { 23 | window.removeEventListener('keydown', handleKeyDown); 24 | window.removeEventListener('keydown', handleKeyUp); 25 | }; 26 | } 27 | }, []); 28 | 29 | if (forceShow) { 30 | return children; 31 | } 32 | 33 | return {children}; 34 | }; 35 | 36 | const Wrapper = styled.div` 37 | position: absolute; 38 | overflow: hidden; 39 | clip: rect(0 0 0 0); 40 | height: 1px; 41 | width: 1px; 42 | margin: -1px; 43 | padding: 0; 44 | border: 0; 45 | `; 46 | 47 | export default VisuallyHidden; 48 | -------------------------------------------------------------------------------- /src/components/VisuallyHidden/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './VisuallyHidden'; 2 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | white: 'hsl(0deg, 0%, 100%)', 3 | gray: { 4 | 100: 'hsl(185deg, 5%, 95%)', 5 | 300: 'hsl(190deg, 5%, 80%)', 6 | 500: 'hsl(196deg, 4%, 60%)', 7 | 700: 'hsl(220deg, 5%, 40%)', 8 | 900: 'hsl(220deg, 3%, 20%)', 9 | }, 10 | primary: 'hsl(340deg, 65%, 47%)', 11 | secondary: 'hsl(240deg, 60%, 63%)', 12 | }; 13 | 14 | export const WEIGHTS = { 15 | normal: 500, 16 | medium: 600, 17 | bold: 800, 18 | }; 19 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In a real app, this data would likely live in a database, 3 | * and be fetched from an API, either at runtime or at 4 | * compile-time. 5 | * 6 | * Keep in mind, this workshop is focused on CSS. In order 7 | * to make it easy to focus on the styling, we're cutting 8 | * some corners when it comes to our data management, and 9 | * our JavaScript in general. Please don't try to glean 10 | * any best-practices from stuff like this data file! 11 | */ 12 | 13 | const SHOES = [ 14 | { 15 | slug: 'tech-challenge', 16 | name: 'NikeCourt Tech Challenge 20', 17 | imageSrc: '/assets/tech-challenge.jpg', 18 | price: 16500, 19 | salePrice: null, 20 | // 1 hour ago! 🔥 21 | releaseDate: Date.now() - 1000 * 60 * 60 * 1, 22 | numOfColors: 2, 23 | }, 24 | { 25 | slug: 'metcon-5', 26 | name: 'Nike Metcon 5 AMP', 27 | imageSrc: '/assets/metcon-5.jpg', 28 | price: 16500, 29 | salePrice: null, 30 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 2, 31 | numOfColors: 1, 32 | }, 33 | { 34 | slug: 'phantom', 35 | name: 'Nike Phantom Vision', 36 | imageSrc: '/assets/phantom.jpg', 37 | price: 16500, 38 | salePrice: null, 39 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 4, 40 | numOfColors: 4, 41 | }, 42 | { 43 | slug: 'pegasus', 44 | name: 'Nike Air Zoom Pegasus', 45 | imageSrc: '/assets/pegasus.jpg', 46 | price: 16500, 47 | salePrice: null, 48 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 16, 49 | numOfColors: 1, 50 | }, 51 | { 52 | slug: 'joyride', 53 | name: 'Nike Joyride Dual Run', 54 | imageSrc: '/assets/joyride.jpg', 55 | price: 17000, 56 | salePrice: null, 57 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 40, 58 | numOfColors: 2, 59 | }, 60 | { 61 | slug: 'legend-academy', 62 | name: 'Nike Tiempo Legend 8', 63 | imageSrc: '/assets/legend-academy.jpg', 64 | price: 16500, 65 | salePrice: 12500, 66 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 50, 67 | numOfColors: 8, 68 | }, 69 | { 70 | slug: 'react-infinity', 71 | name: 'Nike React Infinity Pro', 72 | imageSrc: '/assets/react-infinity.jpg', 73 | price: 16000, 74 | salePrice: 14500, 75 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 75, 76 | numOfColors: 1, 77 | }, 78 | { 79 | slug: 'phantom-flyknit', 80 | name: 'Nike React Phantom Run Flyknit 2', 81 | imageSrc: '/assets/phantom-flyknit.jpg', 82 | price: 18500, 83 | salePrice: 16000, 84 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 100, 85 | numOfColors: 4, 86 | }, 87 | { 88 | slug: 'lebron', 89 | name: 'LeBron 17', 90 | imageSrc: '/assets/lebron.jpg', 91 | price: 26000, 92 | salePrice: null, 93 | releaseDate: Date.now() - 1000 * 60 * 60 * 24 * 120, 94 | numOfColors: 1, 95 | }, 96 | ]; 97 | 98 | export default SHOES; 99 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | import GlobalStyles from './components/GlobalStyles'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Several little utilities for this project. 3 | * 4 | * NOTE: These are NOT generic, and should not be copied 5 | * to other projects. They're quick imperfect implementations 6 | * for the known, fixed data we work with here. 7 | */ 8 | import differenceInDays from 'date-fns/differenceInDays'; 9 | 10 | export function formatPrice(price) { 11 | return `$${price / 100}`; 12 | } 13 | 14 | export function pluralize(string, num) { 15 | return num === 1 ? `1 ${string}` : `${num} ${string}s`; 16 | } 17 | 18 | export function isNewShoe(releaseDate) { 19 | return differenceInDays(new Date(), releaseDate) < 30; 20 | } 21 | --------------------------------------------------------------------------------