├── .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 | This site only works with JavaScript enabled. :(
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 |
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 |
18 | {authenticated ? (
19 | <>
20 |
21 | Share Your Shame
22 |
23 |
24 |
31 |
32 |
33 | log out
34 |
35 | >
36 | ) : (
37 | <>
38 |
39 | Create account
40 |
41 |
42 | Login
43 |
44 | >
45 | )}
46 |
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 |
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 |
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 | {
32 | login({
33 | name: 'theshamewizard',
34 | avatar:
35 | 'https://pbs.twimg.com/profile_images/1050180826753847296/Oy__CCJ0_400x400.jpg'
36 | });
37 | navigate('/');
38 | }}
39 | className="login-big-button"
40 | >
41 | Log In
42 |
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 |
17 | {hideLabel ? {label} : label}
18 |
22 | {unselectedOption && (
23 | {unselectedOption}
24 | )}
25 | {options.map(option => (
26 |
30 | {option}
31 |
32 | ))}
33 |
34 |
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 |
11 |
12 | {totalCount} shamecap{totalCount === 1 || 's'}
13 |
14 |
15 | Filter the shame:
16 | setFilters({ language: e.target.value })}
23 | hideLabel
24 | />
25 | setFilters({ type: e.target.value })}
32 | hideLabel
33 | />
34 |
35 |
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 |
43 | {title}
44 | {showUserDetails && (
45 |
46 | Posted by{' '}
47 |
48 | @{user.name}
49 | {' '}
50 | {moment(created).fromNow()}
51 |
52 | )}
53 | {showControls && (
54 |
55 | deleteShamecap(id)} className="delete-button">
56 | delete this shamecap
57 |
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 |
35 | MOAR Shame!
36 |
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 |
--------------------------------------------------------------------------------