Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

style: BottomSheet 컴포넌트 제작 #34

Merged
merged 12 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
731 changes: 722 additions & 9 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"@emotion/styled": "^11.11.0",
"@tanstack/react-query": "^4.36.1",
"axios": "^1.5.1",
"d3": "^7.8.5",
"emotion-reset": "^3.0.1",
"framer-motion": "^10.16.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
Expand All @@ -26,6 +28,7 @@
"devDependencies": {
"@rushstack/eslint-config": "^3.4.1",
"@tanstack/react-query-devtools": "^4.36.1",
"@types/d3": "^7.4.2",
"@types/node": "^20.8.6",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
Expand Down
File renamed without changes.
30 changes: 30 additions & 0 deletions src/components/common/Avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Avatar.tsx
import styled from '@emotion/styled'

type AvatarProps = {
width: number
height: number
imgUrl: string
margin: string
shadow?: boolean
}

const StyledAvatar = styled.div<AvatarProps>`
width: ${(props) => (typeof props.width === 'number' ? `${props.width}px` : props.width)};
height: ${(props) => (typeof props.height === 'number' ? `${props.height}px` : props.height)};
background-image: url(${(props) => props.imgUrl});
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
border-radius: 50%; // 원 형태로 만들기 위함
margin: ${(props) => props.margin};
box-shadow: ${(props) => (props.shadow ? '0px 0px 10px rgba(0, 0, 0, 0.25)' : 'none')};
`

const Avatar: React.FC<AvatarProps> = ({ width, height, imgUrl, margin, shadow = false }) => {
return (
<StyledAvatar width={width} height={height} imgUrl={imgUrl} shadow={shadow} margin={margin} />
)
}

export default Avatar
152 changes: 152 additions & 0 deletions src/components/common/BottomSheet/ProfileSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import styled from '@emotion/styled'
import { AnimatePresence, motion } from 'framer-motion'
import { MouseEvent, useState } from 'react'
import { AiOutlineClose } from 'react-icons/ai'

import Avatar from '@/components/common/Avatar'
import { Text } from '@/components/common/Text'
import { palette } from '@/styles/palette'

import { InterestButton } from '../Buttons/IconButton'

const Background = styled(motion.div)`
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-end;
overflow-y: hidden;
`

const BottomContentWrapper = styled(motion.div)<{
isDarkMode: boolean
}>`
width: 100%;
display: flex;
flex-direction: column;
height: 378px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
background-color: ${({ isDarkMode }) => (isDarkMode ? palette.GRAY700 : palette.WHITE)};
`

const BottomContentHeader = styled.div<{
isDarkMode: boolean
}>`
width: 100%;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid ${({ isDarkMode }) => (isDarkMode ? palette.GRAY500 : palette.GRAY200)};
background-color: ${({ isDarkMode }) => (isDarkMode ? palette.GRAY700 : palette.WHITE)};
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 24px 0;
`

const BottomContent = styled.div<{
isDarkMode: boolean
}>`
display: flex;
flex-direction: column;
align-items: center;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
background-color: ${({ isDarkMode }) => (isDarkMode ? palette.GRAY700 : palette.WHITE)};
`

type ProfileSheetProps = {
title: string
isDarkMode: boolean
}

const ProfileSheet = ({ title, isDarkMode }: ProfileSheetProps) => {
const [isOpen, setIsOpen] = useState(true) // ProfileSheet의 상태

const handleWrapperClick = (e: MouseEvent) => {
e.stopPropagation()
}

const toggleProfileSheet = () => {
setIsOpen(false)
}

const slideUp = {
hidden: { y: '100%', opacity: 0 },
visible: { y: '0%', opacity: 1, transition: { type: 'spring', damping: 15, stiffness: 100 } },
exit: { y: '100%', opacity: 0, transition: { type: 'spring', damping: 15, stiffness: 100 } },
}

const backgroundFade = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
exit: { opacity: 0 },
}

return (
<AnimatePresence>
{isOpen && (
<Background
// Background에 대한 애니메이션 속성 추가
initial={'hidden'}
animate={'visible'}
exit={'exit'}
variants={backgroundFade}
onClick={toggleProfileSheet}
>
<BottomContentWrapper
isDarkMode={isDarkMode}
onClick={handleWrapperClick}
initial={'hidden'} // 초기 상태
animate={isOpen ? 'visible' : 'partiallyVisible'} // 상태에 따른 애니메이션 값 지정
exit={'exit'} // 컴포넌트가 unmount될 때 상태
variants={slideUp} // 애니메이션 정의
>
<BottomContent isDarkMode={isDarkMode}>
<BottomContentHeader isDarkMode={isDarkMode}>
<Text
font={'Body_20'}
fontWeight={700}
letterSpacing={-1}
style={{
color: isDarkMode ? palette.DARK_WHITE : palette.BLACK,
textAlign: 'center',
backgroundColor: isDarkMode ? palette.GRAY700 : palette.WHITE,
flex: 1,
}}
>
{title}
</Text>
<AiOutlineClose
style={{
position: 'absolute',
right: 17,
width: 30,
height: 30,
color: isDarkMode ? palette.DARK_WHITE : palette.BLACK,
}}
onClick={toggleProfileSheet}
/>
</BottomContentHeader>
<Avatar
width={124}
height={124}
imgUrl={'https://i.imgur.com/VwJQ2KB.png'}
margin={'24px 0 42px 0'}
/>
<InterestButton
nickName={'우땅'}
interests={['웹 소프트웨어 개발', '취업', '주식']}
isDarkMode={isDarkMode}
/>
</BottomContent>
</BottomContentWrapper>
</Background>
)}
</AnimatePresence>
)
}

export default ProfileSheet
163 changes: 163 additions & 0 deletions src/components/common/BottomSheet/RandomMatchingSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import styled from '@emotion/styled'
import { AnimatePresence, motion } from 'framer-motion'
import { MouseEvent, useState } from 'react'
import { AiOutlineClose } from 'react-icons/ai'

import RandomMatchingJoinButton from '@/components/common/Buttons/IconButton/RandomMatchingJoin'
import { Text } from '@/components/common/Text'
import { palette } from '@/styles/palette'

import Timer from './Timer'

const Background = styled.div`
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-end;
overflow-y: hidden;
`

const BottomContentWrapper = styled(motion.div)<{
isDarkMode: boolean
}>`
width: 100%;
display: flex;
flex-direction: column;
height: 378px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
background-color: ${({ isDarkMode }) => (isDarkMode ? palette.GRAY700 : palette.WHITE)};
`

const BottomContentHeader = styled.div<{
isDarkMode: boolean
}>`
width: 100%;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid ${({ isDarkMode }) => (isDarkMode ? palette.GRAY500 : palette.GRAY200)};
background-color: ${({ isDarkMode }) => (isDarkMode ? palette.GRAY700 : palette.WHITE)};
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 24px 0;
`

const BottomContent = styled.div<{
isDarkMode: boolean
}>`
display: flex;
flex-direction: column;
align-items: center;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
background-color: ${({ isDarkMode }) => (isDarkMode ? palette.GRAY700 : palette.WHITE)};
`

type RandomMatchingSheetProps = {
title: string
isDarkMode: boolean
moveToRandomMatching: () => void
cancelRandomMatching: () => void
}

const RandomMatchingSheet = ({
title,
isDarkMode,
moveToRandomMatching,
cancelRandomMatching,
}: RandomMatchingSheetProps) => {
const [isOpen, setIsOpen] = useState(true) // RandomMatchingSheet의 상태

const handleWrapperClick = (e: MouseEvent) => {
e.stopPropagation()
}

const toggleRandomMatchingSheet = () => {
// isOpen이 true일 때만 상태를 토글
if (isOpen) {
cancelRandomMatching()
setIsOpen(!isOpen)
}
}

const slideUp = {
hidden: { y: '100%', opacity: 0 },
visible: { y: '0%', opacity: 1, transition: { type: 'spring', damping: 15, stiffness: 100 } },
partiallyVisible: {
y: '85%',
opacity: 1,
transition: { type: 'spring', damping: 15, stiffness: 100 },
},
exit: { y: '100%', opacity: 0, transition: { type: 'spring', damping: 20, stiffness: 100 } },
}
return (
<AnimatePresence>
<Background onClick={toggleRandomMatchingSheet}>
<BottomContentWrapper
isDarkMode={isDarkMode}
onClick={handleWrapperClick}
initial={'hidden'} // 초기 상태
animate={isOpen ? 'visible' : 'partiallyVisible'} // 상태에 따른 애니메이션 값 지정
exit={'exit'} // 컴포넌트가 unmount될 때 상태
variants={slideUp} // 애니메이션 정의
>
<BottomContent isDarkMode={isDarkMode}>
<BottomContentHeader isDarkMode={isDarkMode}>
<Text
font={'Body_20'}
fontWeight={700}
letterSpacing={-1}
style={{
color: isDarkMode ? palette.DARK_WHITE : palette.BLACK,
textAlign: 'center',
backgroundColor: isDarkMode ? palette.GRAY700 : palette.WHITE,
flex: 1,
}}
>
{title}
</Text>
<AiOutlineClose
style={{
position: 'absolute',
right: 17,
width: 30,
height: 30,
color: isDarkMode ? palette.DARK_WHITE : palette.BLACK,
}}
onClick={toggleRandomMatchingSheet}
/>
</BottomContentHeader>
<Timer
totalTime={30000}
isDarkMode={isDarkMode}
timeOver={() => {
console.log('타이머 종료!')
}}
/>
<RandomMatchingJoinButton
isDarkMode={isDarkMode}
moveToRandomMatching={moveToRandomMatching}
/>
<Text
font={'Body_12'}
fontWeight={700}
letterSpacing={-1}
style={{
marginTop: 20,
color: isDarkMode ? palette.GRAY300 : palette.GRAY500,
}}
>
{'현재 매칭에 참가하지 않으면 다음 매칭에 불이익이 있습니다.'}
</Text>
</BottomContent>
</BottomContentWrapper>
</Background>
</AnimatePresence>
)
}

export default RandomMatchingSheet
Loading