├── .gitignore ├── .netlify └── state.json ├── README.md ├── demo ├── course.png └── habit.png ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── app.css ├── app.jsx ├── components │ ├── habit.jsx │ ├── habitAddForm.jsx │ ├── habits.jsx │ ├── navbar.jsx │ └── simpleHabit.jsx ├── index.css └── index.js └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "701a08b4-0140-44ae-83a2-59af42bcf109" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Course](/demo/course.png) 3 | 4 | # Habit Tracker 5 | 6 | 이 프로젝트는 [드림코딩 아카데미](http://academy.dream-coding.com/)에서 진행중인 [리액트 기본강의 & 실전 프로젝트 3개](https://academy.dream-coding.com/courses/react-basic) (유튜브 클론 코딩과 실시간 데이터베이스 저장 명함 만들기 웹앱을 통해 프론트엔드 완성)강의에서 **리액트 개념 정리를 위해 쓰인 예제 프로그램** 입니다. 7 | 8 | ![Habit Tracker](/demo/habit.png) 9 | -------------------------------------------------------------------------------- /demo/course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dream-coding-academy/react_basic_habit_tracker/0a9d6afc4365b6ca186ce35e32c2e6fb5d12b4d7/demo/course.png -------------------------------------------------------------------------------- /demo/habit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dream-coding-academy/react_basic_habit_tracker/0a9d6afc4365b6ca186ce35e32c2e6fb5d12b4d7/demo/habit.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-free": "^5.14.0", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "gh-pages": "^3.1.0", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-scripts": "3.4.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "predeploy": "npm run build", 21 | "deploy": "gh-pages -d build" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dream-coding-academy/react_basic_habit_tracker/0a9d6afc4365b6ca186ce35e32c2e6fb5d12b4d7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | App 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | button { 6 | outline: none; 7 | border: 0; 8 | cursor: pointer; 9 | } 10 | 11 | .navbar { 12 | font-size: 2rem; 13 | padding: 1em; 14 | background-color: blanchedalmond; 15 | } 16 | 17 | .navbar-logo { 18 | color: green; 19 | margin-right: 0.5em; 20 | } 21 | 22 | .navbar-count { 23 | display: inline-block; 24 | text-align: center; 25 | font-size: 2rem; 26 | width: 2.5rem; 27 | height: 2.5rem; 28 | line-height: 2.5rem; 29 | margin-left: 0.2em; 30 | background-color: green; 31 | color: white; 32 | border-radius: 50%; 33 | } 34 | 35 | .habits { 36 | padding: 0.5em; 37 | padding-top: 2em; 38 | } 39 | 40 | .add-input { 41 | font-size: 2rem; 42 | } 43 | 44 | .add-input { 45 | margin-right: 0.2em; 46 | } 47 | 48 | .add-button { 49 | height: 100%; 50 | font-size: 1.5rem; 51 | padding: 0.3em 0.5em; 52 | background-color: green; 53 | color: white; 54 | } 55 | 56 | .habit { 57 | font-size: 2.5rem; 58 | padding: 0.5em; 59 | list-style: none; 60 | } 61 | 62 | .habit-count { 63 | display: inline-block; 64 | text-align: center; 65 | font-size: 2rem; 66 | width: 2.5rem; 67 | height: 2.5rem; 68 | line-height: 2.5rem; 69 | margin-left: 0.2em; 70 | margin-right: 0.5em; 71 | background-color: darkseagreen; 72 | color: white; 73 | border-radius: 50%; 74 | } 75 | 76 | .habit-button { 77 | font-size: 2.5rem; 78 | margin: 0 0.1em; 79 | background-color: transparent; 80 | } 81 | 82 | .habit-button:hover { 83 | opacity: 0.8; 84 | } 85 | 86 | .habit-increase, 87 | .habit-decrease { 88 | color: goldenrod; 89 | } 90 | 91 | .habit-delete { 92 | font-size: 2rem; 93 | color: darkred; 94 | } 95 | 96 | .habits-reset { 97 | font-size: 1.5rem; 98 | padding: 0.2em 1em; 99 | background-color: green; 100 | color: white; 101 | } 102 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useCallback } from 'react'; 3 | import { useState } from 'react'; 4 | import './app.css'; 5 | import Habits from './components/habits'; 6 | import Navbar from './components/navbar'; 7 | 8 | const App = () => { 9 | const [habits, setHabits] = useState([ 10 | { id: 1, name: 'Reading', count: 0 }, 11 | { id: 2, name: 'Running', count: 0 }, 12 | { id: 3, name: 'Coding', count: 0 }, 13 | ]); 14 | 15 | const handleIncrement = useCallback(habit => { 16 | setHabits(habits => 17 | habits.map(item => { 18 | if (item.id === habit.id) { 19 | return { ...habit, count: habit.count + 1 }; 20 | } 21 | return item; 22 | }) 23 | ); 24 | }, []); 25 | 26 | const handleDecrement = useCallback(habit => { 27 | setHabits(habits => 28 | habits.map(item => { 29 | if (item.id === habit.id) { 30 | const count = habit.count - 1; 31 | return { ...habit, count: count < 0 ? 0 : count }; 32 | } 33 | return item; 34 | }) 35 | ); 36 | }, []); 37 | 38 | const handleDelete = useCallback(habit => { 39 | setHabits(habits => habits.filter(item => item.id !== habit.id)); 40 | }, []); 41 | 42 | const handleAdd = useCallback(name => { 43 | setHabits(habits => [...habits, { id: Date.now(), name, count: 0 }]); 44 | }, []); 45 | 46 | const handleReset = useCallback(() => { 47 | setHabits(habits => 48 | habits.map(habit => { 49 | if (habit.count !== 0) { 50 | return { ...habit, count: 0 }; 51 | } 52 | return habit; 53 | }) 54 | ); 55 | }, []); 56 | 57 | return ( 58 | <> 59 | item.count > 0).length} /> 60 | 68 | 69 | ); 70 | }; 71 | 72 | export default App; 73 | -------------------------------------------------------------------------------- /src/components/habit.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | const Habit = memo(({ habit, onIncrement, onDecrement, onDelete }) => { 4 | const handleIncrement = () => { 5 | onIncrement(habit); 6 | }; 7 | 8 | const handleDecrement = () => { 9 | onDecrement(habit); 10 | }; 11 | 12 | const handleDelete = () => { 13 | onDelete(habit); 14 | }; 15 | 16 | return ( 17 |
  • 18 | {habit.name} 19 | {habit.count} 20 | 23 | 26 | 29 |
  • 30 | ); 31 | }); 32 | 33 | export default Habit; 34 | -------------------------------------------------------------------------------- /src/components/habitAddForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | const HabitAddForm = memo(props => { 4 | const formRef = React.createRef(); 5 | const inputRef = React.createRef(); 6 | 7 | const onSubmit = event => { 8 | event.preventDefault(); 9 | const name = inputRef.current.value; 10 | name && props.onAdd(name); 11 | formRef.current.reset(); 12 | }; 13 | 14 | return ( 15 |
    16 | 22 | 23 |
    24 | ); 25 | }); 26 | 27 | export default HabitAddForm; 28 | -------------------------------------------------------------------------------- /src/components/habits.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Habit from './habit'; 3 | import HabitAddForm from './habitAddForm'; 4 | 5 | const Habits = ({ habits, onIncrement, onDecrement, onDelete, onAdd, onReset }) => { 6 | return ( 7 |
    8 | 9 |
      10 | {habits.map(habit => ( 11 | 18 | ))} 19 |
    20 | 23 |
    24 | ); 25 | }; 26 | 27 | export default Habits; 28 | -------------------------------------------------------------------------------- /src/components/navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | class Navbar extends PureComponent { 4 | render() { 5 | return ( 6 | 11 | ); 12 | } 13 | } 14 | 15 | export default Navbar; 16 | -------------------------------------------------------------------------------- /src/components/simpleHabit.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useCallback, useEffect } from 'react'; 2 | 3 | const SimpleHabit = () => { 4 | const [count, setCount] = useState(0); 5 | const spanRef = useRef(); 6 | 7 | const handleIncrement = useCallback(() => { 8 | setCount(count + 1); 9 | }); 10 | 11 | useEffect(() => { 12 | console.log(`mounted & updated!: ${count}`); 13 | }, [count]); 14 | return ( 15 |
  • 16 | 17 | Reading 18 | 19 | {count} 20 | 23 |
  • 24 | ); 25 | }; 26 | 27 | export default SimpleHabit; 28 | -------------------------------------------------------------------------------- /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 ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './app'; 5 | import '@fortawesome/fontawesome-free/js/all.js'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | --------------------------------------------------------------------------------