├── .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 |
14 | setPerson(event.target.value)} 20 | /> 21 | setReason(event.target.value)} 27 | /> 28 | 29 |
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 | --------------------------------------------------------------------------------