├── .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 | 
3 |
4 | # Habit Tracker
5 |
6 | 이 프로젝트는 [드림코딩 아카데미](http://academy.dream-coding.com/)에서 진행중인 [리액트 기본강의 & 실전 프로젝트 3개](https://academy.dream-coding.com/courses/react-basic) (유튜브 클론 코딩과 실시간 데이터베이스 저장 명함 만들기 웹앱을 통해 프론트엔드 완성)강의에서 **리액트 개념 정리를 위해 쓰인 예제 프로그램** 입니다.
7 |
8 | 
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 |
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 |
--------------------------------------------------------------------------------