3. 컴포넌트 스타일링
src/components/Modal
가장 보통의 UI - Modal 에서 구현한 Modal 컴포넌트를 가져와 src/components
하위에 붙여 넣습니다.
// src/components/modal.css
.modal-enter {
opacity: 0;
}
.modal-enter-active {
opacity: 1;
transition: opacity 200ms;
}
.modal-exit {
opacity: 1;
}
.modal-exit-active {
opacity: 0;
transition: opacity 200ms;
}
// src/components/Modal.tsx
import React from 'react';
import styled from "@emotion/styled/macro";
import { CSSTransition } from 'react-transition-group';
import './modal.css';
import Portal from "./Portal";
const Overlay = styled.div`
position: fixed;
z-index: 10;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
`;
const Dim = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
`;
const Container = styled.div`
max-width: 456px;
position: relative;
width: 100%;
`;
interface Props {
isOpen: boolean;
onClose: () => void;
selector?: string;
}
const Modal: React.FC<Props> = ({ children, isOpen, onClose, selector }) => {
return (
<CSSTransition in={isOpen} timeout={300} classNames="modal" unmountOnExit>
<Portal selector={selector}>
<Overlay>
<Dim onClick={onClose} />
<Container>{children}</Container>
</Overlay>
</Portal>
</CSSTransition>
)
}
export default Modal;
src/features/Calendar
가장 보통의 UI - Calendar 에서 구현한 Calendar 컴포넌트를 가져와 src/features/Calendar/index.tsx
에 붙여 넣습니다.
// src/utils.ts
export const pad = (time: number) => {
return `0${time}`.slice(-2);
}
export const getSimpleDateFormat = (d: Date, separator: string = '-') => {
return [d.getFullYear(), pad(d.getMonth() + 1), pad(d.getDate())].join(separator);
}
export const isSameDay = (a: Date, b: Date): boolean => {
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
// src/components/Calendar.tsx
import React, { useMemo, useState } from 'react';
import styled from '@emotion/styled/macro';
import { BiChevronLeft, BiChevronRight } from 'react-icons/bi';
import { isSameDay } from '../utils';
const Header = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
`;
const Title = styled.h1`
margin: 0;
padding: 8px 24px;
font-size: 24px;
font-weight: normal;
text-align: center;
color: #F8F7FA;
`;
const ArrowButton = styled.button<{ pos: 'left' | 'right' }>`
border: none;
border-radius: 4px;
padding: 8px 12px;
background-color: transparent;
font-size: 18px;
cursor: pointer;
color: #F8F7FA;
`;
const ButtonContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
const Table = styled.table`
border-collapse: collapse;
width: 100%;
height: 100%;
border-spacing: 0;
`;
const TableHeader = styled.thead`
padding-block: 12px;
> tr {
> th {
padding-block: 12px;
font-weight: normal;
color: #F8F7FA;
}
}
`;
const TableBody = styled.tbody``;
const TableData = styled.td`
text-align: center;
color: #C9C8CC;
padding: 8px;
position: relative;
`;
const DisplayDate = styled.div<{ isToday?: boolean; isSelected?: boolean; }>`
color: ${({ isToday }) => isToday && '#F8F7FA'};
background-color: ${({ isToday, isSelected }) => isSelected ? '#7047EB' : isToday ? '#313133' : ''};
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
align-self: flex-end;
position: absolute;
top: 8px;
right: 8px;
width: 36px;
height: 36px;
cursor: pointer;
`;
const Base = styled.div`
width: 100%;
height: 100vh;
padding: 24px 12px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
background-color: #28272A;
${Header} + ${Table} {
margin-top: 36px;
}
`;
const DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
const MONTHS = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const Calendar: React.FC = () => {
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 선택한 날짜 상태
const { year, month, firstDay, lastDay } = useMemo(() => { // 선택한 날짜를 기준으로 연, 월, 일, 해당 월의 첫째 날짜, 해달 월의 마지막 날짜 가져온다.
const year = selectedDate.getFullYear();
const month = selectedDate.getMonth();
return ({
year,
month,
firstDay: new Date(year, month, 1),
lastDay: new Date(year, month + 1, 0)
})
}, [selectedDate]);
const selectDate = (date: Date) => {
setSelectedDate(date);
}
const pad = () => [...Array(firstDay.getDay()).keys()].map((p: number) => <TableData key={`pad_${p}`} />);
const range = () => [...Array(lastDay.getDate()).keys()].map((d: number) => {
const thisDay = new Date(year, month, d + 1);
const today = new Date();
return (
<TableData key={d} onClick={() => selectDate(thisDay)}>
<DisplayDate
isSelected={isSameDay(selectedDate, thisDay)}
isToday={isSameDay(today, thisDay)}
>{new Date(year, month, d + 1).getDate()}</DisplayDate>
</TableData>
)
});
const render = () => {
const items = [...pad(), ...range()];
const weeks = Math.ceil(items.length / 7);
return [...Array(weeks).keys()].map((week: number) => (
<tr key={`week_${week}`}>
{items.slice(week * 7, week * 7 + 7)}
</tr>
));
}
return (
<Base>
<Header>
<ButtonContainer>
<ArrowButton pos="left" onClick={() => selectDate(new Date(selectedDate.setMonth(selectedDate.getMonth() - 1)))}>
<BiChevronLeft />
</ArrowButton>
<Title>{`${MONTHS[month]} ${year}`}</Title>
<ArrowButton pos="right" onClick={() => selectDate(new Date(selectedDate.setMonth(selectedDate.getMonth() + 1)))}>
<BiChevronRight />
</ArrowButton>
</ButtonContainer>
</Header>
<Table>
<TableHeader>
<tr>
{
DAYS.map((day, index) => (
<th key={day} align="center">{day}</th>
))
}
</tr>
</TableHeader>
<TableBody>
{render()}
</TableBody>
</Table>
</Base>
)
}
export default Calendar;
src/features/TodoFormModal
: 선택한 날짜에 할 일을 추가 시킬 수 있는 모달
import React, { useRef, useState } from 'react';
import styled from '@emotion/styled/macro';
import Modal from '../../components/Modal';
const Container = styled.div`
width: 100vw;
max-width: 386px;
padding: 8px;
`;
const Date = styled.small`
display: block;
color: #C9C8CC;
`;
const InputTodo = styled.input`
padding: 16px 24px;
border: none;
width: 100%;
box-sizing: border-box;
background-color: transparent;
color: #C9C8CC;
caret-color: #C9C8CC;
`;
const Card = styled.div`
width: 100%;
max-width: 370px;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
padding: 24px;
box-sizing: border-box;
background-color: #19181A;
${Date} + ${InputTodo} {
margin-top: 24px;
};
`;
const TodoFormModal: React.FC = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleClose = () => setIsOpen(false);
return (
<Modal isOpen={isOpen} onClose={handleClose}>
<Container>
<Card>
<Date>'2021-08-30'</Date>
<InputTodo placeholder="새로운 이벤트" />
</Card>
</Container>
</Modal>
)
}
export default TodoFormModal;
src/features/TodoList
: 할 일 목록
import React from 'react';
import styled from '@emotion/styled/macro';
interface Todo {
id: string;
content: string;
done: boolean;
date: Date;
}
const EtcItem = styled.li`
padding: 2px 4px;
margin: 0;
font-size: 10px;
cursor: pointer;
`;
const TodoItem = styled.li<{ done?: boolean; selected?: boolean; }>`
max-width: 100px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
background-color: ${({ done, selected }) => selected ? 'rgba(112, 71, 235, 1)' : done ? 'transparent' : 'rgba(112, 71, 235, 0.4)'};
padding: 2px 4px;
margin: 0;
border-radius: 8px;
font-size: 10px;
text-decoration: ${({ done }) => done && 'line-through'};
cursor: pointer;
`;
const Base = styled.ul`
list-style: none;
margin: 36px 0 0 0;
padding: 0;
width: 100%;
height: 60px;
${TodoItem} + ${TodoItem} {
margin-top: 1px;
}
${TodoItem} + ${EtcItem} {
margin-top: 1px;
}
`;
interface Props {
items: Array<Todo>;
}
const MAX_TODO_LIST_LENGTH = 3;
const TodoList: React.FC<Props> = ({ items }) => (
<Base>
{items.slice(0, MAX_TODO_LIST_LENGTH).map((item, index) => (
<TodoItem
key={item.id}
done={item.done}
>
{item.content}
</TodoItem>
))}
{items.length > MAX_TODO_LIST_LENGTH && (
<EtcItem>{`그 외 ${items.length - MAX_TODO_LIST_LENGTH}개...`}</EtcItem>
)}
</Base>
);
export default TodoList;
src/features/TodoStatisticsModal
: 선택한 날짜의 할 일 통계(Statistics)
import React, { useState } from 'react';
import styled from '@emotion/styled/macro';
import { HiOutlineTrash } from 'react-icons/hi';
import Modal from '../../components/Modal';
const Container = styled.div`
width: 100vw;
max-width: 386px;
padding: 8px;
`;
const Date = styled.small`
display: block;
color: #C9C8CC;
`;
const TodoActionButton = styled.button<{ secondary?: boolean; }>`
border: none;
background-color: transparent;
color: ${({ secondary }) => secondary && '#ff6b6b'};
cursor: pointer;
`;
const TodoActions = styled.span`
flex: 1 0 5%;
`;
const Content = styled.span`
flex: 1 0 95%;
`;
const TodoItem = styled.li`
width: 100%;
display: flex;
color: #C9C8CC;
align-items: center;
border-radius: 8px;
`;
const TodoList = styled.ul`
list-style: circle;
margin: 0;
padding: 0;
width: 100%;
${TodoItem} + ${TodoItem} {
margin-top: 8px;
}
`;
const Statistics = styled.p`
color: #7047EB;
font-size: 16px;
font-weight: bold;
`;
const Card = styled.div`
width: 100%;
max-width: 370px;
border-radius: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
padding: 24px;
box-sizing: border-box;
background-color: #19181A;
${Date} + ${TodoList} {
margin-top: 24px;
}
;
`;
const TodoStatisticsModal: React.FC = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleClose = () => setIsOpen(false);
return (
<Modal isOpen={isOpen} onClose={handleClose}>
<Container>
<Card>
<Date>2021-08-30</Date>
<Statistics>할 일 0개 남음</Statistics>
<TodoList>
<TodoItem key={todo.id}>
<Content>{todo.content}</Content>
<TodoActions>
<TodoActionButton secondary onClick={() => removeTodo(todo.id)}>
<HiOutlineTrash />
</TodoActionButton>
</TodoActions>
</TodoItem>
</TodoList>
</Card>
</Container>
</Modal>
)
}
export default TodoStatisticsModal;
Last updated