Skip to content

Commit fb3bea7

Browse files
authored
Merge pull request #95 from prgrms-web-devcourse-final-project/66-design/walk-common-modal
[Design] #66 산책에 사용되는 공통 모달 UI 구현
2 parents 5c1cf7f + 36c469b commit fb3bea7

File tree

7 files changed

+687
-1
lines changed

7 files changed

+687
-1
lines changed

src/assets/report.svg

Lines changed: 5 additions & 0 deletions
Loading

src/components/Select/index.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useState } from 'react'
2+
import * as S from './styles'
3+
4+
interface Option {
5+
value: string
6+
label: string
7+
}
8+
9+
interface SelectProps {
10+
options: Option[]
11+
value: string
12+
onChange: (value: string) => void
13+
placeholder?: string
14+
}
15+
16+
const Select = ({ options, value, onChange, placeholder }: SelectProps) => {
17+
const [isOpen, setIsOpen] = useState(false)
18+
19+
const selectedOption = options.find(option => option.value === value)
20+
21+
return (
22+
<S.SelectContainer>
23+
<S.SelectButton onClick={() => setIsOpen(!isOpen)}>
24+
{selectedOption ? selectedOption.label : placeholder || '선택하세요'}
25+
<S.Arrow isOpen={isOpen} />
26+
</S.SelectButton>
27+
28+
{isOpen && (
29+
<S.OptionList>
30+
{options.map(option => (
31+
<S.Option
32+
key={option.value}
33+
onClick={() => {
34+
onChange(option.value)
35+
setIsOpen(false)
36+
}}
37+
isSelected={value === option.value}
38+
>
39+
{option.label}
40+
</S.Option>
41+
))}
42+
</S.OptionList>
43+
)}
44+
</S.SelectContainer>
45+
)
46+
}
47+
48+
export default Select

src/components/Select/styles.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import styled, { css } from 'styled-components'
2+
3+
export const SelectContainer = styled.div`
4+
position: relative;
5+
width: 100%;
6+
margin-bottom: 33px;
7+
`
8+
9+
export const SelectButton = styled.button`
10+
width: 100%;
11+
padding: 12px 16px;
12+
background: white;
13+
border: 1px solid ${({ theme }) => theme.colors.grayscale.gc_3};
14+
border-radius: 8px;
15+
display: flex;
16+
justify-content: space-between;
17+
align-items: center;
18+
cursor: pointer;
19+
font-size: 14px;
20+
color: ${({ theme }) => theme.colors.grayscale.font_1};
21+
height: 54px;
22+
`
23+
24+
export const Arrow = styled.span<{ isOpen: boolean }>`
25+
width: 0;
26+
height: 0;
27+
border-left: 5px solid transparent;
28+
border-right: 5px solid transparent;
29+
border-top: 5px solid ${({ theme }) => theme.colors.grayscale.font_2};
30+
transform: ${({ isOpen }) => (isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
31+
transition: transform 0.2s ease;
32+
`
33+
34+
export const OptionList = styled.ul`
35+
position: absolute;
36+
top: 100%;
37+
left: 0;
38+
right: 0;
39+
margin-top: 4px;
40+
padding: 0;
41+
background: white;
42+
border: 1px solid ${({ theme }) => theme.colors.grayscale.gc_1};
43+
border-radius: 8px;
44+
list-style: none;
45+
max-height: 200px;
46+
overflow-y: auto;
47+
z-index: 1000;
48+
`
49+
50+
export const Option = styled.li<{ isSelected: boolean }>`
51+
padding: 12px 16px;
52+
cursor: pointer;
53+
font-size: 14px;
54+
55+
${({ isSelected, theme }) =>
56+
isSelected &&
57+
css`
58+
background-color: ${theme.colors.brand.lighten_3};
59+
color: ${theme.colors.brand.default};
60+
`}
61+
62+
&:hover {
63+
background-color: ${({ theme }) => theme.colors.grayscale.gc_3};
64+
}
65+
`
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { SetStateAction, useState, useEffect } from 'react'
2+
import * as S from './styles'
3+
import Select from '~components/Select'
4+
import { WalkModalProps, RequestUserInfo, OtherUserInfo } from '~types/modal'
5+
6+
const reportOptions = [
7+
{ value: 'dog', label: '강아지가 사나워요.' },
8+
{ value: 'other', label: '기타' },
9+
]
10+
11+
const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalProps) => {
12+
useEffect(() => {
13+
document.body.style.overflow = 'hidden'
14+
15+
return () => {
16+
document.body.style.overflow = 'unset'
17+
}
18+
}, [])
19+
20+
const [selectedReportType, setSelectedReportType] = useState('')
21+
const getModalContent = () => {
22+
switch (type) {
23+
case 'request':
24+
return {
25+
message: '함께 산책하기 위해 멘트를 입력하세요.',
26+
confirmText: '전송하기',
27+
}
28+
case 'accept':
29+
return {
30+
message: '산책 요청이 왔어요!',
31+
confirmText: '수락',
32+
cancelText: '거절',
33+
}
34+
case 'complete':
35+
return {
36+
message: '신고가 완료됐습니다.',
37+
confirmText: '확인',
38+
}
39+
case 'progress':
40+
return {
41+
title: '강번따 응답',
42+
message: `${userInfo.name}이(가) 거절했어요.`,
43+
confirmText: '다시 시도',
44+
cancelText: '취소',
45+
}
46+
case 'friend':
47+
return {
48+
message: `${userInfo.name}이 30분동안 산책했어요.`,
49+
confirmText: '수락',
50+
cancelText: '거절',
51+
}
52+
case 'report':
53+
return {
54+
title: '어떤 이유로 신고하시나요?',
55+
confirmText: '전달',
56+
cancelText: '취소',
57+
}
58+
case 'reportComplete':
59+
return {
60+
title: '신고 완료!',
61+
message: '신고가 완료됐습니다.',
62+
confirmText: '확인',
63+
}
64+
case 'walkRequest':
65+
return {
66+
title: '산책 친구하실래요?',
67+
confirmText: '수락',
68+
cancelText: '거절',
69+
}
70+
}
71+
}
72+
73+
const modalContent = getModalContent()
74+
75+
return (
76+
<S.ModalOverlay onClick={onClose}>
77+
<S.ModalContent type={type} onClick={e => e.stopPropagation()}>
78+
{(type === 'accept' || type === 'progress' || type === 'walkRequest') && (
79+
<>
80+
<h1>{type === 'progress' || type === 'walkRequest' ? modalContent?.title : modalContent?.message}</h1>
81+
{type === 'walkRequest' && <S.ReportIcon />}
82+
</>
83+
)}
84+
{type !== 'accept' && type === 'friend' && <div className='date'>2024.12.14</div>}
85+
86+
{type !== 'progress' && type !== 'report' && type !== 'reportComplete' && (
87+
<S.UserInfo type={type}>
88+
<S.Avatar type={type} />
89+
<S.Info type={type}>
90+
<h3>{userInfo.name}</h3>
91+
{type === 'request' || type === 'accept' || type === 'walkRequest' ? (
92+
<>
93+
<p>
94+
{(userInfo as RequestUserInfo).breed} <S.InfoSeparator $height={8} />{' '}
95+
{(userInfo as RequestUserInfo).age} <S.InfoSeparator $height={8} />{' '}
96+
{(userInfo as RequestUserInfo).gender}
97+
</p>
98+
</>
99+
) : (
100+
<>
101+
{(userInfo as OtherUserInfo).location && <p>{(userInfo as OtherUserInfo).location}</p>}
102+
{(userInfo as OtherUserInfo).time && <p>{(userInfo as OtherUserInfo).time}</p>}
103+
</>
104+
)}
105+
</S.Info>
106+
</S.UserInfo>
107+
)}
108+
109+
{type === 'report' && (
110+
<>
111+
<h1>{modalContent?.title}</h1>
112+
<S.SelectWrapper>
113+
<Select
114+
options={reportOptions}
115+
value={selectedReportType}
116+
onChange={(value: SetStateAction<string>) => setSelectedReportType(value)}
117+
placeholder='신고 사유 선택'
118+
/>
119+
</S.SelectWrapper>
120+
</>
121+
)}
122+
123+
{type === 'request' ? (
124+
<S.Message as='textarea' type={type} className='message' placeholder={modalContent?.message} />
125+
) : type === 'progress' ? (
126+
<S.Message type={type} className='message'>
127+
{modalContent?.message}
128+
</S.Message>
129+
) : null}
130+
131+
{type === 'reportComplete' && (
132+
<>
133+
<h1>{modalContent?.title}</h1>
134+
<S.Message type={type}>{modalContent?.message}</S.Message>
135+
</>
136+
)}
137+
138+
<S.ButtonGroup type={type}>
139+
{modalContent?.cancelText && (
140+
<S.Button type={type} variant='cancel' onClick={onCancel}>
141+
{modalContent.cancelText}
142+
</S.Button>
143+
)}
144+
<S.Button type={type} variant='confirm' onClick={onConfirm}>
145+
{modalContent?.confirmText}
146+
</S.Button>
147+
</S.ButtonGroup>
148+
</S.ModalContent>
149+
</S.ModalOverlay>
150+
)
151+
}
152+
153+
export default WalkModal

0 commit comments

Comments
 (0)