diff --git a/.gitignore b/.gitignore
index 4d29575..601ba11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,9 @@
# production
/build
+#
+*.swp
+
# misc
.DS_Store
.env.local
diff --git a/README.md b/README.md
index 9d9614c..acd25e6 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+## Result Image
+
+
+
## Available Scripts
In the project directory, you can run:
diff --git a/backend/todo/migrations/0003_auto_20191009_1311.py b/backend/todo/migrations/0003_auto_20191009_1311.py
new file mode 100644
index 0000000..a517013
--- /dev/null
+++ b/backend/todo/migrations/0003_auto_20191009_1311.py
@@ -0,0 +1,28 @@
+# Generated by Django 2.2.5 on 2019-10-09 13:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('todo', '0002_todo_done'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='todo',
+ name='date',
+ field=models.IntegerField(default=1),
+ ),
+ migrations.AddField(
+ model_name='todo',
+ name='month',
+ field=models.IntegerField(default=1),
+ ),
+ migrations.AddField(
+ model_name='todo',
+ name='year',
+ field=models.IntegerField(default=2019),
+ ),
+ ]
diff --git a/backend/todo/models.py b/backend/todo/models.py
index 46fcb02..e999097 100644
--- a/backend/todo/models.py
+++ b/backend/todo/models.py
@@ -4,3 +4,6 @@ class Todo(models.Model):
title = models.CharField(max_length=120)
content = models.TextField()
done = models.BooleanField(default=False)
+ year = models.IntegerField(default=2019)
+ month = models.IntegerField(default=1)
+ date = models.IntegerField(default=1)
diff --git a/backend/todo/views.py b/backend/todo/views.py
index 598af14..d40f339 100644
--- a/backend/todo/views.py
+++ b/backend/todo/views.py
@@ -20,6 +20,9 @@ def index(request, id=None):
'id': todo.id,
'title': todo.title,
'content': todo.content,
+ 'year': todo.year,
+ 'month': todo.month,
+ 'date': todo.date,
'done': todo.done,
}
return JsonResponse(response_dict, safe=False)
@@ -30,14 +33,20 @@ def index(request, id=None):
body = request.body.decode()
title = json.loads(body)['title']
content = json.loads(body)['content']
+ dueDate = json.loads(body)['dueDate']
except (KeyError, JSONDecodeError) as e:
return HttpResponseBadRequest()
- todo = Todo(title=title, content=content, done=False)
+ todo = Todo(title=title, content=content,
+ year=dueDate['year'], month=dueDate['month'],
+ date=dueDate['date'], done=False)
todo.save()
response_dict = {
'id': todo.id,
'title': todo.title,
'content': todo.content,
+ 'year': todo.year,
+ 'month': todo.month,
+ 'date': todo.date,
'done': todo.done,
}
return HttpResponse(json.dumps(response_dict), status=201)
diff --git a/package.json b/package.json
index 793dc98..aed177e 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"react-router-dom": "^5.0.1",
"react-scripts": "3.1.1",
"redux": "^4.0.4",
- "redux-thunk": "^2.3.0"
+ "redux-thunk": "^2.3.0",
+ "semantic-ui-react": "^0.88.1"
},
"scripts": {
"start": "react-scripts start",
diff --git a/result.png b/result.png
new file mode 100644
index 0000000..2338524
Binary files /dev/null and b/result.png differ
diff --git a/src/App.js b/src/App.js
index a6e850e..2b94fe0 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,6 +2,7 @@ import React from 'react';
import './App.css';
import TodoList from './containers/TodoList/TodoList';
+import TodoCalendar from './containers/TodoCalendar/TodoCalendar';
import RealDetail from './containers/TodoList/RealDetail/RealDetail';
import NewTodo from './containers/TodoList/NewTodo/NewTodo';
@@ -14,12 +15,13 @@ function App(props) {
} />
+
Not Found
} />
-
+
);
}
diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css
new file mode 100644
index 0000000..efc1c9a
--- /dev/null
+++ b/src/components/Calendar/Calendar.css
@@ -0,0 +1,20 @@
+.cell .date {
+ font-size: 20px;
+}
+
+.todoTitle {
+ cursor: pointer;
+}
+
+.notdone {
+ color: blue;
+}
+
+.done {
+ text-decoration: line-through;
+ color: #adb5bd;
+}
+
+.sunday {
+ color: red;
+}
diff --git a/src/components/Calendar/Calendar.js b/src/components/Calendar/Calendar.js
new file mode 100644
index 0000000..a9a7c6d
--- /dev/null
+++ b/src/components/Calendar/Calendar.js
@@ -0,0 +1,87 @@
+import React, { Component } from 'react';
+import { Table } from 'semantic-ui-react'
+
+import './Calendar.css';
+
+const CALENDAR_HEADER = (
+
+
+ Sun
+ Mon
+ Tue
+ Wed
+ Thu
+ Fri
+ Sat
+
+
+);
+
+const renderCalenderBody = (dates, todos, clickDone) => {
+ let i = 0;
+ const rows = [];
+ for (let week=0; week<5; week++){
+ let day = 0; // Sunday
+
+ let row = [];
+ for (let day=0; day<7; day++) {
+ const date = dates[i];
+ if (date !== undefined && day === date.getDay()) {
+ row.push(
+
+ {date.getDate()}
+ {
+ todos.filter(todo => {
+ return todo.year === date.getFullYear() &&
+ todo.month === date.getMonth() &&
+ todo.date === date.getDate();
+ }).map(todo => {
+ return (
+ clickDone(todo.id)}>
+ {todo.title}
+
+ )
+ })
+ }
+
+ )
+ i++;
+ } else {
+ row.push( )
+ }
+ }
+ rows.push(row);
+ }
+
+ return (
+
+ {rows.map((row, i) => ({row}))}
+
+ );
+}
+
+const renderCalendar = (dates, todos, clickDone) => (
+
+ {CALENDAR_HEADER}
+ {renderCalenderBody(dates, todos, clickDone)}
+
+)
+
+const Calendar = (props) => {
+ const dates = [];
+ const year = props.year;
+ const month = props.month - 1;
+ let date = 1;
+ let maxDate = (new Date(year, month + 1, 0)).getDate();
+
+ for (let date=1; date<=maxDate; date++) {
+ dates.push(new Date(year, month, date));
+ }
+
+ return renderCalendar(dates, props.todos, props.clickDone);
+}
+
+export default Calendar
diff --git a/src/components/Calendar/Calendar.test.js b/src/components/Calendar/Calendar.test.js
new file mode 100644
index 0000000..f64dd24
--- /dev/null
+++ b/src/components/Calendar/Calendar.test.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { mount, shallow } from 'enzyme';
+import Calendar from './Calendar';
+import { getMockStore } from '../../test-utils/mocks';
+import * as actionCreators from '../../store/actions/todo';
+
+const stubInitialState = {
+ todos: [
+ {id: 1, title: 'TODO_TEST_TITLE_1', done: true, year: 2020, month: 1, date: 1},
+ {id: 2, title: 'TODO_TEST_TITLE_2', done: false, year: 2020, month: 1, date: 31},
+ {id: 3, title: 'TODO_TEST_TITLE_3', done: false, year: 2020, month: 1, date: 27},
+ ],
+ selectedTodo: null,
+};
+
+const mockStore = getMockStore(stubInitialState);
+
+describe('', () => {
+ let calendar, spyToggleTodo, spyGetTodos;
+
+ beforeEach(() => {
+ spyToggleTodo = jest.spyOn(actionCreators, 'toggleTodo')
+ .mockImplementation(id => { return dispatch => {}; });
+ calendar = (
+
+ );
+ spyGetTodos = jest.spyOn(actionCreators, 'getTodos')
+ .mockImplementation(() => { return dispatch => {}; });
+ })
+
+ it('should render without errors', () => {
+ const component = shallow();
+ const wrapper = component.find('.Calendar');
+ });
+
+ it('should render Calendar', () => {
+ const component = mount(calendar);
+ let wrapper = component.find('.sun').at(0);
+ expect(wrapper.text()).toBe('Sun')
+ wrapper = component.find('.mon').at(0);
+ expect(wrapper.text()).toBe('Mon')
+ wrapper = component.find('.tue').at(0);
+ expect(wrapper.text()).toBe('Tue')
+ wrapper = component.find('.wed').at(0);
+ expect(wrapper.text()).toBe('Wed')
+ wrapper = component.find('.thu').at(0);
+ expect(wrapper.text()).toBe('Thu')
+ wrapper = component.find('.fri').at(0);
+ expect(wrapper.text()).toBe('Fri')
+ wrapper = component.find('.sat').at(0);
+ expect(wrapper.text()).toBe('Sat')
+ });
+});
diff --git a/src/components/Todo/Todo.css b/src/components/Todo/Todo.css
index f2105b8..277d467 100644
--- a/src/components/Todo/Todo.css
+++ b/src/components/Todo/Todo.css
@@ -28,4 +28,10 @@
margin-left: 1rem;
color: orange;
font-weight: 800;
-}
\ No newline at end of file
+}
+
+.Todo .due {
+ flex: 1;
+ text-align: left;
+ word-break: break-all;
+}
diff --git a/src/components/Todo/Todo.js b/src/components/Todo/Todo.js
index 1755a8d..4f4105c 100644
--- a/src/components/Todo/Todo.js
+++ b/src/components/Todo/Todo.js
@@ -12,6 +12,7 @@ const Todo = (props) => {
{props.title}
{props.done && ✓
}
+ due: {props.year}.{props.month+1}.{props.date}
diff --git a/src/containers/TodoCalendar/TodoCalendar.css b/src/containers/TodoCalendar/TodoCalendar.css
new file mode 100644
index 0000000..ef70190
--- /dev/null
+++ b/src/containers/TodoCalendar/TodoCalendar.css
@@ -0,0 +1,10 @@
+.link{
+ text-align: left;
+}
+
+.header {
+ text-align: left;
+ font-size: 40px;
+ margin-left: 10rem;
+ font-weight: bold;
+}
diff --git a/src/containers/TodoCalendar/TodoCalendar.js b/src/containers/TodoCalendar/TodoCalendar.js
new file mode 100644
index 0000000..91b24ca
--- /dev/null
+++ b/src/containers/TodoCalendar/TodoCalendar.js
@@ -0,0 +1,70 @@
+import React, { Component } from 'react';
+
+import { NavLink } from 'react-router-dom';
+
+import { connect } from 'react-redux';
+import { withRouter } from 'react-router';
+import Calendar from '../../components/Calendar/Calendar';
+
+import * as actionCreators from '../../store/actions/index';
+
+import './TodoCalendar.css';
+
+class TodoCalendar extends Component {
+ state = {
+ year: 2019,
+ month: 10,
+ }
+ componentDidMount() {
+ this.props.onGetAll();
+ }
+
+ handleClickPrev = () => {
+ this.setState({
+ year: this.state.month === 1 ? this.state.year - 1 : this.state.year,
+ month: this.state.month === 1 ? 12 : this.state.month - 1
+ })
+ }
+
+ handleClickNext = () => {
+ this.setState({
+ year: this.state.month === 12 ? this.state.year + 1 : this.state.year,
+ month: this.state.month === 12 ? 1 : this.state.month + 1
+ })
+ }
+
+ render() {
+ return (
+
+
See TodoList
+
+
+ {this.state.year}.{this.state.month}
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ storedTodos: state.td.todos,
+ };
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onToggleTodo: (id) =>
+ dispatch(actionCreators.toggleTodo(id)),
+ onGetAll: () =>
+ dispatch(actionCreators.getTodos())
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TodoCalendar));
diff --git a/src/containers/TodoCalendar/TodoCalendar.test.js b/src/containers/TodoCalendar/TodoCalendar.test.js
new file mode 100644
index 0000000..f50050c
--- /dev/null
+++ b/src/containers/TodoCalendar/TodoCalendar.test.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import { connectRouter, ConnectedRouter } from 'connected-react-router';
+import { Route, Redirect, Switch } from 'react-router-dom';
+
+import TodoCalendar from './TodoCalendar';
+import { getMockStore } from '../../test-utils/mocks';
+import { history } from '../../store/store';
+import * as actionCreators from '../../store/actions/todo';
+
+const stubInitialState = {
+ todos: [
+ {id: 1, title: 'TODO_TEST_TITLE_1', done: true, year: 2020, month: 1, date: 1},
+ {id: 2, title: 'TODO_TEST_TITLE_2', done: false, year: 2020, month: 1, date: 31},
+ {id: 3, title: 'TODO_TEST_TITLE_3', done: false, year: 2020, month: 1, date: 27},
+ ],
+ selectedTodo: null,
+};
+
+const mockStore = getMockStore(stubInitialState);
+
+describe('', () => {
+ let todoCalendar, spyGetTodos;
+
+ beforeEach(() => {
+ todoCalendar = (
+
+
+
+
+
+
+
+ );
+ spyGetTodos = jest.spyOn(actionCreators, 'getTodos')
+ .mockImplementation(() => { return dispatch => {}; });
+ })
+
+ it('should render Calendar', () => {
+ const component = mount(todoCalendar);
+ expect(spyGetTodos).toBeCalledTimes(1);
+ });
+
+ it(`should decrement month on 'handleClickPrev'`, () => {
+ const component = mount(todoCalendar);
+ const wrapper = component.find('button').at(0);
+ wrapper.simulate('click');
+ expect(component.find('.header').text()).toBe(" prev month 2019.9 next month ");
+ });
+
+ it(`should change year and month on 'handleClickPrev'`, () => {
+ const component = mount(todoCalendar);
+ const wrapper = component.find('button').at(0);
+ for(let i=0; i<10; i++) wrapper.simulate('click');
+ expect(component.find('.header').text()).toBe(" prev month 2018.12 next month ");
+ });
+
+ it(`should increment month on 'handleClickNext'`, () => {
+ const component = mount(todoCalendar);
+ const wrapper = component.find('button').at(1);
+ wrapper.simulate('click');
+ expect(component.find('.header').text()).toBe(" prev month 2019.11 next month ");
+ });
+
+ it(`should change year and month on 'handleClickNext'`, () => {
+ const component = mount(todoCalendar);
+ const wrapper = component.find('button').at(1);
+ for(let i=0; i<3; i++) wrapper.simulate('click');
+ expect(component.find('.header').text()).toBe(" prev month 2020.1 next month ");
+ });
+});
+
diff --git a/src/containers/TodoList/NewTodo/NewTodo.js b/src/containers/TodoList/NewTodo/NewTodo.js
index 536cd50..63eafe8 100644
--- a/src/containers/TodoList/NewTodo/NewTodo.js
+++ b/src/containers/TodoList/NewTodo/NewTodo.js
@@ -10,10 +10,27 @@ class NewTodo extends Component {
state = {
title: '',
content: '',
+ dueDate: {
+ year: '',
+ month: '',
+ date: '',
+ },
+ }
+
+ componentDidMount() {
+ const now = new Date();
+ this.setState({
+ ...this.state,
+ dueDate: {
+ year: now.getFullYear(),
+ month: now.getMonth() + 1,
+ date: now.getDate(),
+ },
+ })
}
postTodoHandler = () => {
- this.props.onStoreTodo(this.state.title, this.state.content);
+ this.props.onStoreTodo(this.state.title, this.state.content, this.state.dueDate);
}
render() {
@@ -22,6 +39,7 @@ class NewTodo extends Component {
Add a New Todo!
this.setState({ title: event.target.value })}
@@ -31,6 +49,31 @@ class NewTodo extends Component {
onChange={(event) => this.setState({ content: event.target.value })}
>
+
+ year this.setState({
+ dueDate: {...this.state.dueDate, year: event.target.value }
+ })}
+ >
+ month this.setState({
+ dueDate: {...this.state.dueDate, month: event.target.value }
+ })}
+ >
+ date this.setState({
+ dueDate: {...this.state.dueDate, date: event.target.value }
+ })}
+ >
);
@@ -39,9 +82,9 @@ class NewTodo extends Component {
const mapDispatchToProps = dispatch => {
return {
- onStoreTodo: (title, content) =>
- dispatch(actionCreators.postTodo({ title: title, content: content })),
+ onStoreTodo: (title, content, dueDate) =>
+ dispatch(actionCreators.postTodo({ title: title, content: content, dueDate: dueDate})),
}
};
-export default connect(null, mapDispatchToProps)(NewTodo);
\ No newline at end of file
+export default connect(null, mapDispatchToProps)(NewTodo);
diff --git a/src/containers/TodoList/NewTodo/NewTodo.test.js b/src/containers/TodoList/NewTodo/NewTodo.test.js
index 5696dc7..0ee073c 100644
--- a/src/containers/TodoList/NewTodo/NewTodo.test.js
+++ b/src/containers/TodoList/NewTodo/NewTodo.test.js
@@ -53,9 +53,24 @@ describe('', () => {
it(`should set state properly on title input`, () => {
const title = 'TEST_TITLE'
const component = mount(newTodo);
- const wrapper = component.find('input');
+ let wrapper = component.find('.title');
+ wrapper.simulate('change', { target: { value: title } });
+ let newTodoInstance = component.find(NewTodo.WrappedComponent).instance();
+ expect(newTodoInstance.state.title).toEqual(title);
+ expect(newTodoInstance.state.content).toEqual('');
+
+ wrapper = component.find('.year');
+ wrapper.simulate('change', { target: { value: title } });
+ expect(newTodoInstance.state.title).toEqual(title);
+ expect(newTodoInstance.state.content).toEqual('');
+
+ wrapper = component.find('.month');
+ wrapper.simulate('change', { target: { value: title } });
+ expect(newTodoInstance.state.title).toEqual(title);
+ expect(newTodoInstance.state.content).toEqual('');
+
+ wrapper = component.find('.date');
wrapper.simulate('change', { target: { value: title } });
- const newTodoInstance = component.find(NewTodo.WrappedComponent).instance();
expect(newTodoInstance.state.title).toEqual(title);
expect(newTodoInstance.state.content).toEqual('');
});
diff --git a/src/containers/TodoList/TodoList.js b/src/containers/TodoList/TodoList.js
index e53fae3..8a5a2b2 100644
--- a/src/containers/TodoList/TodoList.js
+++ b/src/containers/TodoList/TodoList.js
@@ -27,6 +27,9 @@ class TodoList extends Component {
key={td.id}
title={td.title}
done={td.done}
+ year={td.year}
+ month={td.month}
+ date={td.date}
clickDetail={() => this.clickTodoHandler(td)}
clickDone={() => this.props.onToggleTodo(td.id)}
clickDelete={() => this.props.onDeleteTodo(td.id)}
@@ -35,12 +38,15 @@ class TodoList extends Component {
});
return (
-
-
- {this.props.title}
+
+
See Calendar
+
+
+ {this.props.title}
+
+ {todos}
+
New Todo
- {todos}
-
New Todo
)
}
@@ -63,4 +69,4 @@ const mapDispatchToProps = dispatch => {
}
}
-export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TodoList));
\ No newline at end of file
+export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TodoList));
diff --git a/src/store/actions/todo.js b/src/store/actions/todo.js
index 191fb88..9d32227 100644
--- a/src/store/actions/todo.js
+++ b/src/store/actions/todo.js
@@ -33,12 +33,24 @@ export const postTodo_ = (td) => {
id: td.id,
title: td.title,
content: td.content,
+ dueDate: {
+ year: td.year,
+ month: td.month,
+ date: td.date,
+ }
};
};
export const postTodo = (td) => {
+ const todo = {
+ ...td,
+ dueDate: {
+ ...td.dueDate,
+ month: td.dueDate.month - 1,
+ },
+ }
return (dispatch) => {
- return axios.post('/api/todo/', td)
+ return axios.post('/api/todo/', todo)
.then(res => {
dispatch(postTodo_(res.data));
dispatch(push('/todos/'));
diff --git a/src/store/actions/todo.test.js b/src/store/actions/todo.test.js
index 390a917..5fbd022 100644
--- a/src/store/actions/todo.test.js
+++ b/src/store/actions/todo.test.js
@@ -7,7 +7,12 @@ import store from '../store';
const stubTodo = {
id: 0,
title: 'title 1',
- content: 'content 1'
+ content: 'content 1',
+ dueDate: {
+ year: 2020,
+ month: 10,
+ date: 8,
+ }
};
describe('ActionCreators', () => {
@@ -97,7 +102,7 @@ describe('ActionCreators', () => {
});
})
- store.dispatch(actionCreators.postTodo()).then(() => {
+ store.dispatch(actionCreators.postTodo(stubTodo)).then(() => {
expect(spy).toHaveBeenCalledTimes(1);
done();
});
diff --git a/src/store/reducers/todo.js b/src/store/reducers/todo.js
index 9c048d5..243b060 100644
--- a/src/store/reducers/todo.js
+++ b/src/store/reducers/todo.js
@@ -13,6 +13,9 @@ const reducer = (state = initialState, action) => {
id: action.id,
title: action.title,
content: action.content,
+ year: action.year,
+ month: action.month,
+ date: action.date,
done: action.done,
};
return { ...state, todos: state.todos.concat(newTodo) };
@@ -40,4 +43,4 @@ const reducer = (state = initialState, action) => {
return state;
};
-export default reducer;
\ No newline at end of file
+export default reducer;