Category: Clone

Trello 클론 프로젝트 만들기 - 1

시작하기

지난 포스트에 이어서 trello 컴포넌트에 기능을 마저 추가하도록 하겠습니다.

Board

보드 생성하는 리듀서 및 컴포넌트 기능을 추가해보도록 하겠습니다.

Board 리듀서

store/reducers/board.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import produce from 'immer';
import { action, ActionType, createReducer } from 'typesafe-actions';
import uuid from 'uuid';

export const CREATE_BOARD = "CREATE_BOARD";

export const createBoard = (title: string) => {
const id: string = uuid.v4();
return action(CREATE_BOARD, { id, title });
}

const actions = {
createBoard
};

export { actions };

export interface BoardType {
id: string;
title: string;
}

export interface BoardState {
boards: BoardType[];
}

export type BoardActions = ActionType<typeof actions>;

const initialState: BoardState = {
boards: []
};

export default createReducer<BoardState, BoardActions>(initialState, {
[CREATE_BOARD]: (state, action) =>
produce(state, draft => {
draft.boards = [...state.boards, { id: action.payload.id, title: action.payload.title }];
})
});
store/reducers/index.tsx
1
2
3
4
5
6
7
import { combineReducers } from "redux";
import board from './board';

const rootReducer = combineReducers({ board });

export type RootState = ReturnType<typeof rootReducer>
export default rootReducer;

Board 컴포넌트

components/Board/CreateBoardCard.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import styled from 'styled-components';
import { BoardStyle } from './BoardStyle';
import { useDispatch } from 'react-redux';
import { createBoard } from 'store/reducers/board';

//...

const CreateBoardCard: React.FC = () => {
const dispatch = useDispatch();
return <CreateBoardCardStyle>
<Input placeholder="Create new board" onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.keyCode === 13) {
dispatch(createBoard(e.currentTarget.value))
e.currentTarget.value = '';
}
}} />
</CreateBoardCardStyle>
}
export default CreateBoardCard;

보드 입력시 엔터를 누르면 생성되는 기능을 추가했습니다.

components/Board/BoardCard.tsx
1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { BoardStyle } from './BoardStyle';
import { BoardType } from 'store/reducers/board';

//...
const BoardCard: React.FC<{ board: BoardType }> = ({ board }) => {
return <BoardStyle><LinkStyle to={`/board/${board.id}`}>{board.title}</LinkStyle></BoardStyle>
}
export default BoardCard;

보드 아이디에 따른 라우터 설정을 해줍니다.

pages/BoardPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import styled from 'styled-components';
import CreateBoardCard from 'components/Boards/CreateBoardCard';
import BoardCard from 'components/Boards/BoardCard';
import { useSelector } from 'react-redux';
import { BoardType } from 'store/reducers/board';
import { RootState } from 'store/reducers';

const BoardWrap = styled.section`
width: 100%;
display: flex;
flex-wrap: wrap;
`

const BoardPage:React.FC = () => {
const boardState = useSelector((state: RootState) => state.board);
return <BoardWrap>
{boardState.boards.map((board: BoardType) => <BoardCard key={board.id} board={board}/>)}
<CreateBoardCard />
</BoardWrap>
}

export default BoardPage;

Lists

Lists 리듀서

store/reducers/lists.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import produce from 'immer';
import { action, ActionType, createReducer } from 'typesafe-actions';
import uuid from 'uuid';

export const CREATE_LISTS = "CREATE_LISTS";
export const UPDATE_LISTS_TITLE = "UPDATE_LISTS_TITLE";

export const createLists = (title: string, boardId: string) => {
const id: string = uuid.v4();
return action(CREATE_LISTS, { id, title, boardId });
}

export const updateListsTitle = (id: string, title: string) => action(UPDATE_LISTS_TITLE, { id, title });

const actions = {
createLists,
updateListsTitle
};

export { actions };

export interface ListsType {
id: string;
boardId: string;
title: string;
}

export interface ListsState {
lists: ListsType[];
}

export type ListsActions = ActionType<typeof actions>;

const initialState: ListsState = {
lists: []
};

export default createReducer<ListsState, ListsActions>(initialState, {
[CREATE_LISTS]: (state, action) =>
produce(state, draft => {
draft.lists = [...state.lists, { id: action.payload.id, boardId: action.payload.boardId, title: action.payload.title }];
}),
[UPDATE_LISTS_TITLE]: (state, action) =>
produce(state, draft => {
draft.lists = state.lists.map((list: ListsType) => {
if (list.id === action.payload.id) {
list = { ...list, title: action.payload.title }
}
return list;
})
})
});
store/reducers/index.tsx
1
2
3
4
5
6
7
8
import { combineReducers } from "redux";
import board from './board';
import lists from './lists';

const rootReducer = combineReducers({ board, lists });

export type RootState = ReturnType<typeof rootReducer>
export default rootReducer;

Lists 컴포넌트

components/Lists/CreateLists.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { createLists } from 'store/reducers/lists';
import { useParams } from 'react-router-dom';

//...

const CreateLists: React.FC = () => {
const { id: boardId } = useParams();
const dispatch = useDispatch();
return <ListsWrapper>
<ListsContent>
<ListHeader
type="text"
placeholder="Create lists"
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
if (e.keyCode === 13 && value) {
if (boardId) {
dispatch(createLists(e.currentTarget.value, boardId))
e.currentTarget.value = '';
}
}
}} />
</ListsContent>
</ListsWrapper>
}

export default CreateLists;
components/Lists/Lists.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import ListCard from './ListCard';
import { ListsType, updateListsTitle } from 'store/reducers/lists';
import CreateCard from './CreateCard';
import { RootState } from 'store/reducers';
import { CardType } from 'store/reducers/card';

//...

const Lists: React.FC<{ list: ListsType }> = ({ list }) => {
const dispatch = useDispatch();
const cards = useSelector((state: RootState) =>
state.card.cards.filter((card: CardType) => card.listsId === list.id)
);
return <ListsWrapper>
<ListsContent>
<ListHeader type="text" defaultValue={list.title}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
if (e.keyCode === 13 && value) {
dispatch(updateListsTitle(list.id, e.currentTarget.value))
e.currentTarget.blur();
}
}} />
<ListsStyle>
{cards.map((card: CardType, i: number) => <ListCard key={i} card={card} />)}
</ListsStyle>
<CreateCard listId={list.id} />
</ListsContent>
</ListsWrapper>
}

export default Lists;
pages/ListPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import styled from 'styled-components';
import Lists from 'components/Lists/Lists';
import CreateLists from 'components/Lists/CreateLists';
import { useSelector } from 'react-redux';
import { RootState } from 'store/reducers';
import { ListsType } from 'store/reducers/lists';

//...

const ListPage = () => {
const listsState = useSelector((state:RootState) => state.lists);
return (<BoardStyle>
<BoardListWrapper>
{listsState.lists.map((list:ListsType) => <Lists key={list.id} list={list}/>)}
<CreateLists />
</BoardListWrapper>
</BoardStyle>)
}

export default ListPage;

Card

Card 리듀서

store/reducers/card.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import produce from 'immer';
import { action, ActionType, createReducer } from 'typesafe-actions';
import uuid from 'uuid';

export const CREATE_CARD = "CREATE_CARD";
export const UPDATE_CARD = "UPDATE_CARD";

export const createCard = (listsId: string, cardName: string) => {
const id: string = uuid.v4();
return action(CREATE_CARD, { id, cardName, listsId })
}

export const updateCard = (cardId: string, cardName: string) => action(UPDATE_CARD, { cardId, cardName });

const actions = {
createCard,
updateCard
};

export { actions };

export interface CardType {
id: string;
cardName: string;
listsId: string;
}

export interface ListsState {
cards: CardType[];
}

export type ListsActions = ActionType<typeof actions>;

const initialState: ListsState = {
cards: []
};

export default createReducer<ListsState, ListsActions>(initialState, {
[CREATE_CARD]: (state, action) =>
produce(state, draft => {
draft.cards = [...state.cards, { id: action.payload.id, listsId: action.payload.listsId, cardName: action.payload.cardName }];
}),
[UPDATE_CARD]: (state, action) =>
produce(state, draft => {
draft.cards = state.cards.map((card: CardType) => {
if (card.id === action.payload.cardId) {
card = { ...card, cardName: action.payload.cardName };
}
return card;
});
})
});
store/reducers/index.ts
1
2
3
4
5
6
7
8
9
import { combineReducers } from "redux";
import board from './board';
import lists from './lists';
import card from './card';

const rootReducer = combineReducers({ board, lists, card });

export type RootState = ReturnType<typeof rootReducer>
export default rootReducer;

Card 컴포넌트

components/Lists/CreateCard.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { useState } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { createCard } from "store/reducers/card";

//...

const CreateCard: React.FC<{ listId: string }> = ({ listId }) => {
const dispatch = useDispatch();
const [cardName, setCardName] = useState('');
const create = () => {
dispatch(createCard(listId, cardName))
setCardName('');
}
return <CreateCardWrapper>
<Input placeholder="Add create card"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCardName(e.target.value)}
value={cardName}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.keyCode === 13 && cardName) {
create();
}
}} />
<Icon>
<FontAwesomeIcon icon={faPlus} size="sm" color="rgba(0,0,0,0.5)" onClick={() => create()} />
</Icon>
</CreateCardWrapper>
}

export default CreateCard;
components/Lists/ListsCard.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { useRef } from "react";
import styled from "styled-components";
import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPen } from "@fortawesome/free-solid-svg-icons";
import { CardType, updateCard } from "store/reducers/card";

//...

const ListCard: React.FC<{ card: CardType }> = ({ card }) => {
const inputEl = useRef<HTMLInputElement>(null);
const dispatch = useDispatch();
return (
<ListCardStyle>
<ListCardContent>
<ListCardInput ref={inputEl} defaultValue={card.cardName}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
if (e.keyCode === 13 && value) {
dispatch(updateCard(card.id, e.currentTarget.value))
e.currentTarget.blur();
}
}} />
<Icon onClick={() => {
if (inputEl && inputEl.current) {
inputEl.current.focus();
}
}}>
<FontAwesomeIcon icon={faPen} size="sm" color="rgba(0,0,0,0.5)" />
</Icon>
</ListCardContent>
</ListCardStyle>
);
};

export default ListCard;

이것으로 각각의 컴포넌트에 기능을 추가했습니다.
다음 포스트에서는 리스트와 카드간의 드래그 앤 드롭부분을 구현해보도록 하겠습니다.
결과물

Share