Skip to content

Commit

Permalink
feat: 리마인더 구현 (#142)
Browse files Browse the repository at this point in the history
* feat: 리마인더 페이지 생성 및 라우터 적용

* feat: reminder 관련 msw 모킹 생성

* feat: reminder api 추가

* design: 기본 디자인 추가

* feat: reminder데이터 UI에 활용하기 좋게 mapping 타입 선언

* feat: reminder 데이터 fetch 구현 및 해당 데이터 UI에 맞춰 구현

* design: 리마인더 디자인 구현

* refactor: DateInput 컴포넌트 callback수정 및 min,max 설정

* feat: ReminderCard 컴포넌트 구현

* design: dateInput 글자 넘치는 것 css로 커버

* design: 전역에서 사용할 디자인 추가

* refactor: BackgroundProps export 추가

* feat: ReminderCardBox 연결

* chore: image 경로 추가 및 console 제거

* design: 리마인더 카드 내부에 갭 설정

* refactor: data 값 변경

* refactor: lastWaterDay -> dDay로 변경

* refactor: 상태에 따른 알림 메세지 다르게 설정

* refactor: 중복되는 날짜에 카드가 있을 경우 날짜 하루만 표시

* feat: 공용 checkbox 구현

* refactor: CheckBox 컴포넌트 적용 및 Image 컴포넌트 사용

* feat: 오늘부터 특정 일수 이후의 날짜를 구하는 함수 구현

* refactor: action 발생시 날짜 찍히도록 설정

* feat: 물주기 msw구현

* refactor: msw 값 반환시에 내림차순으로 정렬

* refactor: reminder페이지 관심사 분리에 따른 컴포넌트 분리

* feat: 미루기 api 구현

* refactor: 물주기 api 훅 분리

* refactor: reminder에 사용되는 hooks들 분리

* chore: 사용하지 않는 console 제거 및 로직 변화에 따른 스토리북 수정

* refactor: reminder msw 수정 및 입력 값에 따라 계산되도록 설정

* refactor: action 메서드 contextAPI 사용 및 promise 순서 보장

* refactor: 날짜 미루기 조건 설정

- 미루기가 오늘보다 늦는다면 미뤄지는 날짜 기준이 lastWaterDate가 됨
- else 미뤄지는 날짜 기준이 today임.

* chore: ReminderCard ->Card로 파일명 변경

* test: CardBox 스토리 작성

* test: MonthBox 스토리 작성

* refactor: params 형태 변경 및 적용

* refactor: checkbox 타입 추가

* refactor: todayStatus type변경 및 적용

* refactor: hooks의 모든 return값을 받을 수 있도록 처리

- 타입을 반환하는 과정에서 as unnkown as Promise<T>를 사용했음.
- 위에 설정 안하면 에러 발생...

* refactor: notDate-> showDate로 변수명 변경

* refactor: mutationProps 타입 선언

* refactor: checkbox id 자체 생성

* chore: 네이밍 PascalCase로 변경

* refactor: 리마인더 타입 수정

* refactor: pushoff -> changeDate로 함수명 변경

* refactor: DateInput htmlinputprops 확장

* refactor: merge 하면서 생긴 파일 path 수정

* refactor: Authorization 추가

* refactor: Props -> params로 subfix 변경

* refactor: checkbox 타입으로 전달

* style: import type 설정 및 사용하지 않는 코드 제거

* chore: 인증 이메일 변경

* refactor: 구조분해 할당 필요없는 부분 제거

* refactor: 쓸모없는 key 제거

* refactor: 타입 재활용 및 중복 코드 삭제

* design: hover 제거
  • Loading branch information
hozzijeong authored and Kim0914 committed Aug 2, 2023
1 parent aa3abac commit 289d287
Show file tree
Hide file tree
Showing 34 changed files with 1,276 additions and 15 deletions.
40 changes: 40 additions & 0 deletions frontend/src/apis/reminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ChangeDateParams, WaterPlantParams } from 'types/api/reminder';
import { BASE_URL } from 'constants/index';

export const REMINDER = `${BASE_URL}/reminders`;

const headers = {
'Content-Type': 'application/json',
Authorization: '[email protected]',
};

const getReminder = () => {
return fetch(REMINDER, {
method: 'GET',
headers,
});
};

const waterPlant = ({ id, body }: WaterPlantParams) => {
return fetch(`${REMINDER}/${id}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
};

const changeDate = ({ id, body }: ChangeDateParams) => {
return fetch(`${REMINDER}/${id}`, {
method: 'PATCH',
headers,
body: JSON.stringify(body),
});
};

const ReminderAPI = {
getReminder,
waterPlant,
changeDate,
};

export default ReminderAPI;
27 changes: 27 additions & 0 deletions frontend/src/components/@common/CheckBox/CheckBox.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react';
import { css } from 'styled-components';
import CheckBox from '.';

const meta: Meta<typeof CheckBox> = {
component: CheckBox,
args: {},
};

export default meta;

type Story = StoryObj<typeof CheckBox>;

export const Default: Story = {
args: {
fillStyle: css`
width: 24px;
height: 24px;
color: ${(props) => props.theme.color.primary};
`,
emptyStyle: css`
width: 24px;
height: 24px;
color: ${(props) => props.theme.color.grayLight};
`,
},
};
13 changes: 13 additions & 0 deletions frontend/src/components/@common/CheckBox/CheckBox.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { styled } from 'styled-components';

export const Label = styled.label`
cursor: pointer;
display: 'flex';
gap: '12px';
align-items: 'center';
`;

export const Input = styled.input`
display: none;
padding: 0;
`;
38 changes: 38 additions & 0 deletions frontend/src/components/@common/CheckBox/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useId, useState } from 'react';
import { CSSProp } from 'styled-components';
import { Input, Label } from './CheckBox.style';
import CheckBoxEmpty from '../Icons/CheckBoxEmpty';
import CheckboxFill from '../Icons/CheckBoxFill';

interface CheckBoxProps {
isChecked?: boolean;
fillStyle?: CSSProp;
emptyStyle?: CSSProp;
checkedCallback?: () => void;
}

const CheckBox = ({ isChecked = false, fillStyle, emptyStyle, checkedCallback }: CheckBoxProps) => {
const [checked, setChecked] = useState(isChecked);
const id = useId();

const checkHandler = () => {
setChecked((prev) => !prev);
if (!checked) {
// true인 경우에 실행되는 메서드
checkedCallback && checkedCallback();
}
};

return (
<Label htmlFor={id}>
<Input type="checkbox" id={id} />
{checked ? (
<CheckboxFill onClick={checkHandler} customCSS={fillStyle} />
) : (
<CheckBoxEmpty onClick={checkHandler} customCSS={emptyStyle} />
)}
</Label>
);
};

export default CheckBox;
32 changes: 32 additions & 0 deletions frontend/src/components/@common/Icons/CheckBoxEmpty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { SVGProps } from 'react';
import { CSSProp, styled } from 'styled-components';

interface SVGPropsExtends extends SVGProps<SVGSVGElement> {
customCSS?: CSSProp;
}

const Svg = styled.svg<SVGPropsExtends>`
${({ customCSS }) => customCSS && customCSS}
`;

const MdiCheckboxBlankOutline = (props: SVGPropsExtends) => {
const { customCSS, ...args } = props;

return (
<Svg
xmlns="http://www.w3.org/2000/svg"
customCSS={customCSS}
width="1em"
height="1em"
viewBox="0 0 24 24"
{...args}
>
<path
fill="currentColor"
d="M19 3H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m0 2v14H5V5h14Z"
></path>
</Svg>
);
};

export default MdiCheckboxBlankOutline;
31 changes: 31 additions & 0 deletions frontend/src/components/@common/Icons/CheckBoxFill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { SVGProps } from 'react';
import { styled, type CSSProp } from 'styled-components';

interface SVGPropsExtends extends SVGProps<SVGSVGElement> {
customCSS?: CSSProp;
}

const Svg = styled.svg<SVGPropsExtends>`
${({ customCSS }) => customCSS && customCSS}
`;

const MdiCheckboxMarked = (props: SVGPropsExtends) => {
const { customCSS, ...args } = props;
return (
<Svg
xmlns="http://www.w3.org/2000/svg"
customCSS={customCSS}
width="1em"
height="1em"
viewBox="0 0 24 24"
{...args}
>
<path
fill="currentColor"
d="m10 17l-5-5l1.41-1.42L10 14.17l7.59-7.59L19 8m0-5H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Z"
></path>
</Svg>
);
};

export default MdiCheckboxMarked;
3 changes: 3 additions & 0 deletions frontend/src/components/DateInput/DateInput.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ export const DateValue = styled.label<{
display: inline-block;
width: 100%;
font: 500 1.8rem/2.2rem 'NanumSquareRound';
color: ${({ $placeholder, theme }) => ($placeholder ? theme.color.gray : 'black')};
text-align: center;
`;

export const Date = styled.input.attrs({ type: 'date' })`
Expand Down
24 changes: 12 additions & 12 deletions frontend/src/components/DateInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
import { useId } from 'react';
import { Date, Wrapper, DateValue } from './DateInput.style';
import { getToday, convertDateKorYear } from 'utils/date';
import { convertDateKorYear } from 'utils/date';

interface DateInputProps {
interface DateInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
value: string;
onChange?: (value: string) => void;
placeholder?: string;
changeCallback?: (value: string) => void;
}

const DateInput = ({ value, onChange, placeholder = '날짜를 입력해 주세요' }: DateInputProps) => {
const today = getToday();
const DateInput = ({
value,
changeCallback,
placeholder = '날짜를 입력해 주세요',
min,
max,
}: DateInputProps) => {
const dateId = useId();

const changeHandler: React.ChangeEventHandler<HTMLInputElement> = (event) => {
const { value } = event.target;

if (value > today) {
return;
}

onChange?.(value);
changeCallback?.(value);
};

return (
<Wrapper>
<DateValue htmlFor={dateId} $placeholder={value === ''}>
{value ? convertDateKorYear(value) : placeholder}
</DateValue>
<Date id={dateId} type="date" onChange={changeHandler} />
<Date id={dateId} type="date" onChange={changeHandler} min={min} max={max} />
</Wrapper>
);
};
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/components/Reminder/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ReminderExtendType } from 'types/api/reminder';
import type { Meta, StoryObj } from '@storybook/react';
import { getParticularDateFromSpecificDay } from 'utils/date';
import ReminderCard from '.';

const meta: Meta<typeof ReminderCard> = {
component: ReminderCard,
};

export default meta;

type Story = StoryObj<typeof ReminderCard>;

export const Default: Story = {
render: () => {
const mockData: ReminderExtendType = {
petPlantId: 1,
image: 'https://images.unsplash.com/photo-1598983062491-5934ce558814',
nickName: '참새나무',
dictionaryPlantName: '알로카시아',
dDay: 0,
nextWaterDate: getParticularDateFromSpecificDay(0, new Date()),
date: '26',
status: 'today',
lastWaterDate: getParticularDateFromSpecificDay(-7, new Date()),
};

return <ReminderCard data={mockData} />;
},
};

export const Late: Story = {
render: () => {
const mockData: ReminderExtendType = {
petPlantId: 1,
image: 'https://images.unsplash.com/photo-1598983062491-5934ce558814',
nickName: '참새나무',
dictionaryPlantName: '알로카시아',
dDay: 19,
nextWaterDate: getParticularDateFromSpecificDay(-19, new Date()),
date: '07',
status: 'late',
lastWaterDate: getParticularDateFromSpecificDay(-26, new Date()),
};

return <ReminderCard data={mockData} />;
},
};

export const None: Story = {
render: () => {
const mockData: ReminderExtendType = {
petPlantId: 1,
image: 'https://images.unsplash.com/photo-1598983062491-5934ce558814',
nickName: '참새나무',
dictionaryPlantName: '알로카시아',
dDay: -3,
nextWaterDate: getParticularDateFromSpecificDay(3, new Date()),
date: '29',
status: 'future',
lastWaterDate: getParticularDateFromSpecificDay(-4, new Date()),
};

return <ReminderCard data={mockData} />;
},
};
76 changes: 76 additions & 0 deletions frontend/src/components/Reminder/Card/Card.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { TodayStatus } from 'types/api/reminder';
import { styled } from 'styled-components';
import { BackgroundProps } from 'pages/Reminder/Reminder.style';
import theme from '../../../style/theme.style';

export const convertCardStatusBar = (status: TodayStatus) => {
switch (status) {
case 'late':
return theme.color.accent;
case 'today':
return theme.color.primary;
case 'future':
return theme.color.grayDark;
default:
return theme.color.grayDark;
}
};

export const Wrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 280px;
height: 86px;
background: ${({ theme }) => theme.color.background};
border: solid 0.5px ${({ theme }) => theme.color.grayLight};
border-radius: 8px 0 0 8px;
`;

export const StatusBar = styled.div<BackgroundProps>`
width: 6px;
height: 100%;
background: ${({ status }) => convertCardStatusBar(status)};
border-radius: 8px 0 0 8px;
`;

export const ContentBox = styled.div`
display: flex;
flex-direction: column;
justify-content: space-around;
width: 128px;
height: 68px;
`;

export const NickName = styled.p`
font: 600 1.4rem/2.1rem 'NanumsquareRound';
color: ${({ theme }) => theme.color.sub};
`;

export const DictionaryPlantName = styled.p`
color: ${({ theme }) => theme.color.grayDark};
`;

export const Alert = styled.p<BackgroundProps>`
font: ${({ theme }) => theme.font.reminderCardContent};
color: ${({ status }) => convertCardStatusBar(status)};
`;

export const ActionBox = styled.div`
width: 58px;
height: 100%;
border-left: solid 0.5px ${({ theme }) => theme.color.gray};
div {
height: 50%;
label {
font: ${({ theme }) => theme.font.reminderCardContent};
}
input {
height: 100%;
}
}
`;
Loading

0 comments on commit 289d287

Please sign in to comment.