├── .gitignore
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── public
└── index.html
└── src
├── Application.js
├── Grudge.js
├── Grudges.js
├── NewGrudge.js
├── index.js
├── initialState.js
└── styles.css
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "none",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "fluid": false
11 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Grudges (Frontend Masters: React State)
2 |
3 | We're starting out with a basic version of an application that uses hooks to manage state.
4 |
5 | There are two issues that we'd like to solve for.
6 |
7 | - **Prop drilling**: `Grudges` needs to receive `toggleForgiveness` even though it will never use it. It's just passing it down to `Grudge`.
8 | - **Needless re-renders**: Everything re-renders even when we just check a single checkbox. We could try to get clever with some of React's performance helpers—or we can just manage our state better.
9 |
10 | ## Introducting the Application
11 |
12 | - Turn on the "Highlight updates when components render." feature in the React developer tools.
13 | - Notice how a checking a checkbox re-renderers everything.
14 | - Notice how this is not the case in `NewGrudge`.
15 |
16 | ## Using a Reducer
17 |
18 | We could try to get clever here with `useCallback` and `React.memo`, but since we're always replacing the array of grudges, this is never really going to work out.
19 |
20 | What if we took a different approach to managing state?
21 |
22 | Let's make a new file called `reducer.js`.
23 |
24 | ```js
25 | const reducer = (state = [], action) => {
26 | return state;
27 | };
28 | ```
29 |
30 | And then we swap out that `useState` with a `useReducer`.
31 |
32 | ```js
33 | const [grudges, dispatch] = useReducer(reducer, initialState);
34 | ```
35 |
36 | We're going to create an action type and an action creator.
37 |
38 | ```js
39 | const GRUDGE_ADD = 'GRUDGE_ADD';
40 | const GRUDGE_FORGIVE = 'GRUDGE_FORGIVE';
41 | ```
42 |
43 | ```js
44 | const addGrudge = ({ person, reason }) => {
45 | dispatch({
46 | type: GRUDGE_ADD,
47 | payload: {
48 | person,
49 | reason
50 | }
51 | });
52 | };
53 | ```
54 |
55 | We'll add it to the reducer.
56 |
57 | ```js
58 | const reducer = (state = [], action) => {
59 | if (action.type === GRUDGE_ADD) {
60 | return [
61 | {
62 | id: id(),
63 | ...action.payload
64 | },
65 | ...state
66 | ];
67 | }
68 | return state;
69 | };
70 | ```
71 |
72 | ### Forgiveness
73 |
74 | Let's make an action creator
75 |
76 | ```js
77 | const forgiveGrudge = id => {
78 | dispatch({
79 | type: GRUDGE_FORGIVE,
80 | payload: {
81 | id
82 | }
83 | });
84 | };
85 | ```
86 |
87 | We'll also update the reducer here.
88 |
89 | ```js
90 | if (action.type === GRUDGE_FORGIVE) {
91 | return state.map(grudge => {
92 | if (grudge.id === action.payload.id) {
93 | return { ...grudge, forgiven: !grudge.forgiven };
94 | }
95 | return grudge;
96 | });
97 | }
98 | ```
99 |
100 | We'll thread through `forgiveGrudge` as `onForgive`.
101 |
102 | ```js
103 |
104 | ```
105 |
106 | That prop drilling isn't great, but we'll deal with it in a bit.
107 |
108 | ## Memoization
109 |
110 | - Wrap the action creators in `useCallback`
111 | - Wrap `NewGrudge` and `Grudge` in `React.memo`
112 | - Notice how we can reduce re-renders
113 |
114 | ## The Context API
115 |
116 | The above example wasn't _too_ bad. But, you can see how it might get a bit out of hand as our application grows.
117 |
118 | What if two very different distant cousin components needed the same data?
119 |
120 | Modern builds of React allow you to use something called the Context API to make this better. It's basically a way for very different pieces of your application to communicate with each other.
121 |
122 | We're going to rip a lot out of `Application.js` and move it to a new file called `GrudgeContext.js` and it's going to look something like this.
123 |
124 | ```js
125 | import React, { useReducer, createContext, useCallback } from 'react';
126 | import initialState from './initialState';
127 | import id from 'uuid/v4';
128 |
129 | export const GrudgeContext = createContext();
130 |
131 | const GRUDGE_ADD = 'GRUDGE_ADD';
132 | const GRUDGE_FORGIVE = 'GRUDGE_FORGIVE';
133 |
134 | const reducer = (state = [], action) => {
135 | if (action.type === GRUDGE_ADD) {
136 | return [
137 | {
138 | id: id(),
139 | ...action.payload
140 | },
141 | ...state
142 | ];
143 | }
144 |
145 | if (action.type === GRUDGE_FORGIVE) {
146 | return state.map(grudge => {
147 | if (grudge.id === action.payload.id) {
148 | return { ...grudge, forgiven: !grudge.forgiven };
149 | }
150 | return grudge;
151 | });
152 | }
153 |
154 | return state;
155 | };
156 |
157 | export const GrudgeProvider = ({ children }) => {
158 | const [grudges, dispatch] = useReducer(reducer, initialState);
159 |
160 | const addGrudge = useCallback(
161 | ({ person, reason }) => {
162 | dispatch({
163 | type: GRUDGE_ADD,
164 | payload: {
165 | person,
166 | reason
167 | }
168 | });
169 | },
170 | [dispatch]
171 | );
172 |
173 | const toggleForgiveness = useCallback(
174 | id => {
175 | dispatch({
176 | type: GRUDGE_FORGIVE,
177 | payload: {
178 | id
179 | }
180 | });
181 | },
182 | [dispatch]
183 | );
184 |
185 | return (
186 |
187 | {children}
188 |
189 | );
190 | };
191 | ```
192 |
193 | Now, `Application.js` looks a lot more slimmed down.
194 |
195 | ```js
196 | import React from 'react';
197 |
198 | import Grudges from './Grudges';
199 | import NewGrudge from './NewGrudge';
200 |
201 | const Application = () => {
202 | return (
203 |
204 |
205 |
206 |
207 | );
208 | };
209 |
210 | export default Application;
211 | ```
212 |
213 | ### Wrapping the Application in Your New Provider
214 |
215 | ```js
216 | ReactDOM.render(
217 |
218 |
219 | ,
220 | rootElement
221 | );
222 | ```
223 |
224 | That works and it's cool, but it's still missing the point.
225 |
226 | ### Hooking Up the Context API
227 |
228 | So, we don't need that pass through on `Grudges` anymore. Let's rip that out completely.
229 |
230 | ```js
231 | import React from 'react';
232 | import Grudge from './Grudge';
233 |
234 | const Grudges = ({ grudges = [] }) => {
235 | return (
236 |
237 | Grudges ({grudges.length})
238 | {grudges.map(grudge => (
239 |
240 | ))}
241 |
242 | );
243 | };
244 |
245 | export default Grudges;
246 | ```
247 |
248 | But, we will need to tell it about the grudges so that it can iterate through them.
249 |
250 | ```js
251 | import React from 'react';
252 | import Grudge from './Grudge';
253 | import { GrudgeContext } from './GrudgeContext';
254 |
255 | const Grudges = () => {
256 | const { grudges } = React.useContext(GrudgeContext);
257 |
258 | return (
259 |
260 | Grudges ({grudges.length})
261 | {grudges.map(grudge => (
262 |
263 | ))}
264 |
265 | );
266 | };
267 |
268 | export default Grudges;
269 | ```
270 |
271 | #### Individual Grudges
272 |
273 | ```js
274 | import React from 'react';
275 | import { GrudgeContext } from './GrudgeContext';
276 |
277 | const Grudge = ({ grudge }) => {
278 | const { toggleForgiveness } = React.useContext(GrudgeContext);
279 |
280 | return (
281 |
282 | {grudge.person}
283 | {grudge.reason}
284 |
285 |
293 |
294 |
295 | );
296 | };
297 |
298 | export default Grudge;
299 | ```
300 |
301 | ### Adding a New Grudge with the Context API
302 |
303 | In this case, we _just_ need the ability to add a grudge.
304 |
305 | ```js
306 | const NewGrudge = () => {
307 | const [person, setPerson] = React.useState('');
308 | const [reason, setReason] = React.useState('');
309 | const { addGrudge } = React.useContext(GrudgeContext);
310 |
311 | const handleSubmit = event => {
312 | event.preventDefault();
313 |
314 | addGrudge({
315 | person,
316 | reason
317 | });
318 | };
319 |
320 | return (
321 | // …
322 | );
323 | };
324 |
325 | export default NewGrudge;
326 | ```
327 |
328 | ## Implementing Undo/Redo
329 |
330 | ```js
331 | {
332 | past: [allPastStates],
333 | present: currentStateOfTheWorld,
334 | future: [anyAndAllFutureStates]
335 | }
336 | ```
337 |
338 | ### Some Tasting Notes
339 |
340 | - We lost all of our performance optimizations.
341 | - It's a trade off.
342 | - Grudge List might seem like a toy application, but it could also represent a smaller part of a larger system.
343 | - Could you use the Context API to get things all of the way down to this level and then use the approach we had previous?
344 |
345 | ## Alternative Data Structures
346 |
347 | Okay, so that array stuff is a bit wonky.
348 |
349 | What if we used an object?
350 |
351 | All of this is going to happen in `GrudgeContext.js`.
352 |
353 | What if our data was structured more like this?
354 |
355 | ```js
356 | const defaultGrudges = {
357 | 1: {
358 | id: 1,
359 | person: name.first(),
360 | reason: 'Parked too close to me in the parking lot',
361 | forgiven: false
362 | },
363 | 2: {
364 | id: 2,
365 | person: name.first(),
366 | reason: 'Did not brew another pot of coffee after drinking the last cup',
367 | forgiven: false
368 | }
369 | };
370 | ```
371 |
372 | ```js
373 | export const GrudgeProvider = ({ children }) => {
374 | const [grudges, setGrudges] = useState({});
375 |
376 | const addGrudge = grudge => {
377 | grudge.id = id();
378 | setGrudges({
379 | [grudge.id]: grudge,
380 | ...grudges
381 | });
382 | };
383 |
384 | const toggleForgiveness = id => {
385 | const newGrudges = { ...grudges };
386 | const target = grudges[id];
387 | target.forgiven = !target.forgiven;
388 | setGrudges(newGrudges);
389 | };
390 |
391 | return (
392 |
395 | {children}
396 |
397 | );
398 | };
399 | ```
400 |
401 | ## Implementing Undo and Redo
402 |
403 | We need to think about the past, present, and future.
404 |
405 | ```js
406 | const defaultState = {
407 | past: [],
408 | present: [],
409 | future: []
410 | };
411 | ```
412 |
413 | We've broken almost everything. So, let's make this a bit better.
414 |
415 | ```js
416 | const reducer = (state, action) => {
417 | if (action.type === ADD_GRUDGE) {
418 | return {
419 | past: [],
420 | present: [
421 | {
422 | id: uniqueId(),
423 | ...action.payload
424 | },
425 | ...state.present
426 | ],
427 | future: []
428 | };
429 | }
430 |
431 | if (action.type === FORGIVE_GRUDGE) {
432 | return {
433 | past: [],
434 | present: state.present.filter(grudge => grudge.id !== action.payload.id),
435 | future: []
436 | };
437 | }
438 |
439 | return state;
440 | };
441 | ```
442 |
443 | ### Adding to the Stack
444 |
445 | ```js
446 | past: [state.present, ...state.past];
447 | ```
448 |
449 | ```js
450 | if (action.type === UNDO) {
451 | const [newPresent, ...newPast] = state.past;
452 | return {
453 | past: newPast,
454 | present: newPresent,
455 | future: [state.present, ...state.present]
456 | };
457 | }
458 | ```
459 |
460 | ```js
461 | const undo = useCallback(() => {
462 | dispatch({ type: UNDO });
463 | }, [dispatch]);
464 | ```
465 |
466 | ```js
467 |
470 | ```
471 |
472 | ### Getting Redo
473 |
474 | ```js
475 | if (action.type === REDO) {
476 | const [newPresent, ...newFuture] = state.future;
477 | return {
478 | past: [state.present, ...state.past],
479 | present: newPresent,
480 | future: newFuture
481 | };
482 | }
483 | ```
484 |
485 | ## Abstracting All of This
486 |
487 | ```js
488 | const useUndoReducer = (reducer, initialState) => {
489 | const undoState = {
490 | past: [],
491 | present: initialState,
492 | future: []
493 | };
494 |
495 | const undoReducer = (state, action) => {
496 | const newPresent = reducer(state, action);
497 |
498 | if (action.type === UNDO) {
499 | const [newPresent, ...newPast] = state.past;
500 | return {
501 | past: newPast,
502 | present: newPresent,
503 | future: [state.present, ...state.future]
504 | };
505 | }
506 |
507 | if (action.type === REDO) {
508 | const [newPresent, ...newFuture] = state.future;
509 | return {
510 | past: [state.present, ...state.past],
511 | present: newPresent,
512 | future: newFuture
513 | };
514 | }
515 |
516 | return {
517 | past: [state.present, ...state.past],
518 | present: newPresent,
519 | future: []
520 | };
521 | };
522 |
523 | return useReducer(undoReducer, undoState);
524 | };
525 | ```
526 |
527 | ## Implementing a React/Redux-like Abstraction
528 |
529 | Let's make a new file called `connect.js`.
530 |
531 | We'll start with some simple imports.
532 |
533 | ```js
534 | import React, { createContext, useReducer } from 'react';
535 | import initialState from './initialState';
536 | import id from 'uuid/v4';
537 | ```
538 |
539 | Let's also pull in the action types and reducer from `GrudgeContext.js`.
540 |
541 | ```js
542 | export const GRUDGE_ADD = 'GRUDGE_ADD';
543 | export const GRUDGE_FORGIVE = 'GRUDGE_FORGIVE';
544 |
545 | export const reducer = (state = [], action) => {
546 | if (action.type === GRUDGE_ADD) {
547 | return [
548 | {
549 | id: id(),
550 | ...action.payload
551 | },
552 | ...state
553 | ];
554 | }
555 |
556 | if (action.type === GRUDGE_FORGIVE) {
557 | return state.map(grudge => {
558 | if (grudge.id === action.payload.id) {
559 | return { ...grudge, forgiven: !grudge.forgiven };
560 | }
561 | return grudge;
562 | });
563 | }
564 |
565 | return state;
566 | };
567 | ```
568 |
569 | We'll also want to create a context that we can use.
570 |
571 | Alright, so now we'll make a new provider that will take the reducer's state and dispatch and thread it through the application.
572 |
573 | Okay, let's make a generalized `Provider`.
574 |
575 | ```js
576 | export const Provider = ({ reducer, initialState, children }) => {
577 | const [state, dispatch] = useReducer(reducer, initialState);
578 |
579 | return (
580 | {children}
581 | );
582 | };
583 | ```
584 |
585 | Next, we'll make the `connect` function.
586 |
587 | ```js
588 | export const connect = (
589 | mapStateToProps,
590 | mapDispatchToProps
591 | ) => Component => ownProps => {
592 | const { state, dispatch } = useContext(Context);
593 |
594 | let stateProps = {};
595 | let dispatchProps = {};
596 |
597 | if (isFunction(mapStateToProps)) {
598 | stateProps = mapStateToProps(state, ownProps);
599 | }
600 |
601 | if (isFunction(mapDispatchToProps)) {
602 | dispatchProps = mapDispatchToProps(dispatch, ownProps);
603 | }
604 |
605 | Component.displayName = `Connected(${Component.displayName})`;
606 |
607 | return ;
608 | };
609 | ```
610 |
611 | We're going to make three container functions:
612 |
613 | - `NewGrudgeContainer`
614 | - `GrudgesContainer`
615 | - `GrudgeContainer`
616 |
617 | We're also going to need to rip the previous context out of.
618 |
619 | #### index.js
620 |
621 | ```js
622 | import React from 'react';
623 | import ReactDOM from 'react-dom';
624 |
625 | import Application from './Application';
626 |
627 | import { reducer, Provider } from './connect';
628 | import initialState from './initialState';
629 |
630 | import './styles.css';
631 |
632 | const rootElement = document.getElementById('root');
633 |
634 | ReactDOM.render(
635 |
636 |
637 | ,
638 | rootElement
639 | );
640 | ```
641 |
642 | #### GrudgesContainer.js
643 |
644 | ```js
645 | import { connect } from './connect';
646 | import Grudges from './Grudges';
647 |
648 | const mapStateToProps = state => {
649 | console.log({ state });
650 | return { grudges: state };
651 | };
652 |
653 | export default connect(mapStateToProps)(Grudges);
654 | ```
655 |
656 | #### GrudgeContainer.js
657 |
658 | ```js
659 | import { connect, GRUDGE_FORGIVE } from './connect';
660 | import Grudge from './Grudge';
661 |
662 | const mapDispatchToProps = (dispatch, ownProps) => {
663 | return {
664 | forgive() {
665 | dispatch({
666 | type: GRUDGE_FORGIVE,
667 | payload: {
668 | id: ownProps.grudge.id
669 | }
670 | });
671 | }
672 | };
673 | };
674 |
675 | export default connect(null, mapDispatchToProps)(Grudge);
676 | ```
677 |
678 | #### Grudges.js
679 |
680 | ```js
681 | import React from 'react';
682 | import GrudgeContainer from './GrudgeContainer';
683 |
684 | const Grudges = ({ grudges }) => {
685 | return (
686 |
687 | Grudges ({grudges.length})
688 | {grudges.map(grudge => (
689 |
690 | ))}
691 |
692 | );
693 | };
694 |
695 | export default Grudges;
696 | ```
697 |
698 | #### Grudge.js
699 |
700 | ```js
701 | import React from 'react';
702 | import { GrudgeContext } from './GrudgeContext';
703 |
704 | const Grudge = React.memo(({ grudge, forgive }) => {
705 | return (
706 |
707 | {grudge.person}
708 | {grudge.reason}
709 |
710 |
714 |
715 |
716 | );
717 | });
718 |
719 | export default Grudge;
720 | ```
721 |
722 | #### Exercise
723 |
724 | Can you implement `NewGrudgeContainer`?
725 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grudge-list",
3 | "version": "1.0.0",
4 | "description": "",
5 | "keywords": [],
6 | "main": "src/index.js",
7 | "dependencies": {
8 | "random-name": "0.1.2",
9 | "react": "16.8.6",
10 | "react-dom": "16.8.6",
11 | "react-scripts": "3.0.1",
12 | "uuid": "3.3.2"
13 | },
14 | "devDependencies": {
15 | "typescript": "3.3.3"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test --env=jsdom",
21 | "eject": "react-scripts eject"
22 | },
23 | "browserslist": [
24 | ">0.2%",
25 | "not dead",
26 | "not ie <= 11",
27 | "not op_mini all"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
20 | React App
21 |
22 |
23 |
24 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/Application.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import id from 'uuid/v4';
4 |
5 | import Grudges from './Grudges';
6 | import NewGrudge from './NewGrudge';
7 |
8 | import initialState from './initialState';
9 |
10 | const Application = () => {
11 | const [grudges, setGrudges] = useState(initialState);
12 |
13 | const addGrudge = grudge => {
14 | grudge.id = id();
15 | grudge.forgiven = false;
16 | setGrudges([grudge, ...grudges]);
17 | };
18 |
19 | const toggleForgiveness = id => {
20 | setGrudges(
21 | grudges.map(grudge => {
22 | if (grudge.id !== id) return grudge;
23 | return { ...grudge, forgiven: !grudge.forgiven };
24 | })
25 | );
26 | };
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default Application;
37 |
--------------------------------------------------------------------------------
/src/Grudge.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Grudge = ({ grudge, onForgive }) => {
4 | const forgive = () => onForgive(grudge.id);
5 |
6 | return (
7 |
8 | {grudge.person}
9 | {grudge.reason}
10 |
11 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Grudge;
21 |
--------------------------------------------------------------------------------
/src/Grudges.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Grudge from './Grudge';
3 |
4 | const Grudges = ({ grudges = [], onForgive }) => {
5 | return (
6 |
7 | Grudges ({grudges.length})
8 | {grudges.map(grudge => (
9 |
10 | ))}
11 |
12 | );
13 | };
14 |
15 | export default Grudges;
16 |
--------------------------------------------------------------------------------
/src/NewGrudge.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const NewGrudge = ({ onSubmit }) => {
4 | const [person, setPerson] = useState('');
5 | const [reason, setReason] = useState('');
6 |
7 | const handleChange = event => {
8 | event.preventDefault();
9 | onSubmit({ person, reason });
10 | };
11 |
12 | return (
13 |
30 | );
31 | };
32 |
33 | export default NewGrudge;
34 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Application from './Application';
5 |
6 | import './styles.css';
7 |
8 | const rootElement = document.getElementById('root');
9 |
10 | ReactDOM.render(, rootElement);
11 |
--------------------------------------------------------------------------------
/src/initialState.js:
--------------------------------------------------------------------------------
1 | import name from 'random-name';
2 | import id from 'uuid/v4';
3 |
4 | // This is some dummy data.
5 | const initialState = [
6 | {
7 | id: id(),
8 | person: name.first(),
9 | reason: 'Parked too close to me in the parking lot',
10 | forgiven: false
11 | },
12 | {
13 | id: id(),
14 | person: name.first(),
15 | reason: 'Did not brew another pot of coffee after drinking the last cup',
16 | forgiven: false
17 | },
18 | {
19 | id: id(),
20 | person: name.first(),
21 | reason: 'Failed to wish me a happy birthday but ate my cake',
22 | forgiven: false
23 | },
24 | {
25 | id: id(),
26 | person: name.first(),
27 | reason: 'Generally obnoxious and unrepentant about that fact',
28 | forgiven: true
29 | },
30 | {
31 | id: id(),
32 | person: name.first(),
33 | reason: 'Cut me in line at Safeway and then made eye contact',
34 | forgiven: false
35 | },
36 | {
37 | id: id(),
38 | person: name.first(),
39 | reason: 'Ate the last slice of pizza and left the box out',
40 | forgiven: false
41 | },
42 | {
43 | id: id(),
44 | person: name.first(),
45 | reason: 'Brought "paper products" to a potluck',
46 | forgiven: false
47 | },
48 | {
49 | id: id(),
50 | person: name.first(),
51 | reason: 'Talked over me when I was telling a story',
52 | forgiven: false
53 | },
54 | {
55 | id: id(),
56 | person: name.first(),
57 | reason: 'Changed my playlist as soon as I left the room for 30 seconds',
58 | forgiven: false
59 | },
60 | {
61 | id: id(),
62 | person: name.first(),
63 | reason: 'Spoiled the plot line for Avengers: Endgame',
64 | forgiven: false
65 | },
66 | {
67 | id: id(),
68 | person: name.first(),
69 | reason: 'Ate all of the vegan ham leftovers despite being labelled',
70 | forgiven: false
71 | }
72 | ];
73 |
74 | export default initialState;
75 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | html,
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | body,
7 | input {
8 | font: caption;
9 | }
10 |
11 | input {
12 | padding: 0.5em;
13 | border: 1px solid #f32e5b;
14 | background-color: #fbbfcd;
15 | }
16 |
17 | input:not(:first-of-type) {
18 | border-left: none;
19 | }
20 |
21 | button,
22 | .button,
23 | input[type='submit'] {
24 | background-color: #f32e5b;
25 | border: 1px solid #e10d3d;
26 | color: white;
27 | padding: 0.5em;
28 | transition: all 0.2s;
29 | }
30 |
31 | button:hover,
32 | .button:hover,
33 | input[type='submit']:hover {
34 | background-color: #f65e81;
35 | }
36 |
37 | button:active,
38 | .button:active,
39 | input[type='submit']:active {
40 | background-color: #f4466e;
41 | }
42 |
43 | button.full-width,
44 | .button.full-width,
45 | input[type='submit'].full-width {
46 | width: 100%;
47 | margin: 1em 0;
48 | }
49 |
50 | .Application {
51 | margin: auto;
52 | max-width: 400px;
53 | }
54 |
55 | .Grudges-searchTerm {
56 | background-color: white;
57 | margin-bottom: 0.5em;
58 | width: 100%;
59 | }
60 |
61 | .Grudge {
62 | border: 1px solid #f4466e;
63 | padding: 1em;
64 | margin: 1em 0;
65 | }
66 |
67 | .Grudge h3 {
68 | margin-top: 0;
69 | }
70 |
71 | .Grudge-forgiven {
72 | color: #f4466e;
73 | }
74 |
75 | .NewGrudge {
76 | display: flex;
77 | }
78 |
79 | .NewGrudge-input {
80 | width: 100%;
81 | }
82 |
--------------------------------------------------------------------------------