├── .babelrc ├── .gitignore ├── .prettierrc ├── README.md ├── index.html ├── package.json ├── src ├── components │ ├── add │ │ ├── add.js │ │ ├── add.scss │ │ └── codemirror.js │ ├── app │ │ ├── app.js │ │ ├── app.scss │ │ ├── header.js │ │ └── router.js │ ├── dashboard │ │ ├── chart.js │ │ ├── dashboard.js │ │ └── dashboard.scss │ ├── home │ │ ├── home.js │ │ └── home.scss │ ├── images │ │ ├── carrot.svg │ │ ├── header.jpg │ │ ├── header.png │ │ ├── logo.svg │ │ └── shame-box.svg │ ├── layout │ │ ├── fonts │ │ │ ├── roboto-mono-v6-latin-italic.woff │ │ │ ├── roboto-mono-v6-latin-italic.woff2 │ │ │ ├── roboto-mono-v6-latin-regular.woff │ │ │ └── roboto-mono-v6-latin-regular.woff2 │ │ ├── layout.js │ │ ├── layout.scss │ │ └── mono-font.scss │ ├── loading │ │ ├── loading.js │ │ └── loading.scss │ ├── login │ │ ├── login.js │ │ └── login.scss │ ├── select │ │ └── select.js │ ├── shamelist │ │ ├── code.js │ │ ├── controls.js │ │ ├── filters.js │ │ ├── shame.js │ │ ├── shamelist.js │ │ └── shamelist.scss │ └── user │ │ ├── user.js │ │ └── user.scss ├── constants │ └── index.js ├── context │ ├── analytics.js │ ├── shamecaps.js │ └── user.js ├── index.js └── pages │ ├── add.js │ ├── dashboard.js │ ├── home.js │ ├── login.js │ └── user.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "useBuiltIns": "usage", "corejs": 2 }], 4 | "@babel/react" 5 | ], 6 | "plugins": ["@babel/plugin-syntax-dynamic-import"] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_* 4 | slides/ 5 | .vscode 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Performance Tuning for React Applications 2 | 3 | This is the source code for an advanced React Performance workshop. It was designed by [Sara Vieira](https://github.com/SaraVieira), [Sia Karamalegos](https://github.com/siakaramalegos), and [Jason Lengstorf](https://github.com/jlengstorf). 4 | 5 | - [Abstract](#abstract) 6 | - [Prerequisites](#prerequisites) 7 | - [About the trainers](#about-the-trainers) 8 | - [Slides and controls](#slides-and-controls) 9 | - [Resources](#resources) 10 | 11 | ## Abstract 12 | Getting performance right is hard, even when you have the luxury of starting an app from scratch. It's even harder when you need to improve the performance of existing apps, as is so often the case. In this workshop, Sia Karamalegos, Sara Vieira, and Jason Lengstorf will lead you through the process of: 13 | 14 | - Assessing an existing React app 15 | - Diagnosing performance problems 16 | - Prioritizing highest-impact solution 17 | - Implementing performance fixes 18 | 19 | During this full-day workshop, you’ll learn advanced techniques for improving the performance of React apps, including: 20 | 21 | - Lazy loading resources & components with React.lazy and Suspense 22 | - Leveraging service workers for performance 23 | - Seamlessly preloading and prefetching assets 24 | - Automatically optimizing images 25 | - Dynamically subsetting fonts 26 | - Mitigating the performance impact of third-party scripts 27 | - Code splitting and bundle optimization 28 | - Using psychology to make an app feel faster than it actually is 29 | 30 | By the end of the workshop, you’ll be able to diagnose and solve a variety of real-world performance problems. You’ll also learn how to weigh trade-offs to ensure that both your apps and your teams perform well. The tools added to your toolbox will continue to serve you, your team, and your users for years to come. 31 | 32 | ## Prerequisites 33 | 34 | Intermediate React knowledge, Node/npm installed, Git installed, basic command line knowledge. 35 | 36 | ## About the trainers 37 | 38 | **Sara Vieira** is a developer @CodeSandbox and an international agent of JS Bullshit 🚀, open sorcerer, maker of useless modules, Blogger, Drummer and horror movie fan girl. 39 | 40 | [Twitter](https://twitter.com/NikkitaFTW) | [Website](https://iamsaravieira.com/) | [Github](https://github.com/SaraVieira) 41 | 42 | **Jason Lengstorf** is a developer advocate, senior engineer, and occasional designer at Gatsby. He’s an advocate for building highly productive teams through better communication, well designed systems and processes, and healthy work-life balance, and he blogs about that sometimes. He lives in Portland, Oregon. 43 | 44 | [Twitter](https://twitter.com/jlengstorf) | [Website](https://lengstorf.com/) | [Github](https://github.com/jlengstorf) | [Blog](https://lengstorf.com/blog) | [Facebook](https://www.facebook.com/jlengstorf) 45 | 46 | **Sia Karamalegos** is a developer, international conference speaker, and writer. She is a Google Developer Expert in Web Technologies and a Women Techmakers ambassador. She co-organizes #FrontEndParty, GDG New Orleans, and NOLA Hack Night in the New Orleans area. She is the founder and lead developer for Clio + Calliope Web Development and was recognized in the Silicon Bayou 100, the 100 most influential and active people in tech and entrepreneurship in Louisiana. When she's not coding, speaking, or consulting, Sia likes to design crochet patterns and dabble in charcoal figure drawing. She's also an avid endurance athlete. 47 | 48 | [Twitter](https://twitter.com/thegreengreek) | [Website](https://siakaramalegos.github.io/) | [Github](https://github.com/siakaramalegos) | [Blog](https://medium.com/@thegreengreek) | [StackOverflow](https://stackoverflow.com/users/5049215/sia?tab=profile) | [LinkedIn](https://www.linkedin.com/in/karamalegos) 49 | 50 | ## Slides and Controls 51 | 52 | The slides are deployed [here](https://siakaramalegos.github.io/react-perf-workshop/#/). To advance the slides, use `n` for next and `p` for previous. The right arrow jumps to the next section (and left for previous section). Up and down to advance through slides within a section. 53 | 54 | ## Resources 55 | 56 | Want to learn more about how to measure and improve performance? Here you go: 57 | 58 | - [Modern DevTools course](https://moderndevtools.com/) by Umar Hansa 59 | - [/dev tips](https://umaar.com/dev-tips/) by Umar Hansa 60 | - [Making Google Fonts Faster](https://medium.com/clio-calliope/making-google-fonts-faster-aadf3c02a36d) 61 | - [Loading Fonts With Webpack](https://chriscourses.com/blog/loading-fonts-webpack) 62 | - [Optimize Website Speed with Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/speed/get-started) by Kayce Basques 63 | - [Assessing Loading Performance in Real Life with Navigation and Resource Timing](https://developers.google.com/web/fundamentals/performance/navigation-and-resource-timing/) by Jeremy Wagner 64 | - [User Timing API – Measuring User Experience Performance](https://www.keycdn.com/blog/user-timing/) by Cody Arsenault 65 | - [Measure Performance with the RAIL Model](https://developers.google.com/web/fundamentals/performance/rail) by Meggin Kearney, et. al. 66 | - [Reduce JavaScript Payloads with Tree Shaking](https://developers.google.com/web/fundamentals/performance/optimizing-javascript/tree-shaking/) by Jeremy Wagner 67 | - [Reduce JavaScript Payloads with Code Splitting](https://developers.google.com/web/fundamentals/performance/optimizing-javascript/code-splitting/) by Jeremy Wagner and Addy Osmani 68 | - [Lazy Loading Images and Video](https://developers.google.com/web/fundamentals/performance/lazy-loading-guidance/images-and-video/) by Jeremy Wagner 69 | - [The Cost of JavaScript in 2018](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4) by Addy Osmani 70 | - [The Cost of JavaScript](https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e) by Addy Osmani 71 | - [Why Web Developers Need to Care about Interactivity](https://philipwalton.com/articles/why-web-developers-need-to-care-about-interactivity/) by Philip Walton 72 | - [Preload, Prefetch And Priorities in Chrome](https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf) by Addy Osmani 73 | - [Webpack build analysis](https://survivejs.com/webpack/optimizing/build-analysis/) on SurviveJS by Juho Vepsäläinen 74 | - [https://www.webpagetest.org/easy](https://www.webpagetest.org/easy) easy no configuration set up for a slow-3G connection and mid-level phone, specifically Chrome on a Motorola G (gen 4) tested from Dulles, Virginia on a 400 Kbps 3G connection with 400ms of latency. 75 | - [Responsive Images](https://developers.google.com/web/fundamentals/design-and-ux/responsive/images) by Pete LePage 76 | - [Responsive Images Udacity Course](https://www.udacity.com/course/responsive-images--ud882) by Google 77 | - [Optimizing Images](https://survivejs.com/webpack/loading/images/#optimizing-images) in Webpack on SurviveJS by Juho Vepsäläinen 78 | - [Can You Afford It?: Real-world Web Performance Budgets](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/) by Alex Russell 79 | - [Deploying ES2015+ Code in Production Today](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/) by Philip Walton 80 | - [HTTP Caching](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching) by Ilya Grigorik 81 | - [Take a (Client) Hint!](https://www.youtube.com/watch?v=md7Ua82fPe4&list=PLe9psSNJBf75O6abYvvjxhm36_QU9H-f2&index=16) by Jeremy Wagner 82 | - [To push, or not to push?! - The future of HTTP/2 server push](https://www.youtube.com/watch?v=ga_-zsTHRm8&list=PLe9psSNJBf75O6abYvvjxhm36_QU9H-f2&index=24) by Patrick Hamann 83 | - [The Future is Fast](https://jlengstorf.github.io/presentations/the-future-is-fast/#/) on performance and Gatsby.js by Jason Lengstorf 84 | - If you have the chance, attend one of [Harry Robert's talks or workshops](https://csswizardry.com/workshops/) on performance 85 | - [Does my site need HTTPS?](https://doesmysiteneedhttps.com/) 86 | 87 | Other sources: 88 | 89 | - [Speed is now a landing page factor for Google Search and Ads](https://developers.google.com/web/updates/2018/07/search-ads-speed) by Addy Osmani and Ilya Grigorik 90 | - [WPO Stats](https://wpostats.com/) - Case studies and experiments demonstrating the impact of web performance optimization (WPO) on user experience and business metrics 91 | - [Why performance matters](https://developers.google.com/web/fundamentals/performance/why-performance-matters/) by Jeremy Wagner 92 | - [End to End Apps with Polymer (Polymer Summit 2017)](https://www.youtube.com/watch?v=0A-2BhEZiM4) talk by Kevin Schaaf which also shows the time to interactive video 93 | - [This is why I prefer Progressive Rendering + Bootstrapping.](https://twitter.com/aerotwist/status/729712502943174657) tweet by Paul Lewis with graphic comparing progressing rendering with SSR and CSR 94 | - [Why Waiting Is Torture](http://www.nytimes.com/2012/08/19/opinion/sunday/why-waiting-in-line-is-torture.html) by Alex Stone 95 | - [The Truth About Download Time](https://articles.uie.com/download_time/) by Christine Perfetti 96 | - [The need for mobile speed: How mobile latency impacts publisher revenue](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) report by DoubleClick by Google 97 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | shame.dev 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-react-perf", 3 | "version": "1.0.0", 4 | "private": true, 5 | "contributors": [ 6 | "Jason Lengstorf ", 7 | "Sara Vieira ", 8 | "Sia Karamalegos " 9 | ], 10 | "license": "MIT", 11 | "scripts": { 12 | "start": "NODE_ENV=development webpack-dev-server", 13 | "build": "webpack" 14 | }, 15 | "devDependencies": { 16 | "@babel/cli": "^7.2.3", 17 | "@babel/core": "^7.4.0", 18 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 19 | "@babel/preset-env": "^7.4.2", 20 | "@babel/preset-react": "^7.0.0", 21 | "babel-loader": "^8.0.5", 22 | "clean-webpack-plugin": "^2.0.1", 23 | "compression-webpack-plugin": "^2.0.0", 24 | "css-loader": "^2.1.1", 25 | "file-loader": "^3.0.1", 26 | "html-webpack-plugin": "^3.2.0", 27 | "mini-css-extract-plugin": "^0.5.0", 28 | "node-sass": "^4.11.0", 29 | "optimize-css-assets-webpack-plugin": "^5.0.1", 30 | "react-svg-loader": "^2.1.0", 31 | "sass-loader": "^7.1.0", 32 | "style-loader": "^0.23.1", 33 | "webpack": "^4.29.6", 34 | "webpack-bundle-analyzer": "^3.2.0", 35 | "webpack-cli": "^3.3.0", 36 | "webpack-dev-server": "^3.2.1", 37 | "workbox-webpack-plugin": "^4.2.0" 38 | }, 39 | "dependencies": { 40 | "@babel/polyfill": "^7.4.0", 41 | "@reach/router": "^1.2.1", 42 | "codemirror": "^5.45.0", 43 | "core-js": "2", 44 | "date-fns": "^2.0.0-alpha.27", 45 | "moment": "^2.24.0", 46 | "prettier": "^1.16.4", 47 | "prism-react-renderer": "^0.1.6", 48 | "react": "^16.8.6", 49 | "react-codemirror2": "^5.1.0", 50 | "react-dom": "^16.8.6", 51 | "react-helmet": "^5.2.0", 52 | "react-vis": "^1.11.6", 53 | "slugify": "^1.3.4", 54 | "uuid": "^3.3.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/add/add.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { navigate } from '@reach/router'; 3 | import uuid from 'uuid/v4'; 4 | import slugify from 'slugify'; 5 | import { useUser } from '../../context/user'; 6 | import { useShamecaps } from '../../context/shamecaps'; 7 | import { LANGUAGES, TYPES } from '../../constants'; 8 | import Layout from '../layout/layout'; 9 | import Select from '../select/select'; 10 | import CodeMirror from './codemirror'; 11 | 12 | import './add.scss'; 13 | 14 | const Add = () => { 15 | const [title, setTitle] = useState(''); 16 | const [language, setLanguage] = useState(LANGUAGES[0]); 17 | const [type, setType] = useState(TYPES[0]); 18 | const [code, setCode] = useState(''); 19 | const { user } = useUser(); 20 | const { createShamecap } = useShamecaps(); 21 | 22 | const handleSubmit = event => { 23 | event.preventDefault(); 24 | 25 | const data = { 26 | id: uuid(), 27 | title, 28 | language: slugify(language, { lower: true }), 29 | type: slugify(type, { lower: true }), 30 | code: code.trim(), 31 | created: Date.now(), 32 | user: { name: user.name } 33 | }; 34 | 35 | createShamecap(data); 36 | navigate('/?language=all&type=all', { state: { created: true } }); 37 | }; 38 | 39 | return ( 40 | 41 |
42 |

It happened again, didn’t it?

43 |

You’re among friends, @{user.name}. Unburden your soul.

44 |
45 | Details 46 |
47 | 55 | setType(e.target.value)} 64 | /> 65 |
66 |
67 | setCode(c)} /> 68 | 71 | 72 |
73 | ); 74 | }; 75 | 76 | export default Add; 77 | -------------------------------------------------------------------------------- /src/components/add/add.scss: -------------------------------------------------------------------------------- 1 | .add-form { 2 | margin-top: 60px; 3 | 4 | fieldset { 5 | border: none; 6 | margin-bottom: 1rem; 7 | } 8 | 9 | .details-wrapper { 10 | border: none; 11 | display: flex; 12 | justify-content: space-between; 13 | width: 100%; 14 | } 15 | 16 | label { 17 | display: inline-block; 18 | font-size: 12px; 19 | letter-spacing: 0.1em; 20 | margin-left: 1rem; 21 | text-transform: uppercase; 22 | width: 100%; 23 | 24 | &:first-child { 25 | margin-left: 0; 26 | } 27 | } 28 | 29 | input, 30 | select { 31 | appearance: none; 32 | background: transparent; 33 | border: 1px solid #d6deeb; 34 | border-radius: 4px; 35 | box-sizing: border-box; 36 | display: block; 37 | font-size: 16px; 38 | height: 36px; 39 | margin: 0; 40 | padding: 0 10px; 41 | width: 100%; 42 | } 43 | } 44 | 45 | .add-heading { 46 | font-size: 1.5rem; 47 | margin-bottom: 0.5rem; 48 | } 49 | 50 | .react-codemirror2 { 51 | font-weight: normal; 52 | font-family: 'Roboto Mono', 'Courier New', Courier, monospace; 53 | font-size: 14px; 54 | 55 | .CodeMirror { 56 | border-radius: 4px; 57 | } 58 | } 59 | 60 | .submit-button { 61 | align-items: center; 62 | background: #ff2c83; 63 | border: none; 64 | border-radius: 4px; 65 | color: #fff; 66 | display: flex; 67 | font-family: Montserrat; 68 | font-weight: 600; 69 | font-size: 20px; 70 | justify-content: center; 71 | margin: 1.25rem 0 0; 72 | padding: 0.5rem 1rem; 73 | } 74 | -------------------------------------------------------------------------------- /src/components/add/codemirror.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Controlled } from 'react-codemirror2'; 3 | 4 | import 'codemirror/lib/codemirror.css'; 5 | import 'codemirror/theme/material.css'; 6 | import 'codemirror/theme/neat.css'; 7 | import 'codemirror/mode/xml/xml'; 8 | import 'codemirror/mode/javascript/javascript'; 9 | 10 | export default ({ onChange }) => { 11 | const [value, setValue] = useState(''); 12 | return ( 13 | { 21 | setValue(val); 22 | }} 23 | onChange={() => onChange(value)} 24 | /> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/app/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import Header from './header'; 4 | import Router from './router'; 5 | import { AnalyticsProvider } from '../../context/analytics'; 6 | import { ShamecapsProvider } from '../../context/shamecaps'; 7 | import { UserProvider } from '../../context/user'; 8 | 9 | import './app.scss'; 10 | 11 | const App = () => ( 12 | 13 | 14 | 15 | 16 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /src/components/app/app.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Montserrat:400,600,900'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | font-family: Montserrat, -apple-system, BlinkMacSystemFont, 'Segoe UI', 10 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 11 | 'Segoe UI Symbol'; 12 | margin: 0; 13 | } 14 | 15 | .header { 16 | height: 80px; 17 | background: #011627; 18 | display: flex; 19 | align-items: center; 20 | 21 | .header-wrapper { 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | width: 1100px; 26 | max-width: 80%; 27 | margin: auto; 28 | } 29 | 30 | .user-avatar { 31 | border: 2px solid #82aaff; 32 | box-sizing: border-box; 33 | border-radius: 50%; 34 | margin: 0; 35 | } 36 | 37 | nav { 38 | display: flex; 39 | align-items: center; 40 | 41 | .create-account-button { 42 | width: 230px; 43 | height: 50px; 44 | background: #ff2c83; 45 | border: none; 46 | border-radius: 4px; 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | 51 | font-family: Montserrat; 52 | font-weight: 600; 53 | font-size: 20px; 54 | text-decoration: none; 55 | 56 | color: #ffffff; 57 | margin-right: 15px; 58 | } 59 | 60 | .login-button, 61 | .logout-button { 62 | background: none; 63 | border: none; 64 | font-family: Montserrat; 65 | font-weight: 600; 66 | font-size: 20px; 67 | margin-left: 15px; 68 | text-align: center; 69 | text-decoration-line: underline; 70 | 71 | color: #ff2c83; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/app/header.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link } from '@reach/router'; 3 | import { useUser } from '../../context/user'; 4 | import Logo from '../images/logo.svg'; 5 | 6 | const Header = () => { 7 | const { user, logout } = useUser(); 8 | const authenticated = !!user.name; 9 | 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 | 17 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Header; 53 | -------------------------------------------------------------------------------- /src/components/app/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router } from '@reach/router'; 3 | 4 | import Home from '../../pages/home'; 5 | import Add from '../../pages/add'; 6 | import Login from '../../pages/login'; 7 | import User from '../../pages/user'; 8 | import Dashboard from '../../pages/dashboard'; 9 | 10 | const AppRouter = () => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default AppRouter; 21 | -------------------------------------------------------------------------------- /src/components/dashboard/chart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RadarChart, CircularGridLines } from 'react-vis'; 3 | 4 | import 'react-vis/dist/style.css'; 5 | 6 | const Chart = ({ metrics }) => { 7 | const chartProps = metrics.reduce( 8 | (props, metric) => ({ 9 | data: [{ ...props.data[0], [metric.key]: metric.value }], 10 | domains: [ 11 | ...props.domains, 12 | { 13 | name: metric.name, 14 | domain: metric.domain, 15 | getValue: d => d[metric.key] 16 | } 17 | ] 18 | }), 19 | { data: [], domains: [] } 20 | ); 21 | 22 | return ( 23 | ''} 45 | width={600} 46 | height={600} 47 | > 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default Chart; 54 | -------------------------------------------------------------------------------- /src/components/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useUser } from '../../context/user'; 3 | import { useShamecaps } from '../../context/shamecaps'; 4 | import Layout from '../layout/layout'; 5 | import Loading from '../loading/loading'; 6 | import Shamelist from '../shamelist/shamelist'; 7 | import { useAnalytics } from '../../context/analytics'; 8 | import Chart from './chart'; 9 | 10 | import './dashboard.scss'; 11 | 12 | const Dashboard = () => { 13 | const { loading, metrics } = useAnalytics(); 14 | const { user } = useUser(); 15 | const { 16 | shamecaps, 17 | limit, 18 | loadMoreShamecaps, 19 | deleteShamecap, 20 | totalCount 21 | } = useShamecaps({ 22 | user: user.name 23 | }); 24 | 25 | return loading ? ( 26 | 27 | ) : ( 28 | <> 29 | 30 |

Your Account

31 |

Here’s how people feel about your shamecaps:

32 |
33 |
34 | 35 |
36 | 37 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default Dashboard; 51 | -------------------------------------------------------------------------------- /src/components/dashboard/dashboard.scss: -------------------------------------------------------------------------------- 1 | @import '~react-vis/dist/style'; 2 | 3 | .dashboard-heading { 4 | font-size: 1.5rem; 5 | margin-bottom: 0.5rem; 6 | margin-top: 60px; 7 | } 8 | 9 | .chart { 10 | align-items: center; 11 | background: #d6deeb40; 12 | border-bottom: 1px solid #d6deeb; 13 | border-top: 1px solid #d6deeb; 14 | display: flex; 15 | height: 300px; 16 | justify-content: center; 17 | width: 100%; 18 | 19 | .rv-radar-chart { 20 | transform: scale(0.5); 21 | } 22 | 23 | @media (min-width: 600px) { 24 | height: 600px; 25 | 26 | .rv-radar-chart { 27 | transform: scale(1); 28 | } 29 | } 30 | } 31 | 32 | .rv-xy-plot__axis__line { 33 | stroke: #637777; 34 | stroke-width: 1; 35 | stroke-opacity: 0.5; 36 | } 37 | 38 | .rv-xy-manipulable-axis__ticks { 39 | opacity: 0; 40 | } 41 | 42 | .rv-xy-plot__circular-grid-lines__line { 43 | stroke: #637777; 44 | stroke-opacity: 0.5; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/home/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useShamecaps } from '../../context/shamecaps'; 3 | import Layout from '../layout/layout'; 4 | import Loading from '../loading/loading'; 5 | import Shamelist from '../shamelist/shamelist'; 6 | 7 | import './home.scss'; 8 | 9 | const Home = ({ location }) => { 10 | const { 11 | loading, 12 | shamecaps, 13 | limit, 14 | loadMoreShamecaps, 15 | totalCount, 16 | } = useShamecaps(); 17 | 18 | if (loading) { 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | return ( 27 | <> 28 |
YOU KNOW WHAT YOU DID
29 | 30 | {location.state && location.state.created && ( 31 |
32 |

33 | Sweet catharsis! Your shame has been released 34 | like a bird into the night. 35 |

36 |
37 | )} 38 | 45 |
46 | 47 | ); 48 | }; 49 | 50 | export default Home; 51 | -------------------------------------------------------------------------------- /src/components/home/home.scss: -------------------------------------------------------------------------------- 1 | .banner { 2 | background-image: url('../images/header.png'); 3 | background-size: cover; 4 | height: 300px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | font-family: Montserrat; 9 | font-weight: 600; 10 | font-size: 60px; 11 | line-height: 80px; 12 | text-align: center; 13 | 14 | color: #ffffff; 15 | 16 | text-shadow: 0px 2px 2px #637777; 17 | } 18 | 19 | .new-shamecap-notice { 20 | background: #addb6740; 21 | border: 1px solid #addb67; 22 | border-radius: 5px; 23 | color: #011627; 24 | font-size: 0.875rem; 25 | margin: 2rem 0 0; 26 | padding: 1rem; 27 | text-align: center; 28 | 29 | p { 30 | margin: 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/images/carrot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/images/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlengstorf/advanced-react-perf/165ce5bf6a0078241ca76bd7c3e6e880ce91c369/src/components/images/header.jpg -------------------------------------------------------------------------------- /src/components/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlengstorf/advanced-react-perf/165ce5bf6a0078241ca76bd7c3e6e880ce91c369/src/components/images/header.png -------------------------------------------------------------------------------- /src/components/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/images/shame-box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/layout/fonts/roboto-mono-v6-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlengstorf/advanced-react-perf/165ce5bf6a0078241ca76bd7c3e6e880ce91c369/src/components/layout/fonts/roboto-mono-v6-latin-italic.woff -------------------------------------------------------------------------------- /src/components/layout/fonts/roboto-mono-v6-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlengstorf/advanced-react-perf/165ce5bf6a0078241ca76bd7c3e6e880ce91c369/src/components/layout/fonts/roboto-mono-v6-latin-italic.woff2 -------------------------------------------------------------------------------- /src/components/layout/fonts/roboto-mono-v6-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlengstorf/advanced-react-perf/165ce5bf6a0078241ca76bd7c3e6e880ce91c369/src/components/layout/fonts/roboto-mono-v6-latin-regular.woff -------------------------------------------------------------------------------- /src/components/layout/fonts/roboto-mono-v6-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlengstorf/advanced-react-perf/165ce5bf6a0078241ca76bd7c3e6e880ce91c369/src/components/layout/fonts/roboto-mono-v6-latin-regular.woff2 -------------------------------------------------------------------------------- /src/components/layout/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './layout.scss'; 4 | 5 | const Layout = ({ children }) => ( 6 |
15 | {children} 16 |
17 | ); 18 | 19 | export default Layout; 20 | -------------------------------------------------------------------------------- /src/components/layout/layout.scss: -------------------------------------------------------------------------------- 1 | @import './mono-font.scss'; 2 | 3 | html { 4 | -ms-text-size-adjust: 100%; 5 | -webkit-text-size-adjust: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | font-family: Montserrat; 11 | font-style: normal; 12 | font-weight: 400; 13 | line-height: 1.45; 14 | font-size: 18px; 15 | color: #637777; 16 | word-wrap: break-word; 17 | font-kerning: normal; 18 | -moz-font-feature-settings: 'kern', 'liga', 'clig', 'calt'; 19 | -ms-font-feature-settings: 'kern', 'liga', 'clig', 'calt'; 20 | -webkit-font-feature-settings: 'kern', 'liga', 'clig', 'calt'; 21 | font-feature-settings: 'kern', 'liga', 'clig', 'calt'; 22 | } 23 | 24 | article, 25 | aside, 26 | details, 27 | figcaption, 28 | figure, 29 | footer, 30 | header, 31 | main, 32 | menu, 33 | nav, 34 | section, 35 | summary { 36 | display: block; 37 | } 38 | 39 | audio, 40 | canvas, 41 | progress, 42 | video { 43 | display: inline-block; 44 | } 45 | 46 | audio:not([controls]) { 47 | display: none; 48 | height: 0; 49 | } 50 | 51 | progress { 52 | vertical-align: baseline; 53 | } 54 | 55 | [hidden], 56 | template { 57 | display: none; 58 | } 59 | 60 | a { 61 | background-color: transparent; 62 | -webkit-text-decoration-skip: objects; 63 | } 64 | 65 | a:active, 66 | a:hover { 67 | outline-width: 0; 68 | } 69 | 70 | abbr[title] { 71 | border-bottom: none; 72 | text-decoration: underline; 73 | text-decoration: underline dotted; 74 | } 75 | 76 | b, 77 | strong { 78 | font-weight: inherit; 79 | font-weight: bolder; 80 | } 81 | 82 | dfn { 83 | font-style: italic; 84 | } 85 | 86 | h1 { 87 | font-size: 2em; 88 | margin: 0.67em 0; 89 | } 90 | 91 | mark { 92 | background-color: #ff0; 93 | color: #000; 94 | } 95 | 96 | small { 97 | font-size: 80%; 98 | } 99 | 100 | sub, 101 | sup { 102 | font-size: 75%; 103 | line-height: 0; 104 | position: relative; 105 | vertical-align: baseline; 106 | } 107 | 108 | sub { 109 | bottom: -0.25em; 110 | } 111 | 112 | sup { 113 | top: -0.5em; 114 | } 115 | 116 | img { 117 | border-style: none; 118 | } 119 | 120 | svg:not(:root) { 121 | overflow: hidden; 122 | } 123 | 124 | figure { 125 | margin: 1em 40px; 126 | } 127 | 128 | hr { 129 | box-sizing: content-box; 130 | height: 0; 131 | overflow: visible; 132 | } 133 | 134 | button, 135 | input, 136 | optgroup, 137 | select, 138 | textarea { 139 | font: inherit; 140 | margin: 0; 141 | } 142 | 143 | optgroup { 144 | font-weight: 700; 145 | } 146 | 147 | button, 148 | input { 149 | overflow: visible; 150 | } 151 | 152 | button, 153 | select { 154 | text-transform: none; 155 | } 156 | 157 | [type='reset'], 158 | [type='submit'], 159 | button, 160 | html [type='button'] { 161 | -webkit-appearance: button; 162 | } 163 | 164 | [type='button']::-moz-focus-inner, 165 | [type='reset']::-moz-focus-inner, 166 | [type='submit']::-moz-focus-inner, 167 | button::-moz-focus-inner { 168 | border-style: none; 169 | padding: 0; 170 | } 171 | 172 | [type='button']:-moz-focusring, 173 | [type='reset']:-moz-focusring, 174 | [type='submit']:-moz-focusring, 175 | button:-moz-focusring { 176 | outline: 1px dotted ButtonText; 177 | } 178 | 179 | fieldset { 180 | border: 1px solid silver; 181 | margin: 0 2px; 182 | padding: 0.35em 0.625em 0.75em; 183 | } 184 | 185 | legend { 186 | box-sizing: border-box; 187 | color: inherit; 188 | display: table; 189 | max-width: 100%; 190 | padding: 0; 191 | white-space: normal; 192 | } 193 | 194 | textarea { 195 | overflow: auto; 196 | } 197 | 198 | [type='checkbox'], 199 | [type='radio'] { 200 | box-sizing: border-box; 201 | padding: 0; 202 | } 203 | 204 | [type='number']::-webkit-inner-spin-button, 205 | [type='number']::-webkit-outer-spin-button { 206 | height: auto; 207 | } 208 | 209 | [type='search'] { 210 | -webkit-appearance: textfield; 211 | outline-offset: -2px; 212 | } 213 | 214 | [type='search']::-webkit-search-cancel-button, 215 | [type='search']::-webkit-search-decoration { 216 | -webkit-appearance: none; 217 | } 218 | 219 | ::-webkit-input-placeholder { 220 | color: inherit; 221 | opacity: 0.54; 222 | } 223 | 224 | ::-webkit-file-upload-button { 225 | -webkit-appearance: button; 226 | font: inherit; 227 | } 228 | 229 | html { 230 | font: 112.5%/1.45em georgia, serif; 231 | box-sizing: border-box; 232 | overflow-y: scroll; 233 | } 234 | 235 | * { 236 | box-sizing: inherit; 237 | } 238 | 239 | *:before { 240 | box-sizing: inherit; 241 | } 242 | 243 | *:after { 244 | box-sizing: inherit; 245 | } 246 | 247 | img { 248 | max-width: 100%; 249 | margin-left: 0; 250 | margin-right: 0; 251 | margin-top: 0; 252 | padding-bottom: 0; 253 | padding-left: 0; 254 | padding-right: 0; 255 | padding-top: 0; 256 | margin-bottom: 1.45rem; 257 | } 258 | 259 | h1, 260 | h2, 261 | h3, 262 | h4, 263 | h5, 264 | h6 { 265 | color: #011627; 266 | font-weight: 900; 267 | line-height: 1.1; 268 | margin-bottom: 1.45rem; 269 | margin-left: 0; 270 | margin-right: 0; 271 | margin-top: 0; 272 | padding-bottom: 0; 273 | padding-left: 0; 274 | padding-right: 0; 275 | padding-top: 0; 276 | text-rendering: optimizeLegibility; 277 | } 278 | 279 | h1 { 280 | font-size: 2.25rem; 281 | } 282 | 283 | h2 { 284 | font-size: 1.62671rem; 285 | } 286 | 287 | h3 { 288 | font-size: 1.38316rem; 289 | } 290 | 291 | h4 { 292 | font-size: 1rem; 293 | } 294 | 295 | h5 { 296 | font-size: 0.85028rem; 297 | } 298 | 299 | h6 { 300 | font-size: 0.78405rem; 301 | } 302 | 303 | hgroup { 304 | margin-left: 0; 305 | margin-right: 0; 306 | margin-top: 0; 307 | padding-bottom: 0; 308 | padding-left: 0; 309 | padding-right: 0; 310 | padding-top: 0; 311 | margin-bottom: 1.45rem; 312 | } 313 | 314 | ul { 315 | margin-left: 1.45rem; 316 | margin-right: 0; 317 | margin-top: 0; 318 | padding-bottom: 0; 319 | padding-left: 0; 320 | padding-right: 0; 321 | padding-top: 0; 322 | margin-bottom: 1.45rem; 323 | list-style-position: outside; 324 | list-style-image: none; 325 | } 326 | 327 | ol { 328 | margin-left: 1.45rem; 329 | margin-right: 0; 330 | margin-top: 0; 331 | padding-bottom: 0; 332 | padding-left: 0; 333 | padding-right: 0; 334 | padding-top: 0; 335 | margin-bottom: 1.45rem; 336 | list-style-position: outside; 337 | list-style-image: none; 338 | } 339 | 340 | dl { 341 | margin-left: 0; 342 | margin-right: 0; 343 | margin-top: 0; 344 | padding-bottom: 0; 345 | padding-left: 0; 346 | padding-right: 0; 347 | padding-top: 0; 348 | margin-bottom: 1.45rem; 349 | } 350 | 351 | dd { 352 | margin-left: 0; 353 | margin-right: 0; 354 | margin-top: 0; 355 | padding-bottom: 0; 356 | padding-left: 0; 357 | padding-right: 0; 358 | padding-top: 0; 359 | margin-bottom: 1.45rem; 360 | } 361 | 362 | p { 363 | margin-left: 0; 364 | margin-right: 0; 365 | margin-top: 0; 366 | padding-bottom: 0; 367 | padding-left: 0; 368 | padding-right: 0; 369 | padding-top: 0; 370 | margin-bottom: 1.45rem; 371 | } 372 | 373 | figure { 374 | margin-left: 0; 375 | margin-right: 0; 376 | margin-top: 0; 377 | padding-bottom: 0; 378 | padding-left: 0; 379 | padding-right: 0; 380 | padding-top: 0; 381 | margin-bottom: 1.45rem; 382 | } 383 | 384 | table { 385 | margin-left: 0; 386 | margin-right: 0; 387 | margin-top: 0; 388 | padding-bottom: 0; 389 | padding-left: 0; 390 | padding-right: 0; 391 | padding-top: 0; 392 | margin-bottom: 1.45rem; 393 | font-size: 1rem; 394 | line-height: 1.45rem; 395 | border-collapse: collapse; 396 | width: 100%; 397 | } 398 | 399 | fieldset { 400 | margin-left: 0; 401 | margin-right: 0; 402 | margin-top: 0; 403 | padding-bottom: 0; 404 | padding-left: 0; 405 | padding-right: 0; 406 | padding-top: 0; 407 | margin-bottom: 1.45rem; 408 | } 409 | 410 | blockquote { 411 | margin-left: 1.45rem; 412 | margin-right: 1.45rem; 413 | margin-top: 0; 414 | padding-bottom: 0; 415 | padding-left: 0; 416 | padding-right: 0; 417 | padding-top: 0; 418 | margin-bottom: 1.45rem; 419 | } 420 | 421 | form { 422 | margin-left: 0; 423 | margin-right: 0; 424 | margin-top: 0; 425 | padding-bottom: 0; 426 | padding-left: 0; 427 | padding-right: 0; 428 | padding-top: 0; 429 | margin-bottom: 1.45rem; 430 | } 431 | 432 | noscript { 433 | margin-left: 0; 434 | margin-right: 0; 435 | margin-top: 0; 436 | padding-bottom: 0; 437 | padding-left: 0; 438 | padding-right: 0; 439 | padding-top: 0; 440 | margin-bottom: 1.45rem; 441 | } 442 | 443 | iframe { 444 | margin-left: 0; 445 | margin-right: 0; 446 | margin-top: 0; 447 | padding-bottom: 0; 448 | padding-left: 0; 449 | padding-right: 0; 450 | padding-top: 0; 451 | margin-bottom: 1.45rem; 452 | } 453 | 454 | hr { 455 | margin-left: 0; 456 | margin-right: 0; 457 | margin-top: 0; 458 | padding-bottom: 0; 459 | padding-left: 0; 460 | padding-right: 0; 461 | padding-top: 0; 462 | margin-bottom: calc(1.45rem - 1px); 463 | background: hsla(0, 0%, 0%, 0.2); 464 | border: none; 465 | height: 1px; 466 | } 467 | 468 | address { 469 | margin-left: 0; 470 | margin-right: 0; 471 | margin-top: 0; 472 | padding-bottom: 0; 473 | padding-left: 0; 474 | padding-right: 0; 475 | padding-top: 0; 476 | margin-bottom: 1.45rem; 477 | } 478 | 479 | b { 480 | font-weight: bold; 481 | } 482 | 483 | strong { 484 | font-weight: bold; 485 | } 486 | 487 | dt { 488 | font-weight: bold; 489 | } 490 | 491 | th { 492 | font-weight: bold; 493 | } 494 | 495 | li { 496 | margin-bottom: calc(1.45rem / 2); 497 | } 498 | 499 | ol li { 500 | padding-left: 0; 501 | } 502 | 503 | ul li { 504 | padding-left: 0; 505 | } 506 | 507 | li > ol { 508 | margin-left: 1.45rem; 509 | margin-bottom: calc(1.45rem / 2); 510 | margin-top: calc(1.45rem / 2); 511 | } 512 | 513 | li > ul { 514 | margin-left: 1.45rem; 515 | margin-bottom: calc(1.45rem / 2); 516 | margin-top: calc(1.45rem / 2); 517 | } 518 | 519 | blockquote *:last-child { 520 | margin-bottom: 0; 521 | } 522 | 523 | li *:last-child { 524 | margin-bottom: 0; 525 | } 526 | 527 | p *:last-child { 528 | margin-bottom: 0; 529 | } 530 | 531 | li > p { 532 | margin-bottom: calc(1.45rem / 2); 533 | } 534 | 535 | code { 536 | font-size: 0.85rem; 537 | line-height: 1.45rem; 538 | } 539 | 540 | kbd { 541 | font-size: 0.85rem; 542 | line-height: 1.45rem; 543 | } 544 | 545 | samp { 546 | font-size: 0.85rem; 547 | line-height: 1.45rem; 548 | } 549 | 550 | abbr { 551 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 552 | cursor: help; 553 | } 554 | 555 | acronym { 556 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 557 | cursor: help; 558 | } 559 | 560 | abbr[title] { 561 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 562 | cursor: help; 563 | text-decoration: none; 564 | } 565 | 566 | thead { 567 | text-align: left; 568 | } 569 | 570 | td, 571 | th { 572 | text-align: left; 573 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12); 574 | font-feature-settings: 'tnum'; 575 | -moz-font-feature-settings: 'tnum'; 576 | -ms-font-feature-settings: 'tnum'; 577 | -webkit-font-feature-settings: 'tnum'; 578 | padding-left: 0.96667rem; 579 | padding-right: 0.96667rem; 580 | padding-top: 0.725rem; 581 | padding-bottom: calc(0.725rem - 1px); 582 | } 583 | 584 | th:first-child, 585 | td:first-child { 586 | padding-left: 0; 587 | } 588 | 589 | th:last-child, 590 | td:last-child { 591 | padding-right: 0; 592 | } 593 | 594 | tt, 595 | code, 596 | .prism-code { 597 | font-family: 'Roboto Mono', 'Courier New', Courier, monospace; 598 | } 599 | 600 | @media only screen and (max-width: 480px) { 601 | html { 602 | font-size: 100%; 603 | } 604 | } 605 | 606 | .select-wrapper { 607 | position: relative; 608 | display: inline-block; 609 | select { 610 | padding: 0 10px; 611 | height: 36px; 612 | background: transparent; 613 | margin-left: 10px; 614 | appearance: none; 615 | 616 | border: 1px solid #d6deeb; 617 | box-sizing: border-box; 618 | padding-right: 40px; 619 | } 620 | 621 | &:after { 622 | content: ''; 623 | width: 14px; 624 | height: 9px; 625 | background-image: url('../images/carrot.svg'); 626 | display: block; 627 | position: absolute; 628 | top: 50%; 629 | transform: translateY(-50%); 630 | right: 12px; 631 | } 632 | } 633 | 634 | .screen-reader-text { 635 | position: absolute; 636 | width: 1px; 637 | height: 1px; 638 | padding: 0; 639 | overflow: hidden; 640 | -webkit-clip: rect(0, 0, 0, 0); 641 | clip: rect(0, 0, 0, 0); 642 | white-space: nowrap; 643 | border: 0; 644 | } 645 | -------------------------------------------------------------------------------- /src/components/layout/mono-font.scss: -------------------------------------------------------------------------------- 1 | /* roboto-mono-regular - latin */ 2 | @font-face { 3 | font-family: 'Roboto Mono'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: local('Roboto Mono'), local('RobotoMono-Regular'), 7 | url('./fonts/roboto-mono-v6-latin-regular.woff2') format('woff2'), 8 | /* Chrome 26+, Opera 23+, Firefox 39+ */ 9 | url('./fonts/roboto-mono-v6-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ 10 | } 11 | /* roboto-mono-italic - latin */ 12 | @font-face { 13 | font-family: 'Roboto Mono'; 14 | font-style: italic; 15 | font-weight: 400; 16 | src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), 17 | url('./fonts/roboto-mono-v6-latin-italic.woff2') format('woff2'), 18 | /* Chrome 26+, Opera 23+, Firefox 39+ */ 19 | url('./fonts/roboto-mono-v6-latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ 20 | } 21 | -------------------------------------------------------------------------------- /src/components/loading/loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './loading.scss'; 4 | 5 | const Loading = () => ( 6 |
7 |

Loading...

8 |
9 | ); 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /src/components/loading/loading.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Loader from https://projects.lukehaas.me/css-loaders/ 3 | */ 4 | .loading, 5 | .loading:before, 6 | .loading:after { 7 | background: #c792ea; 8 | -webkit-animation: load1 1s infinite ease-in-out; 9 | animation: load1 1s infinite ease-in-out; 10 | width: 1em; 11 | height: 4em; 12 | } 13 | .loading { 14 | color: #c792ea; 15 | text-indent: -9999em; 16 | margin: 88px auto; 17 | position: relative; 18 | font-size: 11px; 19 | -webkit-transform: translateZ(0); 20 | -ms-transform: translateZ(0); 21 | transform: translateZ(0); 22 | -webkit-animation-delay: -0.16s; 23 | animation-delay: -0.16s; 24 | } 25 | .loading:before, 26 | .loading:after { 27 | position: absolute; 28 | top: 0; 29 | content: ''; 30 | } 31 | .loading:before { 32 | left: -1.5em; 33 | -webkit-animation-delay: -0.32s; 34 | animation-delay: -0.32s; 35 | } 36 | .loading:after { 37 | left: 1.5em; 38 | } 39 | @-webkit-keyframes load1 { 40 | 0%, 41 | 80%, 42 | 100% { 43 | box-shadow: 0 0; 44 | height: 4em; 45 | } 46 | 40% { 47 | box-shadow: 0 -2em; 48 | height: 5em; 49 | } 50 | } 51 | @keyframes load1 { 52 | 0%, 53 | 80%, 54 | 100% { 55 | box-shadow: 0 0; 56 | height: 4em; 57 | } 58 | 40% { 59 | box-shadow: 0 -2em; 60 | height: 5em; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/login/login.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Layout from '../layout/layout'; 3 | import { useUser } from '../../context/user'; 4 | import ShameBox from '../images/shame-box.svg'; 5 | 6 | import './login.scss'; 7 | import { navigate } from '@reach/router'; 8 | 9 | const Login = () => { 10 | const { login } = useUser(); 11 | 12 | return ( 13 | 14 |
15 | 16 |
17 |

Log in. Share the shame.

18 |

19 | We’ve all been there. We take a shortcut, hit a weird edge case, or 20 | run into some code that’s just not going to cooperate, and then it 21 | happens: we ask our principles to avert their eyes and write some 22 | truly appalling code. 23 |

24 |

25 | To make the shame easier to bear, we’ve created a safe space where 26 | we can all post the hot garbage we write from time to time in the 27 | name of getting shit done. 28 |

29 |

Sign in. Share your shame. Unburden your soul.

30 | 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Login; 50 | -------------------------------------------------------------------------------- /src/components/login/login.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | display: block; 3 | @supports (display: grid) { 4 | @media (min-width: 1024px) { 5 | align-items: center; 6 | display: grid; 7 | grid-template-columns: 1fr 1fr; 8 | grid-column-gap: 4rem; 9 | margin-top: 100px; 10 | } 11 | } 12 | } 13 | 14 | .shame-box-illustration { 15 | display: block; 16 | height: auto; 17 | max-width: 250px; 18 | margin: 60px auto 40px; 19 | width: 500px; 20 | 21 | @supports (display: grid) { 22 | @media (min-width: 1024px) { 23 | margin-bottom: 0; 24 | margin-top: 0; 25 | max-width: 100%; 26 | } 27 | } 28 | } 29 | 30 | .login-box { 31 | background: #fff; 32 | border: 1px solid #82aaff; 33 | border-radius: 0.25rem; 34 | padding: 1.5rem; 35 | 36 | /* override some global/Firebase styles */ 37 | form, 38 | ul, 39 | li.firebaseui-list-item { 40 | margin-bottom: 0; 41 | } 42 | } 43 | 44 | .login-heading { 45 | font-size: 1.625rem; 46 | } 47 | 48 | .login-big-button { 49 | width: 230px; 50 | height: 50px; 51 | background: #ff2c83; 52 | border: none; 53 | border-radius: 4px; 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | 58 | font-family: Montserrat; 59 | font-weight: 600; 60 | font-size: 20px; 61 | text-decoration: none; 62 | 63 | color: #ffffff; 64 | margin-right: 15px; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/select/select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import uuid from 'uuid/v5'; 3 | import slugify from 'slugify'; 4 | 5 | const NAMESPACE = '4c3fc58e-50df-4047-bede-21002905cf53'; 6 | 7 | const Select = ({ 8 | options = [], 9 | handleChange = () => {}, 10 | defaultValue, 11 | label, 12 | hideLabel = false, 13 | unselectedOption, 14 | unselectedOptionValue 15 | }) => ( 16 | 35 | ); 36 | 37 | export default Select; 38 | -------------------------------------------------------------------------------- /src/components/shamelist/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Highlight, { defaultProps } from 'prism-react-renderer'; 3 | import theme from 'prism-react-renderer/themes/nightOwl'; 4 | 5 | const Code = ({ code, language = 'js' }) => ( 6 | 7 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 8 |
 9 |         {tokens.map((line, i) => (
10 |           
11 | {line.map((token, key) => ( 12 | 13 | ))} 14 |
15 | ))} 16 |
17 | )} 18 |
19 | ); 20 | 21 | export default Code; 22 | -------------------------------------------------------------------------------- /src/components/shamelist/controls.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Controls = () => ( 4 |
5 | 11 | 12 | 20 | 28 | 36 | 37 | 38 |
39 | ); 40 | 41 | export default Controls; 42 | -------------------------------------------------------------------------------- /src/components/shamelist/filters.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LANGUAGES, TYPES } from '../../constants'; 3 | import { useShamecaps } from '../../context/shamecaps'; 4 | import Select from '../select/select'; 5 | 6 | const Filters = ({ totalCount }) => { 7 | const { filters, setFilters } = useShamecaps(); 8 | 9 | return ( 10 | 36 | ); 37 | }; 38 | 39 | export default Filters; 40 | -------------------------------------------------------------------------------- /src/components/shamelist/shame.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from '@reach/router'; 3 | import prettier from 'prettier/standalone'; 4 | import moment from 'moment'; 5 | import Code from './code'; 6 | import Controls from './controls'; 7 | 8 | export default ({ 9 | id, 10 | language, 11 | code, 12 | user, 13 | title, 14 | showUserDetails, 15 | created, 16 | showControls, 17 | deleteShamecap 18 | }) => { 19 | const plugins = [ 20 | require('prettier/parser-graphql'), 21 | require('prettier/parser-babylon'), 22 | require('prettier/parser-markdown') 23 | ]; 24 | 25 | let prettierCode; 26 | try { 27 | prettierCode = prettier.format(code, { 28 | parser: language === 'javascript' ? 'babel' : language, 29 | plugins 30 | }); 31 | } catch { 32 | prettierCode = code; 33 | } 34 | 35 | return ( 36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 |

{title}

44 | {showUserDetails && ( 45 | 46 | Posted by{' '} 47 | 48 | @{user.name} 49 | {' '} 50 | {moment(created).fromNow()} 51 | 52 | )} 53 | {showControls && ( 54 | 55 | 58 | 59 | )} 60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/shamelist/shamelist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Filters from './filters'; 3 | import Shame from './shame'; 4 | 5 | import './shamelist.scss'; 6 | 7 | const Shamelist = ({ 8 | shamecaps, 9 | loadMoreShamecaps, 10 | showUserDetails, 11 | showControls, 12 | deleteShamecap, 13 | totalCount 14 | }) => ( 15 | <> 16 | 17 |
18 | {shamecaps.map(shamecap => ( 19 | 26 | ))} 27 |
28 | 29 | {shamecaps.length < totalCount && ( 30 | 37 | )} 38 | 39 | ); 40 | 41 | export default Shamelist; 42 | -------------------------------------------------------------------------------- /src/components/shamelist/shamelist.scss: -------------------------------------------------------------------------------- 1 | .filters { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | margin-bottom: 40px; 6 | margin-top: 60px; 7 | } 8 | 9 | .shamelist { 10 | display: grid; 11 | grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); 12 | grid-gap: 50px; 13 | 14 | @media screen and (max-width: 768px) { 15 | grid-template-columns: 1fr; 16 | } 17 | 18 | .title { 19 | font-weight: 900; 20 | line-height: normal; 21 | font-size: 24px; 22 | margin: 12px 0; 23 | color: #011627; 24 | } 25 | 26 | .details { 27 | font-style: normal; 28 | font-weight: normal; 29 | line-height: normal; 30 | font-size: 16px; 31 | 32 | color: #637777; 33 | 34 | a { 35 | color: #637777; 36 | } 37 | } 38 | } 39 | 40 | .shame { 41 | background: #663399; 42 | padding: 20px; 43 | } 44 | 45 | .terminal { 46 | color: rgb(171, 178, 191); 47 | background-color: rgb(1, 22, 39); 48 | min-width: inherit; 49 | position: relative; 50 | z-index: 1; 51 | box-shadow: rgba(0, 0, 0, 0.55) 0px 20px 68px; 52 | border-radius: 5px; 53 | padding: 20px; 54 | 55 | .controls { 56 | position: relative; 57 | margin-bottom: 10px; 58 | } 59 | 60 | .prism-code { 61 | white-space: pre-wrap; 62 | font-size: 14px; 63 | } 64 | } 65 | 66 | .load-more-button, 67 | .delete-button { 68 | align-items: center; 69 | background: #ff2c83; 70 | border: none; 71 | border-radius: 4px; 72 | color: #fff; 73 | display: flex; 74 | font-family: Montserrat; 75 | font-weight: 600; 76 | justify-content: center; 77 | margin-top: 0.5rem; 78 | padding: 0.25rem 1rem; 79 | } 80 | 81 | .load-more-button { 82 | font-size: 20px; 83 | margin: 40px auto 0; 84 | padding: 0.5rem 1rem; 85 | } 86 | -------------------------------------------------------------------------------- /src/components/user/user.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from '@reach/router'; 3 | import { useShamecaps } from '../../context/shamecaps'; 4 | import Layout from '../layout/layout'; 5 | import Shamelist from '../shamelist/shamelist'; 6 | 7 | import './user.scss'; 8 | 9 | const User = ({ username }) => { 10 | const { shamecaps, limit, loadMoreShamecaps, totalCount } = useShamecaps({ 11 | user: username 12 | }); 13 | 14 | return ( 15 | 16 |
17 |

Showing shamecaps for @{username}

18 | ← back to all shamecaps 19 |
20 | 26 |
27 | ); 28 | }; 29 | 30 | export default User; 31 | -------------------------------------------------------------------------------- /src/components/user/user.scss: -------------------------------------------------------------------------------- 1 | .user-page-header { 2 | margin-top: 60px; 3 | } 4 | 5 | .user-heading { 6 | font-size: 1.5rem; 7 | margin-bottom: 0.5rem; 8 | } 9 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const LANGUAGES = ['JavaScript', 'GraphQL', 'HTML', 'CSS']; 2 | 3 | export const TYPES = ['Hacks & Shenanigans', 'I Give Up', 'I Had To']; 4 | -------------------------------------------------------------------------------- /src/context/analytics.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Because we don’t want to require everyone to set up new accounts, API keys, 3 | * or be limited by flaky wifi, we’re going to fake our data management. 4 | * 5 | * This pulls fake analytics and simulates request latency. 6 | */ 7 | import React, { createContext, useContext, useEffect, useReducer } from 'react'; 8 | 9 | const getRandomValue = () => Math.ceil(Math.random() * 8 + 2); 10 | 11 | const types = { 12 | load: 'ANALYTICS_LOAD' 13 | }; 14 | 15 | const initialState = { 16 | loading: true, 17 | metrics: [] 18 | }; 19 | 20 | const reducer = (state, action) => { 21 | switch (action.type) { 22 | case types.load: 23 | return { 24 | ...state, 25 | loading: false, 26 | metrics: action.metrics 27 | }; 28 | 29 | default: 30 | console.error(`Unknown action type ${action.type}`); 31 | return state; 32 | } 33 | }; 34 | 35 | const fetchData = () => { 36 | return new Promise(resolve => { 37 | // Simulate a slooooooooow data call. 38 | setTimeout(() => { 39 | resolve([ 40 | { key: 'cringe', name: '🙈', domain: [0, 11], value: getRandomValue() }, 41 | { key: 'fuckit', name: '🤬', domain: [0, 11], value: getRandomValue() }, 42 | { key: 'scary', name: '😱', domain: [0, 11], value: getRandomValue() }, 43 | { key: 'evil', name: '😈', domain: [0, 11], value: getRandomValue() }, 44 | { 45 | key: 'heartbreaking', 46 | name: '💔', 47 | domain: [0, 11], 48 | value: getRandomValue() 49 | }, 50 | { key: 'sad', name: '😭', domain: [0, 11], value: getRandomValue() } 51 | ]); 52 | }, 4000); 53 | }); 54 | }; 55 | 56 | export const AnalyticsContext = createContext(); 57 | 58 | export const AnalyticsProvider = ({ children }) => ( 59 | 60 | {children} 61 | 62 | ); 63 | 64 | export const useAnalytics = () => { 65 | const [state, dispatch] = useContext(AnalyticsContext); 66 | 67 | useEffect(() => { 68 | async function getData() { 69 | const data = await fetchData(); 70 | 71 | dispatch({ type: types.load, metrics: data }); 72 | } 73 | 74 | getData(); 75 | }, []); 76 | 77 | return state; 78 | }; 79 | -------------------------------------------------------------------------------- /src/context/shamecaps.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Because we don’t want to require everyone to set up new accounts, API keys, 3 | * or be limited by flaky wifi, we’re going to fake our data management. 4 | * 5 | * This stores shamecaps in localStorage and simulates request latency. 6 | */ 7 | import React, { createContext, useEffect, useReducer, useContext } from 'react'; 8 | import { navigate } from '@reach/router'; 9 | 10 | const LIMIT = 10; 11 | 12 | const types = { 13 | create: 'SHAMECAP_CREATE', 14 | delete: 'SHAMECAP_DELETE', 15 | load: 'SHAMECAP_LOAD', 16 | filter: 'SHAMECAP_FILTER', 17 | loadMoreShamecaps: 'SHAMECAP_UPDATE_LIMIT' 18 | }; 19 | 20 | // Support query string params for filters. 21 | const queryString = new URL(document.location).searchParams; 22 | 23 | const localShame = window.localStorage.getItem('shamecaps'); 24 | const shamecaps = 25 | localShame && localShame.length 26 | ? JSON.parse(localShame) 27 | : [ 28 | { 29 | id: 1, 30 | code: `const superhacks = 'this is terrible code';`, 31 | user: { name: 'SaraVieira' }, 32 | title: 'Superhacks', 33 | created: 1552860977820, 34 | language: 'javascript' 35 | }, 36 | { 37 | id: 2, 38 | code: `// oh my god I’m so sorry`, 39 | user: { name: 'jlengstorf' }, 40 | title: 'I’m so sorry', 41 | created: 1552860957820, 42 | language: 'javascript' 43 | } 44 | ]; 45 | 46 | const fetchShamecaps = limit => { 47 | const getData = () => shamecaps.slice(0, limit); 48 | 49 | return new Promise(resolve => { 50 | // Simulate network latency. 51 | setTimeout(() => { 52 | const shamecaps = getData(); 53 | resolve(shamecaps); 54 | }, 850); 55 | }); 56 | }; 57 | 58 | // Load from localStorage if items are set. Otherwise seed with a couple entries. 59 | const initialState = { 60 | loading: true, 61 | limit: LIMIT, 62 | shamecaps: [], 63 | filters: { 64 | language: queryString.get('language') || 'all', 65 | type: queryString.get('type') || 'all' 66 | } 67 | }; 68 | 69 | const reducer = (state, action) => { 70 | switch (action.type) { 71 | case types.load: 72 | return { 73 | ...state, 74 | loading: false, 75 | shamecaps: action.shamecaps 76 | }; 77 | 78 | case types.loadMoreShamecaps: 79 | return { 80 | ...state, 81 | limit: state.limit + LIMIT 82 | }; 83 | 84 | case types.create: 85 | const nextShamecaps = [...state.shamecaps, action.shamecap]; 86 | window.localStorage.setItem('shamecaps', JSON.stringify(nextShamecaps)); 87 | return { 88 | ...state, 89 | shamecaps: nextShamecaps 90 | }; 91 | 92 | case types.delete: 93 | return { 94 | ...state, 95 | shamecaps: state.shamecaps.filter(s => s.id !== action.id) 96 | }; 97 | 98 | case types.filter: 99 | return { 100 | ...state, 101 | filters: { ...state.filters, ...action.filters } 102 | }; 103 | 104 | default: 105 | console.error(`Unrecognized action “${action.type}”`); 106 | return state; 107 | } 108 | }; 109 | 110 | export const ShamecapsContext = createContext(); 111 | 112 | export const ShamecapsProvider = ({ children }) => ( 113 | 114 | {children} 115 | 116 | ); 117 | 118 | export const useShamecaps = userFilters => { 119 | const [{ loading, limit, shamecaps, filters }, dispatch] = useContext( 120 | ShamecapsContext 121 | ); 122 | 123 | // Only on mount — and only if no data has loaded — load the shamecaps. 124 | useEffect(() => { 125 | if (loading) { 126 | async function getData() { 127 | const data = await fetchShamecaps(); 128 | dispatch({ type: types.load, shamecaps: data }); 129 | } 130 | 131 | getData(); 132 | } 133 | }, []); 134 | 135 | useEffect(() => { 136 | const url = new URL(document.location); 137 | if (url.pathname === '/') { 138 | const params = url.searchParams; 139 | const originalQueryString = `${params}`; 140 | Object.entries(filters).forEach(([key, val]) => { 141 | params.set(key, val); 142 | }); 143 | 144 | if (originalQueryString !== `${params}`) { 145 | navigate(`${url.pathname}?${params}`); 146 | } 147 | } 148 | }, [filters]); 149 | 150 | const createShamecap = shamecap => dispatch({ type: types.create, shamecap }); 151 | const deleteShamecap = id => dispatch({ type: types.delete, id }); 152 | const loadMoreShamecaps = () => dispatch({ type: types.loadMoreShamecaps }); 153 | const setFilters = filters => dispatch({ type: types.filter, filters }); 154 | 155 | // If user filters are applied, make sure we use them. 156 | const { language = filters.language, type = filters.type, user } = 157 | userFilters || filters; 158 | 159 | const filteredShamecaps = shamecaps 160 | // Show most recent shamecaps first 161 | .sort((a, b) => b.created - a.created) 162 | // Apply filters 163 | .filter(shamecap => 164 | [ 165 | language && language !== 'all' ? shamecap.language === language : true, 166 | type && type !== 'all' ? shamecap.type === type : true, 167 | user ? shamecap.user.name === user : true 168 | ].every(Boolean) 169 | ); 170 | 171 | return { 172 | loading, 173 | shamecaps: filteredShamecaps.slice(0, limit), 174 | totalCount: filteredShamecaps.length, 175 | filters, 176 | limit, 177 | createShamecap, 178 | deleteShamecap, 179 | loadMoreShamecaps, 180 | setFilters 181 | }; 182 | }; 183 | -------------------------------------------------------------------------------- /src/context/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Because we don’t want to require everyone to set up new accounts, API keys, 3 | * or be limited by flaky wifi, we’re going to fake our authentication. 4 | * 5 | * This stores user info in localStorage to simulate a valid session and does 6 | * nothing secure whatsoever. 7 | * 8 | * 🚨 DANGER WILL ROBINSON: This is not a valid authentication solution. 9 | * 😎 It _is_, however, a very handy way to manage state in React. 10 | * 11 | * This approach is a combination of Luke Hall’s and Kent C. Dodd’s ideas: 12 | * - https://medium.com/simply/state-management-with-react-hooks-and-context-api-at-10-lines-of-code-baf6be8302c 13 | * - https://kentcdodds.com/blog/the-state-reducer-pattern-with-react-hooks 14 | */ 15 | import React, { createContext, useEffect, useReducer, useContext } from 'react'; 16 | 17 | export const UserContext = createContext(); 18 | 19 | const types = { 20 | login: 'USER_LOGIN', 21 | logout: 'USER_LOGOUT' 22 | }; 23 | 24 | const initialState = { 25 | name: window.localStorage.getItem('name') || null, 26 | avatar: window.localStorage.getItem('avatar') || null 27 | }; 28 | 29 | const reducer = (state, action) => { 30 | switch (action.type) { 31 | case types.login: 32 | return { 33 | name: action.name, 34 | avatar: action.avatar 35 | }; 36 | 37 | case types.logout: 38 | return { 39 | name: null, 40 | avatar: null 41 | }; 42 | 43 | default: 44 | console.error(`Unknown action “${action.type}”`); 45 | return state; 46 | } 47 | }; 48 | 49 | export const UserProvider = ({ children }) => ( 50 | 51 | {children} 52 | 53 | ); 54 | 55 | export const useUser = () => { 56 | const [user, dispatch] = useContext(UserContext); 57 | useEffect(() => { 58 | if (user.name) { 59 | window.localStorage.setItem('name', user.name); 60 | window.localStorage.setItem('avatar', user.avatar); 61 | } else { 62 | window.localStorage.removeItem('name'); 63 | window.localStorage.removeItem('avatar'); 64 | } 65 | }, [user]); 66 | 67 | const login = ({ name, avatar }) => 68 | dispatch({ type: types.login, name, avatar }); 69 | const logout = () => dispatch({ type: types.logout }); 70 | 71 | return { user, login, logout }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/app/app'; 4 | 5 | ReactDOM.render(, document.querySelector('#root')); 6 | -------------------------------------------------------------------------------- /src/pages/add.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Add from '../components/add/add'; 3 | 4 | const AddPage = () => ; 5 | 6 | export default AddPage; 7 | -------------------------------------------------------------------------------- /src/pages/dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dashboard from '../components/dashboard/dashboard'; 3 | 4 | const DashboardPage = () => ; 5 | 6 | export default DashboardPage; 7 | -------------------------------------------------------------------------------- /src/pages/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import Home from '../components/home/home'; 4 | 5 | const HomePage = ({ location }) => { 6 | return ( 7 | 8 | 9 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default HomePage; 20 | -------------------------------------------------------------------------------- /src/pages/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Login from '../components/login/login'; 3 | 4 | const LoginPage = () => ; 5 | 6 | export default LoginPage; 7 | -------------------------------------------------------------------------------- /src/pages/user.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import User from '../components/user/user'; 3 | 4 | const UserPage = ({ username }) => ; 5 | 6 | export default UserPage; 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: { 7 | main: './src/index.js' 8 | }, 9 | output: { 10 | filename: '[name].[hash].js', 11 | path: path.resolve('./dist') 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | exclude: [/node_modules/], 18 | use: ['babel-loader'] 19 | }, 20 | { 21 | test: /\.s?(a|c)ss$/, 22 | use: ['style-loader', 'css-loader', 'sass-loader'] 23 | }, 24 | { 25 | test: /\.svg$/, 26 | use: [ 27 | 'babel-loader', 28 | { 29 | loader: 'react-svg-loader', 30 | options: { jsx: true } 31 | } 32 | ] 33 | }, 34 | { 35 | test: /\.(png|jpg|gif)$/, 36 | use: ['file-loader'] 37 | }, 38 | { 39 | test: /\.(woff|woff2|eot|ttf|otf)$/, 40 | use: { 41 | loader: 'file-loader', 42 | options: { 43 | name: '[name].[ext]' 44 | } 45 | } 46 | } 47 | ] 48 | }, 49 | plugins: [ 50 | new HtmlWebpackPlugin({ 51 | template: 'index.html' 52 | }), 53 | new CleanWebpackPlugin() 54 | ], 55 | devtool: 'source-map', 56 | devServer: { 57 | host: 'localhost', 58 | port: 3000, 59 | historyApiFallback: true, 60 | open: true 61 | } 62 | }; 63 | --------------------------------------------------------------------------------