├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── public ├── 404.html ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.test.js ├── AppOriginal.css ├── components ├── Another.jsx ├── App.jsx ├── AppClass.jsx ├── AppOriginal.jsx ├── NavigationBar.jsx ├── NoTodos.jsx ├── Root.jsx ├── TodoClearCompleted.jsx ├── TodoCompleteAllTodos.jsx ├── TodoFilters.jsx ├── TodoForm.jsx ├── TodoItemsRemaining.jsx ├── TodoList.jsx ├── api │ ├── Joke.jsx │ ├── Reddit.jsx │ └── useFetch.js ├── github-markdown.css └── pages │ ├── Comments.jsx │ ├── Details.jsx │ ├── IconClosed.jsx │ ├── IconOpen.jsx │ ├── Issues.jsx │ ├── NoMatch.jsx │ └── api.jsx ├── context └── TodosContext.js ├── hooks ├── useLocalStorage.js └── useToggle.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js ├── reset.css └── setupTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "avoid" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Todo App 2 | 3 | 4 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 5 | 6 | ## Getting Started 7 | [Live Demo](https://ismaildasci.github.io/react-todo/) 8 | 9 | 10 | ### Installation 11 | Follow the steps below to run this project on your local machine. 12 | 13 | Prerequisites 14 | 15 | Node.js and NPM (comes with Node.js) 16 | 17 | 1. Clone the Repository 18 | 19 | Clone the repository to your local machine: 20 | ```sh 21 | git clone https://github.com/ismaildasci/react-todo.git 22 | cd react-todo 23 | 24 | ``` 25 | 26 | 2. Install Dependencies 27 | Install the project dependencies: 28 | ```sh 29 | npm install 30 | 31 | ``` 32 | 33 | 1. Run the Application 34 | 35 | Start the application: 36 | ```sh 37 | npm start 38 | 39 | ``` 40 | This command runs the app in development mode. Open http://localhost:3000/react-todo to view it in the browser. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc-react", 3 | "version": "0.1.0", 4 | "homepage": "https://ismaildasci.github.io/react-todo", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "date-fns": "^2.30.0", 11 | "prop-types": "^15.8.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-markdown": "^8.0.7", 15 | "react-query": "^3.39.3", 16 | "react-router-dom": "^6.14.0", 17 | "react-scripts": "5.0.1", 18 | "react-transition-group": "^4.4.5", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start --public-url /react-todo/", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject", 26 | "predeploy": "npm run build", 27 | "deploy": "gh-pages -d build" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "gh-pages": "^5.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redirecting 7 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismaildasci/react-todo/01816e6a4762f12870d128f9a9cdb276b39e1c5a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismaildasci/react-todo/01816e6a4762f12870d128f9a9cdb276b39e1c5a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismaildasci/react-todo/01816e6a4762f12870d128f9a9cdb276b39e1c5a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 11 | sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .todo-app-container { 17 | min-height: 100vh; 18 | background: #f3f4f6; 19 | } 20 | 21 | .container { 22 | margin: auto; 23 | padding-top: 24px; 24 | max-width: 48rem; 25 | } 26 | 27 | .todo-app { 28 | margin: auto; 29 | margin-top: 30px; 30 | padding: 2rem; 31 | background: white; 32 | border-radius: 0.25rem; 33 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 34 | max-width: 32rem; 35 | color: #374151; 36 | } 37 | 38 | h2 { 39 | font-size: 24px; 40 | font-weight: bold; 41 | } 42 | 43 | .todo-input { 44 | width: 100%; 45 | border: none; 46 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 47 | padding: 14px; 48 | font-size: 16px; 49 | margin-top: 16px; 50 | } 51 | 52 | .todo-list { 53 | margin-top: 32px; 54 | } 55 | 56 | .todo-item-container { 57 | display: flex; 58 | justify-content: space-between; 59 | align-items: center; 60 | margin-top: 24px; 61 | } 62 | 63 | .todo-item { 64 | display: flex; 65 | align-items: center; 66 | flex: 1; 67 | font-size: 18px; 68 | margin-right: 24px; 69 | } 70 | 71 | .todo-item-label { 72 | margin-left: 16px; 73 | } 74 | 75 | .todo-item-input { 76 | margin-left: 8px; 77 | width: 100%; 78 | border: none; 79 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 80 | padding: 6px 8px; 81 | font-size: 18px; 82 | } 83 | 84 | .line-through { 85 | text-decoration: line-through; 86 | } 87 | 88 | .x-button { 89 | background: white; 90 | color: #6B7280; 91 | border: none; 92 | cursor: pointer; 93 | } 94 | 95 | .x-button:hover { 96 | color: #1F2937; 97 | } 98 | 99 | .x-button-icon { 100 | width: 1.5rem; 101 | height: 1.5rem; 102 | } 103 | 104 | .button { 105 | color: #6B7280; 106 | font-size: 14px; 107 | background: white; 108 | border: 1px solid lightgray; 109 | padding: 6px; 110 | border-radius: 5px; 111 | cursor: pointer; 112 | } 113 | 114 | .button:hover { 115 | border: 1px solid lightgray; 116 | } 117 | 118 | .filter-button { 119 | border: 1px solid white; 120 | } 121 | 122 | .filter-button-active { 123 | border: 1px solid lightgray; 124 | } 125 | 126 | .check-all-container { 127 | display: flex; 128 | justify-content: space-between; 129 | align-items: center; 130 | color: #6B7280; 131 | margin-top: 22px; 132 | padding-top: 16px; 133 | border-top: 1px solid lightgray; 134 | } 135 | 136 | .other-buttons-container { 137 | display: flex; 138 | justify-content: space-between; 139 | align-items: center; 140 | color: #6B7280; 141 | margin-top: 22px; 142 | padding-top: 16px; 143 | border-top: 1px solid lightgray; 144 | } 145 | 146 | .no-todos-container { 147 | width: 300px; 148 | margin: 30px auto; 149 | } 150 | 151 | .no-todos-container p { 152 | margin-top: 20px; 153 | text-align: center; 154 | } 155 | 156 | .name-container { 157 | margin-bottom: 40px; 158 | } 159 | 160 | .name-label { 161 | margin-top: 15px; 162 | } 163 | 164 | .toggles-container { 165 | margin: 20px 0; 166 | } 167 | 168 | nav { 169 | background: white; 170 | padding: 16px; 171 | } 172 | 173 | nav ul { 174 | display: flex; 175 | justify-content: center; 176 | margin: auto; 177 | } 178 | 179 | nav ul li:not(:first-child) { 180 | margin-left: 16px; 181 | } 182 | 183 | .active { 184 | font-weight: bold; 185 | } 186 | 187 | /* Transitions */ 188 | 189 | .slide-vertical-enter { 190 | opacity: 0; 191 | transform: translateY(-20px); 192 | } 193 | 194 | .slide-vertical-enter-active { 195 | opacity: 1; 196 | transform: translateY(0); 197 | transition: all 300ms; 198 | } 199 | 200 | .slide-vertical-exit { 201 | opacity: 1; 202 | transform: translateY(0); 203 | } 204 | 205 | .slide-vertical-exit-active { 206 | opacity: 0; 207 | transition: all 300ms; 208 | transform: translateY(-20px); 209 | } 210 | 211 | .slide-horizontal-enter { 212 | opacity: 0; 213 | transform: translateX(20px); 214 | } 215 | 216 | .slide-horizontal-enter-active { 217 | opacity: 1; 218 | transform: translateX(0); 219 | transition: all 300ms; 220 | } 221 | 222 | .slide-horizontal-exit { 223 | opacity: 1; 224 | transform: translateX(0); 225 | } 226 | 227 | .slide-horizontal-exit-active { 228 | opacity: 0; 229 | transition: all 300ms; 230 | transform: translateX(20px); 231 | } 232 | 233 | *, 234 | *::before, 235 | *::after { 236 | box-sizing: border-box; 237 | } 238 | 239 | body { 240 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 241 | color: #111827; 242 | } 243 | 244 | a { 245 | color: #0265d6; 246 | text-decoration: none; 247 | } 248 | 249 | .container { 250 | max-width: 54rem; 251 | margin: 32px auto; 252 | } 253 | 254 | .font-bold { 255 | font-weight: bold; 256 | } 257 | 258 | .issues-heading { 259 | background: #f5f8fa; 260 | padding: 15px 15px; 261 | border-top-left-radius: 10px; 262 | border-top-right-radius: 10px; 263 | border: 1px solid #e1e4e8; 264 | border-bottom: none; 265 | } 266 | 267 | .issues-heading a { 268 | font-weight: bold; 269 | font-size: 20px; 270 | } 271 | 272 | .issues-heading a:hover { 273 | text-decoration: underline; 274 | } 275 | 276 | .issues-entry { 277 | display: flex; 278 | justify-content: space-between; 279 | align-items: center; 280 | border: 1px solid #e1e4e8; 281 | padding: 12px 8px; 282 | } 283 | 284 | .issues-entry:last-child { 285 | border-bottom-left-radius: 10px; 286 | border-bottom-right-radius: 10px; 287 | } 288 | 289 | .issues-entry:hover { 290 | background: #f5f8fa; 291 | } 292 | 293 | .issues-entry-title-container { 294 | display: flex; 295 | } 296 | 297 | 298 | .issues-entry-title-container { 299 | display: flex; 300 | } 301 | 302 | .issues-title { 303 | margin-left: 8px; 304 | font-weight: bold; 305 | } 306 | 307 | .issues-title a { 308 | color: #111827; 309 | text-decoration: none; 310 | } 311 | 312 | .issues-title a:hover { 313 | color: #0265d6; 314 | } 315 | 316 | .issues-title-details { 317 | font-size: 12px; 318 | font-weight: normal; 319 | margin-top: 8px; 320 | color: #374151; 321 | } 322 | 323 | .comments-count-container { 324 | display: flex; 325 | text-decoration: none; 326 | color: #374151; 327 | } 328 | 329 | .comments-count-container:hover { 330 | color: #0265d6; 331 | } 332 | 333 | .comments-count { 334 | text-decoration: none; 335 | margin-left: 4px; 336 | font-size: 14px; 337 | } 338 | 339 | .open-closed-buttons { 340 | margin-top: 12px; 341 | } 342 | 343 | .open-closed-buttons button { 344 | display: inline-flex; 345 | align-items: center; 346 | border: none; 347 | background: none; 348 | padding: 0; 349 | cursor: pointer; 350 | } 351 | 352 | .open-closed-buttons button:last-child { 353 | margin-left: 14px; 354 | } 355 | 356 | .open-closed-buttons span { 357 | margin-left: 4px; 358 | font-size: 14px; 359 | } 360 | 361 | svg { 362 | fill: currentColor; 363 | } 364 | 365 | .open { 366 | color: #22863a; 367 | } 368 | 369 | .closed { 370 | color: #e5534b; 371 | } 372 | 373 | /* Comments */ 374 | 375 | .comments-container { 376 | margin-top: 48px; 377 | } 378 | 379 | .comments-container h2 { 380 | font-size: 24px; 381 | font-weight: bold; 382 | } 383 | 384 | .comments-container h2 span { 385 | color: #374151; 386 | font-weight: normal; 387 | } 388 | 389 | .issue-details { 390 | font-size: 14px; 391 | margin-top: 12px; 392 | color: #374151; 393 | } 394 | 395 | .issue-details a { 396 | color: #374151; 397 | font-weight: bold; 398 | } 399 | 400 | .issue-details a:hover { 401 | color: #0265d6; 402 | } 403 | 404 | .comment-container { 405 | display: flex; 406 | } 407 | 408 | .comment-container:not(:first-child) { 409 | margin-top: 32px; 410 | } 411 | 412 | .avatar { 413 | width: 42px; 414 | border-radius: 50%; 415 | } 416 | 417 | .comment { 418 | margin-left: 18px; 419 | flex: 1; 420 | max-width: 100%; 421 | } 422 | 423 | .comment-heading { 424 | font-size: 14px; 425 | background: #f5f8fa; 426 | padding: 15px 15px; 427 | border-top-left-radius: 10px; 428 | border-top-right-radius: 10px; 429 | border: 1px solid #e1e4e8; 430 | border-bottom: none; 431 | } 432 | 433 | .comment-heading a { 434 | color: #111827; 435 | font-weight: bold; 436 | } 437 | 438 | .comment-heading a:hover { 439 | color: #0265d6; 440 | text-decoration: underline; 441 | } 442 | 443 | .comment-body { 444 | border-bottom-left-radius: 10px; 445 | border-bottom-right-radius: 10px; 446 | border: 1px solid #e1e4e8; 447 | padding: 16px; 448 | line-height: 1.5; 449 | } 450 | 451 | .border { 452 | padding-top: 32px; 453 | border-bottom: 1px solid #F3F4F6; 454 | } 455 | .navbar { 456 | background-color: #2f3640; 457 | overflow: hidden; 458 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 459 | } 460 | 461 | .navbar ul { 462 | list-style: none; 463 | margin: 0; 464 | padding: 0; 465 | display: flex; 466 | align-items: center; 467 | } 468 | 469 | .navbar li { 470 | padding: 15px 25px; 471 | } 472 | 473 | .navbar a { 474 | color: #ecf0f1; 475 | text-decoration: none; 476 | font-size: 16px; 477 | font-family: 'Roboto', sans-serif; 478 | transition: color 0.3s ease-in-out; 479 | } 480 | 481 | .navbar a:hover, 482 | .navbar a.active { 483 | color: #3498db; 484 | } 485 | .api-button { 486 | background-color: #34495e; 487 | border: none; 488 | color: white; 489 | text-align: center; 490 | text-decoration: none; 491 | display: inline-block; 492 | font-size: 16px; 493 | margin: 10px; 494 | padding: 15px 30px; 495 | cursor: pointer; 496 | border-radius: 8px; 497 | transition: background-color 0.3s ease-in-out, transform 0.3s ease-in-out; 498 | } 499 | 500 | .buttons { 501 | display: flex; 502 | justify-content: center; 503 | margin: 20px; 504 | } 505 | 506 | .api-button { 507 | background-color: #2f3640; 508 | border: 1px solid #444; 509 | color: #fff; 510 | text-align: center; 511 | text-decoration: none; 512 | display: inline-block; 513 | font-size: 14px; 514 | margin: 10px; 515 | padding: 10px 20px; 516 | cursor: pointer; 517 | border-radius: 4px; 518 | } 519 | 520 | .api-button:hover { 521 | background-color: #3498db; 522 | } 523 | 524 | .content { 525 | display: flex; 526 | flex-direction: column; 527 | align-items: center; 528 | margin: 20px; 529 | } 530 | 531 | .reddit-content, 532 | .joke-content { 533 | background-color: #f9f9f9; 534 | padding: 20px; 535 | border-radius: 4px; 536 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 537 | width: 100%; 538 | max-width: 800px; 539 | margin: 10px; 540 | text-align: left; 541 | } 542 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/AppOriginal.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | /* animation: App-logo-spin infinite 20s linear; */ 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | 36 | to { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Another.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Another(props) { 4 | return
Another Component, {props.name}
; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react'; 2 | import NoTodos from './NoTodos'; 3 | import TodoForm from './TodoForm'; 4 | import TodoList from './TodoList'; 5 | import useLocalStorage from '../hooks/useLocalStorage'; 6 | import '../reset.css'; 7 | import '../App.css'; 8 | import { TodosContext } from '../context/TodosContext'; 9 | import { CSSTransition, SwitchTransition } from 'react-transition-group'; 10 | 11 | function App() { 12 | const [name, setName] = useLocalStorage('name', ''); 13 | 14 | const nameInputEl = useRef(null); 15 | const [todos, setTodos] = useLocalStorage('todos', []); 16 | 17 | const [idForTodo, setIdForTodo] = useLocalStorage('idForTodo', 1); 18 | 19 | const [filter, setFilter] = useState('all'); 20 | 21 | function todosFiltered() { 22 | if (filter === 'all') { 23 | return todos; 24 | } else if (filter === 'active') { 25 | return todos.filter(todo => !todo.isComplete); 26 | } else if (filter === 'completed') { 27 | return todos.filter(todo => todo.isComplete); 28 | } 29 | } 30 | 31 | useEffect(() => { 32 | // console.log('use effect running'); 33 | nameInputEl.current.focus(); 34 | 35 | // setName(JSON.parse(localStorage.getItem('name')) ?? ''); 36 | 37 | return function cleanup() { 38 | // console.log('cleaning up'); 39 | }; 40 | }, []); 41 | 42 | function handleNameInput(event) { 43 | setName(event.target.value); 44 | // localStorage.setItem('name', JSON.stringify(event.target.value)); 45 | } 46 | 47 | return ( 48 | 59 |
60 |
61 |

What is your name?

62 |
63 | 71 |
72 | 73 | 0} 75 | timeout={300} 76 | classNames="slide-vertical" 77 | unmountOnExit 78 | > 79 |

Hello, {name}

80 |
81 |
82 |

Todo App

83 | 84 | 85 | 86 | 0} 88 | timeout={300} 89 | classNames="slide-vertical" 90 | unmountOnExit 91 | > 92 | {todos.length > 0 ? : } 93 | 94 | 95 | 96 | {/* 0} 98 | timeout={300} 99 | classNames="slide-vertical" 100 | unmountOnExit 101 | > 102 | 103 | 104 | 105 | 111 | 112 | */} 113 |
114 |
115 | ); 116 | } 117 | 118 | export default App; 119 | -------------------------------------------------------------------------------- /src/components/AppClass.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class AppClass extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | idForTodo: 4, 8 | todos: [ 9 | { 10 | id: 1, 11 | title: 'Finish React Series', 12 | isComplete: false, 13 | }, 14 | { 15 | id: 2, 16 | title: 'Go Grocery', 17 | isComplete: true, 18 | }, 19 | { 20 | id: 3, 21 | title: 'Take over world', 22 | isComplete: false, 23 | }, 24 | ], 25 | }; 26 | } 27 | 28 | addTodo = event => { 29 | event.preventDefault(); 30 | 31 | this.setState(prevState => { 32 | const newTodos = [ 33 | ...prevState.todos, 34 | { 35 | id: 4, 36 | title: 'This is class based components', 37 | isComplete: false, 38 | }, 39 | ]; 40 | 41 | return { todos: newTodos }; 42 | }); 43 | }; 44 | 45 | render() { 46 | return ( 47 |
48 |
49 |

Todo App

50 |
51 | 56 |
57 | 58 |
    59 | {this.state.todos.map((todo, index) => ( 60 |
  • 61 |
    62 | 63 | {todo.title} 64 | {/* */} 65 |
    66 | 81 |
  • 82 | ))} 83 |
84 | 85 |
86 |
87 |
Check All
88 |
89 | 90 | 3 items remaining 91 |
92 | 93 |
94 |
95 | 98 | 99 | 100 |
101 |
102 | 103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/components/AppOriginal.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Another from './Another'; 3 | import logo from './logo.svg'; 4 | import '../App.css'; 5 | 6 | function App() { 7 | const [count, setCount] = useState(0); 8 | 9 | function decrement() { 10 | setCount(prevCount => prevCount - 1); 11 | } 12 | 13 | function increment() { 14 | setCount(prevCount => prevCount + 1); 15 | } 16 | 17 | const someStyle = { 18 | background: 'blue', 19 | color: 'white', 20 | fontSize: '28px', 21 | fontWeight: 'bold', 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 28 |
29 | {count} 30 | 31 | 32 |
33 | logo 34 |

35 | Edit src/App.js and save to reload. 36 |

37 | {true &&

{3 + 2}

} 38 | 44 | Learn React 45 | 46 |
47 |
48 | ); 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /src/components/NavigationBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | export default function NavigationBar() { 5 | return ( 6 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/NoTodos.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NoTodos() { 4 | return ( 5 |
6 | 12 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |

Add some todos...

61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | import NavigationBar from './NavigationBar'; 4 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 5 | import About from './pages/api'; 6 | import Issues from './pages/Issues'; 7 | import Details from './pages/Details'; 8 | import NoMatch from './pages/NoMatch'; 9 | 10 | // const basename = process.env.NODE_ENV === 'development' ? '/' : '/react-todo'; 11 | 12 | export default function Root() { 13 | return ( 14 | 15 |
16 | 17 |
18 | 19 | } exact /> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/TodoClearCompleted.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { TodosContext } from '../context/TodosContext'; 3 | 4 | function TodoClearCompleted() { 5 | const { todos, setTodos } = useContext(TodosContext); 6 | 7 | function clearCompleted() { 8 | setTodos([...todos].filter(todo => !todo.isComplete)); 9 | } 10 | 11 | return ( 12 | 15 | ); 16 | } 17 | 18 | export default TodoClearCompleted; 19 | -------------------------------------------------------------------------------- /src/components/TodoCompleteAllTodos.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { TodosContext } from '../context/TodosContext'; 3 | 4 | function TodoCompleteAllTodos() { 5 | const { todos, setTodos } = useContext(TodosContext); 6 | 7 | function completeAllTodos() { 8 | const updatedTodos = todos.map(todo => { 9 | todo.isComplete = true; 10 | 11 | return todo; 12 | }); 13 | 14 | setTodos(updatedTodos); 15 | } 16 | 17 | return ( 18 |
19 |
20 | Check All 21 |
22 |
23 | ); 24 | } 25 | 26 | export default TodoCompleteAllTodos; 27 | -------------------------------------------------------------------------------- /src/components/TodoFilters.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { TodosContext } from '../context/TodosContext'; 3 | 4 | function TodoFilters() { 5 | const { filter, setFilter, todosFiltered } = useContext(TodosContext); 6 | 7 | return ( 8 |
9 | 20 | 31 | 42 |
43 | ); 44 | } 45 | 46 | export default TodoFilters; 47 | -------------------------------------------------------------------------------- /src/components/TodoForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { TodosContext } from '../context/TodosContext'; 3 | 4 | function TodoForm() { 5 | const { todos, setTodos, idForTodo, setIdForTodo } = useContext(TodosContext); 6 | 7 | const [todoInput, setTodoInput] = useState(''); 8 | 9 | function handleInput(event) { 10 | setTodoInput(event.target.value); 11 | } 12 | 13 | function addTodo(event) { 14 | event.preventDefault(); 15 | 16 | if (todoInput.trim().length === 0) { 17 | return; 18 | } 19 | 20 | setTodos([ 21 | ...todos, 22 | { 23 | id: idForTodo, 24 | title: todoInput, 25 | isComplete: false, 26 | }, 27 | ]); 28 | 29 | setIdForTodo(prevIdForTodo => prevIdForTodo + 1); 30 | 31 | setTodoInput(''); 32 | } 33 | 34 | return ( 35 |
36 | 43 |
44 | ); 45 | } 46 | 47 | export default TodoForm; 48 | -------------------------------------------------------------------------------- /src/components/TodoItemsRemaining.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo } from 'react'; 2 | import { TodosContext } from '../context/TodosContext'; 3 | 4 | function TodoItemsRemaining() { 5 | const { todos } = useContext(TodosContext); 6 | 7 | function remainingCalculation() { 8 | // console.log('calculating remaining todos. This is slow.'); 9 | // for (let index = 0; index < 2000000000; index++) {} 10 | return todos.filter(todo => !todo.isComplete).length; 11 | } 12 | 13 | const remaining = useMemo(remainingCalculation, [todos]); 14 | 15 | return {remaining} items remaining; 16 | } 17 | 18 | export default TodoItemsRemaining; 19 | -------------------------------------------------------------------------------- /src/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import TodoItemsRemaining from './TodoItemsRemaining'; 3 | import TodoClearCompleted from './TodoClearCompleted'; 4 | import TodoCompleteAllTodos from './TodoCompleteAllTodos'; 5 | import TodoFilters from './TodoFilters'; 6 | import useToggle from '../hooks/useToggle'; 7 | import { TodosContext } from '../context/TodosContext'; 8 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 9 | 10 | function TodoList() { 11 | const { todos, setTodos, todosFiltered } = useContext(TodosContext); 12 | const [isFeaturesOneVisible, setFeaturesOneVisible] = useToggle(); 13 | const [isFeaturesTwoVisible, setFeaturesTwoVisible] = useToggle(); 14 | 15 | function deleteTodo(id) { 16 | setTodos([...todos].filter(todo => todo.id !== id)); 17 | } 18 | 19 | function completeTodo(id) { 20 | const updatedTodos = todos.map(todo => { 21 | if (todo.id === id) { 22 | todo.isComplete = !todo.isComplete; 23 | } 24 | 25 | return todo; 26 | }); 27 | 28 | setTodos(updatedTodos); 29 | } 30 | 31 | function markAsEditing(id) { 32 | const updatedTodos = todos.map(todo => { 33 | if (todo.id === id) { 34 | todo.isEditing = true; 35 | } 36 | 37 | return todo; 38 | }); 39 | 40 | setTodos(updatedTodos); 41 | } 42 | 43 | function updateTodo(event, id) { 44 | const updatedTodos = todos.map(todo => { 45 | if (todo.id === id) { 46 | if (event.target.value.trim().length === 0) { 47 | todo.isEditing = false; 48 | return todo; 49 | } 50 | todo.title = event.target.value; 51 | todo.isEditing = false; 52 | } 53 | 54 | return todo; 55 | }); 56 | 57 | setTodos(updatedTodos); 58 | } 59 | 60 | function cancelEdit(event, id) { 61 | const updatedTodos = todos.map(todo => { 62 | if (todo.id === id) { 63 | todo.isEditing = false; 64 | } 65 | 66 | return todo; 67 | }); 68 | 69 | setTodos(updatedTodos); 70 | } 71 | 72 | return ( 73 | <> 74 | 75 | {todosFiltered().map((todo, index) => ( 76 | 81 |
  • 82 |
    83 | completeTodo(todo.id)} 86 | checked={todo.isComplete ? true : false} 87 | /> 88 | 89 | {!todo.isEditing ? ( 90 | markAsEditing(todo.id)} 92 | className={`todo-item-label ${ 93 | todo.isComplete ? 'line-through' : '' 94 | }`} 95 | > 96 | {todo.title} 97 | 98 | ) : ( 99 | updateTodo(event, todo.id)} 102 | onKeyDown={event => { 103 | if (event.key === 'Enter') { 104 | updateTodo(event, todo.id); 105 | } else if (event.key === 'Escape') { 106 | cancelEdit(event, todo.id); 107 | } 108 | }} 109 | className="todo-item-input" 110 | defaultValue={todo.title} 111 | autoFocus 112 | /> 113 | )} 114 |
    115 | 130 |
  • 131 |
    132 | ))} 133 |
    134 | 135 |
    136 | 139 | 142 |
    143 | 144 | 150 |
    151 | 152 | 153 | 154 |
    155 |
    156 | 157 | 163 |
    164 | 165 |
    166 | 167 |
    168 |
    169 |
    170 | 171 | ); 172 | } 173 | 174 | export default TodoList; 175 | -------------------------------------------------------------------------------- /src/components/api/Joke.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useQuery } from 'react-query'; 3 | 4 | export default function Joke() { 5 | const { 6 | data: joke, 7 | isLoading, 8 | isError, 9 | error, 10 | isSuccess, 11 | } = useQuery('joke', fetchJoke, { 12 | // staleTime: 6000, 13 | refetchOnWindowFocus: false, 14 | }); 15 | 16 | function fetchJoke() { 17 | return fetch('https://official-joke-api.appspot.com/jokes/random').then( 18 | response => response.json() 19 | ); 20 | } 21 | 22 | // const { 23 | // data: joke, 24 | // isLoading, 25 | // errorMessage, 26 | // } = useFetch('https://official-joke-api.appspot.com/jokes/random'); 27 | 28 | return ( 29 |
    30 |

    Joke API

    31 | {isLoading &&
    Loading...
    } 32 | {isSuccess &&
    {joke.setup + ' ' + joke.punchline}
    } 33 | {isError &&
    {error.message}
    } 34 |
    35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/api/Reddit.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useQuery } from 'react-query'; 3 | 4 | export default function Reddit() { 5 | // const { 6 | // data: posts, 7 | // isLoading, 8 | // errorMessage, 9 | // } = useFetch('https://www.reddit.com/r/aww.json'); 10 | 11 | const { 12 | data: posts, 13 | isLoading, 14 | isError, 15 | error, 16 | isSuccess, 17 | } = useQuery('posts', fetchPosts, { 18 | retry: false, 19 | }); 20 | 21 | function fetchPosts() { 22 | return fetch('https://www.reddit.com/r/aww.json').then(response => 23 | response.json() 24 | ); 25 | } 26 | 27 | return ( 28 |
    29 |

    Reddit API

    30 | {isLoading &&
    Loading...
    } 31 | {isSuccess && ( 32 | 41 | )} 42 | {isError &&
    {error.message}
    } 43 |
    44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/api/useFetch.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | function useFetch(url) { 4 | const [data, setData] = useState(null); 5 | const [isLoading, setIsLoading] = useState(true); 6 | const [errorMessage, setErrorMessage] = useState(null); 7 | 8 | useEffect(() => { 9 | fetch(url) 10 | .then(response => response.json()) 11 | .then(results => { 12 | setIsLoading(false); 13 | setData(results); 14 | }) 15 | .catch(error => { 16 | setIsLoading(false); 17 | setErrorMessage('There was an error'); 18 | }); 19 | }, [url]); 20 | 21 | return { data, isLoading, errorMessage }; 22 | } 23 | 24 | export default useFetch; 25 | -------------------------------------------------------------------------------- /src/components/github-markdown.css: -------------------------------------------------------------------------------- 1 | .markdown-body .octicon { 2 | display: inline-block; 3 | fill: currentColor; 4 | vertical-align: text-bottom; 5 | } 6 | 7 | .markdown-body .anchor { 8 | float: left; 9 | line-height: 1; 10 | margin-left: -20px; 11 | padding-right: 4px; 12 | } 13 | 14 | .markdown-body .anchor:focus { 15 | outline: none; 16 | } 17 | 18 | .markdown-body h1 .octicon-link, 19 | .markdown-body h2 .octicon-link, 20 | .markdown-body h3 .octicon-link, 21 | .markdown-body h4 .octicon-link, 22 | .markdown-body h5 .octicon-link, 23 | .markdown-body h6 .octicon-link { 24 | color: #1b1f23; 25 | vertical-align: middle; 26 | visibility: hidden; 27 | } 28 | 29 | .markdown-body h1:hover .anchor, 30 | .markdown-body h2:hover .anchor, 31 | .markdown-body h3:hover .anchor, 32 | .markdown-body h4:hover .anchor, 33 | .markdown-body h5:hover .anchor, 34 | .markdown-body h6:hover .anchor { 35 | text-decoration: none; 36 | } 37 | 38 | .markdown-body h1:hover .anchor .octicon-link, 39 | .markdown-body h2:hover .anchor .octicon-link, 40 | .markdown-body h3:hover .anchor .octicon-link, 41 | .markdown-body h4:hover .anchor .octicon-link, 42 | .markdown-body h5:hover .anchor .octicon-link, 43 | .markdown-body h6:hover .anchor .octicon-link { 44 | visibility: visible; 45 | } 46 | 47 | .markdown-body h1:hover .anchor .octicon-link:before, 48 | .markdown-body h2:hover .anchor .octicon-link:before, 49 | .markdown-body h3:hover .anchor .octicon-link:before, 50 | .markdown-body h4:hover .anchor .octicon-link:before, 51 | .markdown-body h5:hover .anchor .octicon-link:before, 52 | .markdown-body h6:hover .anchor .octicon-link:before { 53 | width: 16px; 54 | height: 16px; 55 | content: ' '; 56 | display: inline-block; 57 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill-rule='evenodd' d='M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z'%3E%3C/path%3E%3C/svg%3E"); 58 | } 59 | 60 | .markdown-body { 61 | -ms-text-size-adjust: 100%; 62 | -webkit-text-size-adjust: 100%; 63 | line-height: 1.5; 64 | color: #24292e; 65 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; 66 | font-size: 16px; 67 | line-height: 1.5; 68 | word-wrap: break-word; 69 | } 70 | 71 | .markdown-body details { 72 | display: block; 73 | } 74 | 75 | .markdown-body summary { 76 | display: list-item; 77 | } 78 | 79 | .markdown-body a { 80 | background-color: initial; 81 | } 82 | 83 | .markdown-body a:active, 84 | .markdown-body a:hover { 85 | outline-width: 0; 86 | } 87 | 88 | .markdown-body strong { 89 | font-weight: inherit; 90 | font-weight: bolder; 91 | } 92 | 93 | .markdown-body h1 { 94 | font-size: 2em; 95 | margin: .67em 0; 96 | } 97 | 98 | .markdown-body img { 99 | border-style: none; 100 | } 101 | 102 | .markdown-body code, 103 | .markdown-body kbd, 104 | .markdown-body pre { 105 | font-family: monospace, monospace; 106 | font-size: 1em; 107 | } 108 | 109 | .markdown-body hr { 110 | box-sizing: initial; 111 | height: 0; 112 | overflow: visible; 113 | } 114 | 115 | .markdown-body input { 116 | font: inherit; 117 | margin: 0; 118 | } 119 | 120 | .markdown-body input { 121 | overflow: visible; 122 | } 123 | 124 | .markdown-body [type=checkbox] { 125 | box-sizing: border-box; 126 | padding: 0; 127 | } 128 | 129 | .markdown-body * { 130 | box-sizing: border-box; 131 | } 132 | 133 | .markdown-body input { 134 | font-family: inherit; 135 | font-size: inherit; 136 | line-height: inherit; 137 | } 138 | 139 | .markdown-body a { 140 | color: #0366d6; 141 | text-decoration: none; 142 | } 143 | 144 | .markdown-body a:hover { 145 | text-decoration: underline; 146 | } 147 | 148 | .markdown-body strong { 149 | font-weight: 600; 150 | } 151 | 152 | .markdown-body hr { 153 | height: 0; 154 | margin: 15px 0; 155 | overflow: hidden; 156 | background: transparent; 157 | border: 0; 158 | border-bottom: 1px solid #dfe2e5; 159 | } 160 | 161 | .markdown-body hr:after, 162 | .markdown-body hr:before { 163 | display: table; 164 | content: ""; 165 | } 166 | 167 | .markdown-body hr:after { 168 | clear: both; 169 | } 170 | 171 | .markdown-body table { 172 | border-spacing: 0; 173 | border-collapse: collapse; 174 | } 175 | 176 | .markdown-body td, 177 | .markdown-body th { 178 | padding: 0; 179 | } 180 | 181 | .markdown-body details summary { 182 | cursor: pointer; 183 | } 184 | 185 | .markdown-body kbd { 186 | display: inline-block; 187 | padding: 3px 5px; 188 | font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 189 | line-height: 10px; 190 | color: #444d56; 191 | vertical-align: middle; 192 | background-color: #fafbfc; 193 | border: 1px solid #d1d5da; 194 | border-radius: 3px; 195 | box-shadow: inset 0 -1px 0 #d1d5da; 196 | } 197 | 198 | .markdown-body h1, 199 | .markdown-body h2, 200 | .markdown-body h3, 201 | .markdown-body h4, 202 | .markdown-body h5, 203 | .markdown-body h6 { 204 | margin-top: 0; 205 | margin-bottom: 0; 206 | } 207 | 208 | .markdown-body h1 { 209 | font-size: 32px; 210 | } 211 | 212 | .markdown-body h1, 213 | .markdown-body h2 { 214 | font-weight: 600; 215 | } 216 | 217 | .markdown-body h2 { 218 | font-size: 24px; 219 | } 220 | 221 | .markdown-body h3 { 222 | font-size: 20px; 223 | } 224 | 225 | .markdown-body h3, 226 | .markdown-body h4 { 227 | font-weight: 600; 228 | } 229 | 230 | .markdown-body h4 { 231 | font-size: 16px; 232 | } 233 | 234 | .markdown-body h5 { 235 | font-size: 14px; 236 | } 237 | 238 | .markdown-body h5, 239 | .markdown-body h6 { 240 | font-weight: 600; 241 | } 242 | 243 | .markdown-body h6 { 244 | font-size: 12px; 245 | } 246 | 247 | .markdown-body p { 248 | margin-top: 0; 249 | margin-bottom: 10px; 250 | } 251 | 252 | .markdown-body blockquote { 253 | margin: 0; 254 | } 255 | 256 | .markdown-body ol, 257 | .markdown-body ul { 258 | padding-left: 0; 259 | margin-top: 0; 260 | margin-bottom: 0; 261 | } 262 | 263 | .markdown-body ol ol, 264 | .markdown-body ul ol { 265 | list-style-type: lower-roman; 266 | } 267 | 268 | .markdown-body ol ol ol, 269 | .markdown-body ol ul ol, 270 | .markdown-body ul ol ol, 271 | .markdown-body ul ul ol { 272 | list-style-type: lower-alpha; 273 | } 274 | 275 | .markdown-body dd { 276 | margin-left: 0; 277 | } 278 | 279 | .markdown-body code, 280 | .markdown-body pre { 281 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 282 | font-size: 12px; 283 | } 284 | 285 | .markdown-body pre { 286 | margin-top: 0; 287 | margin-bottom: 0; 288 | } 289 | 290 | .markdown-body input::-webkit-inner-spin-button, 291 | .markdown-body input::-webkit-outer-spin-button { 292 | margin: 0; 293 | -webkit-appearance: none; 294 | appearance: none; 295 | } 296 | 297 | .markdown-body :checked+.radio-label { 298 | position: relative; 299 | z-index: 1; 300 | border-color: #0366d6; 301 | } 302 | 303 | .markdown-body .border { 304 | border: 1px solid #e1e4e8 !important; 305 | } 306 | 307 | .markdown-body .border-0 { 308 | border: 0 !important; 309 | } 310 | 311 | .markdown-body .border-bottom { 312 | border-bottom: 1px solid #e1e4e8 !important; 313 | } 314 | 315 | .markdown-body .rounded-1 { 316 | border-radius: 3px !important; 317 | } 318 | 319 | .markdown-body .bg-white { 320 | background-color: #fff !important; 321 | } 322 | 323 | .markdown-body .bg-gray-light { 324 | background-color: #fafbfc !important; 325 | } 326 | 327 | .markdown-body .text-gray-light { 328 | color: #6a737d !important; 329 | } 330 | 331 | .markdown-body .mb-0 { 332 | margin-bottom: 0 !important; 333 | } 334 | 335 | .markdown-body .my-2 { 336 | margin-top: 8px !important; 337 | margin-bottom: 8px !important; 338 | } 339 | 340 | .markdown-body .pl-0 { 341 | padding-left: 0 !important; 342 | } 343 | 344 | .markdown-body .py-0 { 345 | padding-top: 0 !important; 346 | padding-bottom: 0 !important; 347 | } 348 | 349 | .markdown-body .pl-1 { 350 | padding-left: 4px !important; 351 | } 352 | 353 | .markdown-body .pl-2 { 354 | padding-left: 8px !important; 355 | } 356 | 357 | .markdown-body .py-2 { 358 | padding-top: 8px !important; 359 | padding-bottom: 8px !important; 360 | } 361 | 362 | .markdown-body .pl-3, 363 | .markdown-body .px-3 { 364 | padding-left: 16px !important; 365 | } 366 | 367 | .markdown-body .px-3 { 368 | padding-right: 16px !important; 369 | } 370 | 371 | .markdown-body .pl-4 { 372 | padding-left: 24px !important; 373 | } 374 | 375 | .markdown-body .pl-5 { 376 | padding-left: 32px !important; 377 | } 378 | 379 | .markdown-body .pl-6 { 380 | padding-left: 40px !important; 381 | } 382 | 383 | .markdown-body .f6 { 384 | font-size: 12px !important; 385 | } 386 | 387 | .markdown-body .lh-condensed { 388 | line-height: 1.25 !important; 389 | } 390 | 391 | .markdown-body .text-bold { 392 | font-weight: 600 !important; 393 | } 394 | 395 | .markdown-body .pl-c { 396 | color: #6a737d; 397 | } 398 | 399 | .markdown-body .pl-c1, 400 | .markdown-body .pl-s .pl-v { 401 | color: #005cc5; 402 | } 403 | 404 | .markdown-body .pl-e, 405 | .markdown-body .pl-en { 406 | color: #6f42c1; 407 | } 408 | 409 | .markdown-body .pl-s .pl-s1, 410 | .markdown-body .pl-smi { 411 | color: #24292e; 412 | } 413 | 414 | .markdown-body .pl-ent { 415 | color: #22863a; 416 | } 417 | 418 | .markdown-body .pl-k { 419 | color: #d73a49; 420 | } 421 | 422 | .markdown-body .pl-pds, 423 | .markdown-body .pl-s, 424 | .markdown-body .pl-s .pl-pse .pl-s1, 425 | .markdown-body .pl-sr, 426 | .markdown-body .pl-sr .pl-cce, 427 | .markdown-body .pl-sr .pl-sra, 428 | .markdown-body .pl-sr .pl-sre { 429 | color: #032f62; 430 | } 431 | 432 | .markdown-body .pl-smw, 433 | .markdown-body .pl-v { 434 | color: #e36209; 435 | } 436 | 437 | .markdown-body .pl-bu { 438 | color: #b31d28; 439 | } 440 | 441 | .markdown-body .pl-ii { 442 | color: #fafbfc; 443 | background-color: #b31d28; 444 | } 445 | 446 | .markdown-body .pl-c2 { 447 | color: #fafbfc; 448 | background-color: #d73a49; 449 | } 450 | 451 | .markdown-body .pl-c2:before { 452 | content: "^M"; 453 | } 454 | 455 | .markdown-body .pl-sr .pl-cce { 456 | font-weight: 700; 457 | color: #22863a; 458 | } 459 | 460 | .markdown-body .pl-ml { 461 | color: #735c0f; 462 | } 463 | 464 | .markdown-body .pl-mh, 465 | .markdown-body .pl-mh .pl-en, 466 | .markdown-body .pl-ms { 467 | font-weight: 700; 468 | color: #005cc5; 469 | } 470 | 471 | .markdown-body .pl-mi { 472 | font-style: italic; 473 | color: #24292e; 474 | } 475 | 476 | .markdown-body .pl-mb { 477 | font-weight: 700; 478 | color: #24292e; 479 | } 480 | 481 | .markdown-body .pl-md { 482 | color: #b31d28; 483 | background-color: #ffeef0; 484 | } 485 | 486 | .markdown-body .pl-mi1 { 487 | color: #22863a; 488 | background-color: #f0fff4; 489 | } 490 | 491 | .markdown-body .pl-mc { 492 | color: #e36209; 493 | background-color: #ffebda; 494 | } 495 | 496 | .markdown-body .pl-mi2 { 497 | color: #f6f8fa; 498 | background-color: #005cc5; 499 | } 500 | 501 | .markdown-body .pl-mdr { 502 | font-weight: 700; 503 | color: #6f42c1; 504 | } 505 | 506 | .markdown-body .pl-ba { 507 | color: #586069; 508 | } 509 | 510 | .markdown-body .pl-sg { 511 | color: #959da5; 512 | } 513 | 514 | .markdown-body .pl-corl { 515 | text-decoration: underline; 516 | color: #032f62; 517 | } 518 | 519 | .markdown-body .mb-0 { 520 | margin-bottom: 0 !important; 521 | } 522 | 523 | .markdown-body .my-2 { 524 | margin-bottom: 8px !important; 525 | } 526 | 527 | .markdown-body .my-2 { 528 | margin-top: 8px !important; 529 | } 530 | 531 | .markdown-body .pl-0 { 532 | padding-left: 0 !important; 533 | } 534 | 535 | .markdown-body .py-0 { 536 | padding-top: 0 !important; 537 | padding-bottom: 0 !important; 538 | } 539 | 540 | .markdown-body .pl-1 { 541 | padding-left: 4px !important; 542 | } 543 | 544 | .markdown-body .pl-2 { 545 | padding-left: 8px !important; 546 | } 547 | 548 | .markdown-body .py-2 { 549 | padding-top: 8px !important; 550 | padding-bottom: 8px !important; 551 | } 552 | 553 | .markdown-body .pl-3 { 554 | padding-left: 16px !important; 555 | } 556 | 557 | .markdown-body .pl-4 { 558 | padding-left: 24px !important; 559 | } 560 | 561 | .markdown-body .pl-5 { 562 | padding-left: 32px !important; 563 | } 564 | 565 | .markdown-body .pl-6 { 566 | padding-left: 40px !important; 567 | } 568 | 569 | .markdown-body .pl-7 { 570 | padding-left: 48px !important; 571 | } 572 | 573 | .markdown-body .pl-8 { 574 | padding-left: 64px !important; 575 | } 576 | 577 | .markdown-body .pl-9 { 578 | padding-left: 80px !important; 579 | } 580 | 581 | .markdown-body .pl-10 { 582 | padding-left: 96px !important; 583 | } 584 | 585 | .markdown-body .pl-11 { 586 | padding-left: 112px !important; 587 | } 588 | 589 | .markdown-body .pl-12 { 590 | padding-left: 128px !important; 591 | } 592 | 593 | .markdown-body hr { 594 | border-bottom-color: #eee; 595 | } 596 | 597 | .markdown-body kbd { 598 | display: inline-block; 599 | padding: 3px 5px; 600 | font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 601 | line-height: 10px; 602 | color: #444d56; 603 | vertical-align: middle; 604 | background-color: #fafbfc; 605 | border: 1px solid #d1d5da; 606 | border-radius: 3px; 607 | box-shadow: inset 0 -1px 0 #d1d5da; 608 | } 609 | 610 | .markdown-body:after, 611 | .markdown-body:before { 612 | display: table; 613 | content: ""; 614 | } 615 | 616 | .markdown-body:after { 617 | clear: both; 618 | } 619 | 620 | .markdown-body>:first-child { 621 | margin-top: 0 !important; 622 | } 623 | 624 | .markdown-body>:last-child { 625 | margin-bottom: 0 !important; 626 | } 627 | 628 | .markdown-body a:not([href]) { 629 | color: inherit; 630 | text-decoration: none; 631 | } 632 | 633 | .markdown-body blockquote, 634 | .markdown-body details, 635 | .markdown-body dl, 636 | .markdown-body ol, 637 | .markdown-body p, 638 | .markdown-body pre, 639 | .markdown-body table, 640 | .markdown-body ul { 641 | margin-top: 0; 642 | margin-bottom: 16px; 643 | } 644 | 645 | .markdown-body hr { 646 | height: .25em; 647 | padding: 0; 648 | margin: 24px 0; 649 | background-color: #e1e4e8; 650 | border: 0; 651 | } 652 | 653 | .markdown-body blockquote { 654 | padding: 0 1em; 655 | color: #6a737d; 656 | border-left: .25em solid #dfe2e5; 657 | } 658 | 659 | .markdown-body blockquote>:first-child { 660 | margin-top: 0; 661 | } 662 | 663 | .markdown-body blockquote>:last-child { 664 | margin-bottom: 0; 665 | } 666 | 667 | .markdown-body h1, 668 | .markdown-body h2, 669 | .markdown-body h3, 670 | .markdown-body h4, 671 | .markdown-body h5, 672 | .markdown-body h6 { 673 | margin-top: 24px; 674 | margin-bottom: 16px; 675 | font-weight: 600; 676 | line-height: 1.25; 677 | } 678 | 679 | .markdown-body h1 { 680 | font-size: 2em; 681 | } 682 | 683 | .markdown-body h1, 684 | .markdown-body h2 { 685 | padding-bottom: .3em; 686 | border-bottom: 1px solid #eaecef; 687 | } 688 | 689 | .markdown-body h2 { 690 | font-size: 1.5em; 691 | } 692 | 693 | .markdown-body h3 { 694 | font-size: 1.25em; 695 | } 696 | 697 | .markdown-body h4 { 698 | font-size: 1em; 699 | } 700 | 701 | .markdown-body h5 { 702 | font-size: .875em; 703 | } 704 | 705 | .markdown-body h6 { 706 | font-size: .85em; 707 | color: #6a737d; 708 | } 709 | 710 | .markdown-body ol, 711 | .markdown-body ul { 712 | padding-left: 2em; 713 | } 714 | 715 | .markdown-body ol ol, 716 | .markdown-body ol ul, 717 | .markdown-body ul ol, 718 | .markdown-body ul ul { 719 | margin-top: 0; 720 | margin-bottom: 0; 721 | } 722 | 723 | .markdown-body li { 724 | word-wrap: break-all; 725 | } 726 | 727 | .markdown-body li>p { 728 | margin-top: 16px; 729 | } 730 | 731 | .markdown-body li+li { 732 | margin-top: .25em; 733 | } 734 | 735 | .markdown-body dl { 736 | padding: 0; 737 | } 738 | 739 | .markdown-body dl dt { 740 | padding: 0; 741 | margin-top: 16px; 742 | font-size: 1em; 743 | font-style: italic; 744 | font-weight: 600; 745 | } 746 | 747 | .markdown-body dl dd { 748 | padding: 0 16px; 749 | margin-bottom: 16px; 750 | } 751 | 752 | .markdown-body table { 753 | display: block; 754 | width: 100%; 755 | overflow: auto; 756 | } 757 | 758 | .markdown-body table th { 759 | font-weight: 600; 760 | } 761 | 762 | .markdown-body table td, 763 | .markdown-body table th { 764 | padding: 6px 13px; 765 | border: 1px solid #dfe2e5; 766 | } 767 | 768 | .markdown-body table tr { 769 | background-color: #fff; 770 | border-top: 1px solid #c6cbd1; 771 | } 772 | 773 | .markdown-body table tr:nth-child(2n) { 774 | background-color: #f6f8fa; 775 | } 776 | 777 | .markdown-body img { 778 | max-width: 100%; 779 | box-sizing: initial; 780 | background-color: #fff; 781 | } 782 | 783 | .markdown-body img[align=right] { 784 | padding-left: 20px; 785 | } 786 | 787 | .markdown-body img[align=left] { 788 | padding-right: 20px; 789 | } 790 | 791 | .markdown-body code { 792 | padding: .2em .4em; 793 | margin: 0; 794 | font-size: 85%; 795 | background-color: rgba(27, 31, 35, .05); 796 | border-radius: 3px; 797 | } 798 | 799 | .markdown-body pre { 800 | word-wrap: normal; 801 | } 802 | 803 | .markdown-body pre>code { 804 | padding: 0; 805 | margin: 0; 806 | font-size: 100%; 807 | word-break: normal; 808 | white-space: pre; 809 | background: transparent; 810 | border: 0; 811 | } 812 | 813 | .markdown-body .highlight { 814 | margin-bottom: 16px; 815 | } 816 | 817 | .markdown-body .highlight pre { 818 | margin-bottom: 0; 819 | word-break: normal; 820 | } 821 | 822 | .markdown-body .highlight pre, 823 | .markdown-body pre { 824 | padding: 16px; 825 | overflow: auto; 826 | font-size: 85%; 827 | line-height: 1.45; 828 | background-color: #f6f8fa; 829 | border-radius: 3px; 830 | } 831 | 832 | .markdown-body pre code { 833 | display: inline; 834 | max-width: auto; 835 | padding: 0; 836 | margin: 0; 837 | overflow: visible; 838 | line-height: inherit; 839 | word-wrap: normal; 840 | background-color: initial; 841 | border: 0; 842 | } 843 | 844 | .markdown-body .commit-tease-sha { 845 | display: inline-block; 846 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 847 | font-size: 90%; 848 | color: #444d56; 849 | } 850 | 851 | .markdown-body .full-commit .btn-outline:not(:disabled):hover { 852 | color: #005cc5; 853 | border-color: #005cc5; 854 | } 855 | 856 | .markdown-body .blob-wrapper { 857 | overflow-x: auto; 858 | overflow-y: hidden; 859 | } 860 | 861 | .markdown-body .blob-wrapper-embedded { 862 | max-height: 240px; 863 | overflow-y: auto; 864 | } 865 | 866 | .markdown-body .blob-num { 867 | width: 1%; 868 | min-width: 50px; 869 | padding-right: 10px; 870 | padding-left: 10px; 871 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 872 | font-size: 12px; 873 | line-height: 20px; 874 | color: rgba(27, 31, 35, .3); 875 | text-align: right; 876 | white-space: nowrap; 877 | vertical-align: top; 878 | cursor: pointer; 879 | -webkit-user-select: none; 880 | -moz-user-select: none; 881 | -ms-user-select: none; 882 | user-select: none; 883 | } 884 | 885 | .markdown-body .blob-num:hover { 886 | color: rgba(27, 31, 35, .6); 887 | } 888 | 889 | .markdown-body .blob-num:before { 890 | content: attr(data-line-number); 891 | } 892 | 893 | .markdown-body .blob-code { 894 | position: relative; 895 | padding-right: 10px; 896 | padding-left: 10px; 897 | line-height: 20px; 898 | vertical-align: top; 899 | } 900 | 901 | .markdown-body .blob-code-inner { 902 | overflow: visible; 903 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 904 | font-size: 12px; 905 | color: #24292e; 906 | word-wrap: normal; 907 | white-space: pre; 908 | } 909 | 910 | .markdown-body .pl-token.active, 911 | .markdown-body .pl-token:hover { 912 | cursor: pointer; 913 | background: #ffea7f; 914 | } 915 | 916 | .markdown-body .tab-size[data-tab-size="1"] { 917 | -moz-tab-size: 1; 918 | tab-size: 1; 919 | } 920 | 921 | .markdown-body .tab-size[data-tab-size="2"] { 922 | -moz-tab-size: 2; 923 | tab-size: 2; 924 | } 925 | 926 | .markdown-body .tab-size[data-tab-size="3"] { 927 | -moz-tab-size: 3; 928 | tab-size: 3; 929 | } 930 | 931 | .markdown-body .tab-size[data-tab-size="4"] { 932 | -moz-tab-size: 4; 933 | tab-size: 4; 934 | } 935 | 936 | .markdown-body .tab-size[data-tab-size="5"] { 937 | -moz-tab-size: 5; 938 | tab-size: 5; 939 | } 940 | 941 | .markdown-body .tab-size[data-tab-size="6"] { 942 | -moz-tab-size: 6; 943 | tab-size: 6; 944 | } 945 | 946 | .markdown-body .tab-size[data-tab-size="7"] { 947 | -moz-tab-size: 7; 948 | tab-size: 7; 949 | } 950 | 951 | .markdown-body .tab-size[data-tab-size="8"] { 952 | -moz-tab-size: 8; 953 | tab-size: 8; 954 | } 955 | 956 | .markdown-body .tab-size[data-tab-size="9"] { 957 | -moz-tab-size: 9; 958 | tab-size: 9; 959 | } 960 | 961 | .markdown-body .tab-size[data-tab-size="10"] { 962 | -moz-tab-size: 10; 963 | tab-size: 10; 964 | } 965 | 966 | .markdown-body .tab-size[data-tab-size="11"] { 967 | -moz-tab-size: 11; 968 | tab-size: 11; 969 | } 970 | 971 | .markdown-body .tab-size[data-tab-size="12"] { 972 | -moz-tab-size: 12; 973 | tab-size: 12; 974 | } 975 | 976 | .markdown-body .task-list-item { 977 | list-style-type: none; 978 | } 979 | 980 | .markdown-body .task-list-item+.task-list-item { 981 | margin-top: 3px; 982 | } 983 | 984 | .markdown-body .task-list-item input { 985 | margin: 0 .2em .25em -1.6em; 986 | vertical-align: middle; 987 | } 988 | -------------------------------------------------------------------------------- /src/components/pages/Comments.jsx: -------------------------------------------------------------------------------- 1 | import { formatDistance } from 'date-fns'; 2 | import React from 'react'; 3 | import ReactMarkdown from 'react-markdown'; 4 | import { useQuery } from 'react-query'; 5 | 6 | export default function Comments({ issueNumber }) { 7 | const { 8 | isLoading, 9 | isSuccess, 10 | data: comments, 11 | } = useQuery(['comments', issueNumber], fetchComments); 12 | 13 | function fetchComments() { 14 | return fetch( 15 | `https://api.github.com/repos/facebook/create-react-app/issues/${issueNumber}/comments` 16 | ).then(response => response.json()); 17 | } 18 | 19 | return ( 20 | <> 21 | {isLoading &&
    Loading...
    } 22 | {isSuccess && ( 23 | <> 24 | {comments.map(comment => ( 25 |
    26 | 27 | avatar 32 | 33 |
    34 |
    35 | {comment.user.login}{' '} 36 | commented{' '} 37 | {formatDistance(new Date(comment.created_at), new Date(), { 38 | addSuffix: true, 39 | })} 40 |
    41 |
    42 | 43 |
    44 |
    45 |
    46 | ))} 47 | 48 | )} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/pages/Details.jsx: -------------------------------------------------------------------------------- 1 | import { formatDistance } from 'date-fns'; 2 | import React from 'react'; 3 | import ReactMarkdown from 'react-markdown'; 4 | import { useQuery } from 'react-query'; 5 | import { useParams } from 'react-router-dom'; 6 | import Comments from './Comments'; 7 | 8 | export default function Details() { 9 | const params = useParams(); 10 | 11 | const { 12 | isLoading, 13 | isSuccess, 14 | data: issue, 15 | } = useQuery(['issue', params.id], fetchIssue); 16 | 17 | function fetchIssue() { 18 | return fetch( 19 | `https://api.github.com/repos/facebook/create-react-app/issues/${params.id}` 20 | ).then(response => response.json()); 21 | } 22 | 23 | return ( 24 |
    25 | {isLoading &&
    Loading...
    } 26 | {isSuccess && ( 27 | <> 28 |

    29 | {issue.title} #{issue.number} 30 |

    31 |
    32 | {issue.user.login} opened this 33 | issue{' '} 34 | {formatDistance(new Date(issue.created_at), new Date(), { 35 | addSuffix: true, 36 | })} 37 |
    38 | 39 | )} 40 | 41 | {isSuccess && ( 42 |
    43 | 44 | avatar 45 | 46 |
    47 |
    48 | mdaj06 commented{' '} 49 | {formatDistance(new Date(issue.created_at), new Date(), { 50 | addSuffix: true, 51 | })} 52 |
    53 |
    54 | 55 |
    56 |
    57 |
    58 | )} 59 | 60 |
    61 | {isSuccess && } 62 |
    63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/pages/IconClosed.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IconClosed() { 4 | return ( 5 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/pages/IconOpen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function IconOpen() { 4 | return ( 5 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/pages/Issues.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useQuery } from 'react-query'; 3 | import IconOpen from './IconOpen'; 4 | import IconClosed from './IconClosed'; 5 | import { formatDistance } from 'date-fns'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | export default function Contact() { 9 | const [filter, setFilter] = useState('open'); 10 | 11 | const { 12 | isLoading, 13 | isSuccess, 14 | data: issues, 15 | } = useQuery(['issues', filter], fetchIssues); 16 | 17 | const { isSuccess: isSuccessIssuesOpen, data: issuesOpen } = useQuery( 18 | 'issuesOpen', 19 | fetchIssuesOpen 20 | ); 21 | 22 | const { isSuccess: isSuccessIssuesClosed, data: issuesClosed } = useQuery( 23 | 'issuesClosed', 24 | fetchIssuesClosed 25 | ); 26 | 27 | function fetchIssues() { 28 | return fetch( 29 | `https://api.github.com/repos/facebook/create-react-app/issues?per_page=10&state=${filter}` 30 | ).then(response => response.json()); 31 | } 32 | 33 | function fetchIssuesOpen() { 34 | return fetch( 35 | `https://api.github.com/search/issues?q=repo:facebook/create-react-app+type:issue+state:open&per_page=1` 36 | ).then(response => response.json()); 37 | } 38 | 39 | function fetchIssuesClosed() { 40 | return fetch( 41 | `https://api.github.com/search/issues?q=repo:facebook/create-react-app+type:issue+state:closed&per_page=1` 42 | ).then(response => response.json()); 43 | } 44 | 45 | return ( 46 |
    47 | {isLoading &&
    Loading...
    } 48 | 49 | {isSuccess && ( 50 |
    51 |
    52 | 53 | facebook / create-react-app 54 | 55 |
    56 | 64 | 72 |
    73 |
    74 |
    75 | {issues.map(issue => ( 76 |
    77 |
    78 | {issue.state === 'open' && } 79 | {issue.state === 'closed' && } 80 |
    81 | {issue.title} 82 |
    83 | #{issue.number} opened{' '} 84 | {formatDistance(new Date(issue.created_at), new Date(), { 85 | addSuffix: true, 86 | })}{' '} 87 | by {issue.user.login} 88 |
    89 |
    90 |
    91 | {issue.comments > 0 && ( 92 | 96 | 109 |
    {issue.comments}
    110 | 111 | )} 112 |
    113 | ))} 114 |
    115 |
    116 | )} 117 |
    118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /src/components/pages/NoMatch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NoMatch() { 4 | return
    404 Page Not Found!
    ; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/pages/api.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Reddit from '../api/Reddit'; 3 | import Joke from '../api/Joke'; 4 | 5 | export default function About() { 6 | const [redditVisible, setRedditVisible] = useState(false); 7 | const [jokeVisible, setJokeVisible] = useState(false); 8 | 9 | return ( 10 |
    11 |
    12 | 18 | 24 |
    25 |
    26 | {redditVisible && ( 27 |
    28 | 29 |
    30 | )} 31 | {jokeVisible && ( 32 |
    33 | 34 |
    35 | )} 36 |
    37 |
    38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/context/TodosContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const TodosContext = createContext(); 4 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | function useLocalStorage(key, initialValue) { 4 | const [value, setValue] = useState(() => { 5 | const item = localStorage.getItem(key); 6 | return item ? JSON.parse(item) : initialValue; 7 | }); 8 | 9 | useEffect(() => { 10 | localStorage.setItem(key, JSON.stringify(value)); 11 | }, [key, value]); 12 | return [value, setValue]; 13 | } 14 | 15 | export default useLocalStorage; 16 | -------------------------------------------------------------------------------- /src/hooks/useToggle.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | function useToggle(initialState = true) { 4 | const [visible, setVisible] = useState(initialState); 5 | 6 | function toggle() { 7 | setVisible(prevVisible => !prevVisible); 8 | } 9 | return [visible, toggle]; 10 | } 11 | 12 | export default useToggle; 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import Root from './components/Root'; 5 | import { QueryClient, QueryClientProvider } from 'react-query'; 6 | import { ReactQueryDevtools } from 'react-query/devtools'; 7 | 8 | const root = document.getElementById('root'); 9 | const queryClient = new QueryClient(); 10 | createRoot(root).render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------