Skip to content

Commit

Permalink
Merge pull request #143 from prgrms-web-devcourse-final-project/142-f…
Browse files Browse the repository at this point in the history
…eature/modal-animation

[Feature] #142 모달 애니메이션
  • Loading branch information
shlee9999 authored Dec 8, 2024
2 parents 96ca6fe + 9d4fc25 commit 8db29cb
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 30 deletions.
40 changes: 38 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@tanstack/react-query": "^5.62.2",
"@tanstack/react-query-devtools": "^5.62.2",
"d3": "^7.9.0",
"framer-motion": "^11.13.1",
"ios-style-picker": "^0.0.6",
"ol": "^10.2.1",
"react": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const MobileContainer = styled.div`
left: 50%;
top: 50%;
translate: -50% -50%;
overflow: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Profile/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type ProfileProps = {
export const Profile = styled.div<ProfileProps>`
width: ${({ $size }) => $size + 'px'};
height: ${({ $size }) => $size + 'px'};
background: url(${({ $src }) => $src}) center/cover ${({ theme }) => theme.colors.brand.sub};
background: url(${({ $src }) => $src}) center/cover ${({ theme }) => theme.colors.brand.lighten_2};
cursor: ${({ $userId }) => ($userId ? 'pointer' : 'default')};
border-radius: 50%;
`
96 changes: 94 additions & 2 deletions src/modals/ModalContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,82 @@
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect } from 'react'
import { AnimationType, useModalStore } from '~stores/modalStore'
import * as S from './styles'
import { useModalStore } from '~stores/modalStore'

const animationVariants = {
none: {
initial: {},
animate: {},
exit: {},
},
fade: {
initial: { opacity: 0, height: '100%' },
animate: { opacity: 1, height: '100%' },
exit: { opacity: 0, height: '100%' },
},
slideUp: {
initial: { y: '100%', height: '100%' },
animate: { y: 0, height: '100%' },
exit: { y: '100%', height: '100%' },
},
slideDown: {
initial: { y: -'100%', height: '100%' },
animate: { y: 0, height: '100%' },
exit: { y: -'100%', height: '100%' },
},
slideLeft: {
initial: { x: '100%', height: '100%' },
animate: { x: 0, height: '100%' },
exit: { x: '100%', height: '100%' },
},
slideRight: {
initial: { x: -'100%', height: '100%' },
animate: { x: 0, height: '100%' },
exit: { x: -'100%', height: '100%' },
},
}

const ModalAnimation = ({
children,
animationType,
index,
}: {
children: React.ReactNode
animationType?: AnimationType
index: number
}) => {
const transitionConfig =
animationType === 'fade'
? {
type: 'tween',
duration: 0.15, // Shorter duration for fade animation
delay: index * 0.05, // Adjusted delay for fade animation
}
: {
type: 'tween',
duration: 0.3,
delay: index * 0.1,
}
return (
<motion.div
variants={animationVariants[animationType || 'fade']}
initial='initial'
animate='animate'
exit='exit'
transition={transitionConfig}
style={{
zIndex: 1000 + index, // 동적 z-index
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
>
{children}
</motion.div>
)
}

export default function ModalContainer() {
const { modalList, popModal } = useModalStore()
Expand All @@ -19,5 +95,21 @@ export default function ModalContainer() {
return () => window.removeEventListener('popstate', preventBack)
}, [modalList])

Check warning on line 96 in src/modals/ModalContainer/index.tsx

View workflow job for this annotation

GitHub Actions / lighthouse

React Hook useEffect has a missing dependency: 'popModal'. Either include it or remove the dependency array

return <>{modalList.length ? <S.ModalWrapper>{...modalList}</S.ModalWrapper> : null}</>
return (
<AnimatePresence>
{modalList.length > 0 && (
<S.ModalWrapper>
{modalList.map((modal, index) => (
<ModalAnimation
key={modal.id || index} // 고유한 key 사용
animationType={modal.animationType}
index={index}
>
{modal.content}
</ModalAnimation>
))}
</S.ModalWrapper>
)}
</AnimatePresence>
)
}
4 changes: 2 additions & 2 deletions src/modals/ModalContainer/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export const ModalWrapper = styled.div`
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
z-index: 999;
/* background-color: rgba(0, 0, 0, 0.4); */
z-index: 900;
`
2 changes: 1 addition & 1 deletion src/modals/RegisterDogModal/DogProfileSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function DogProfileSection() {
showToast(alertMessage)
return
}
pushModal(<DogProfileDetailSection />)
pushModal(<DogProfileDetailSection />, 'slideLeft')
}

const handlePrevClick = () => {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/RegisterPage/Dog/SelectSectionButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function SelectSectionButton({ title, description, src, modal }:
const { pushModal } = useModalStore()

return (
<S.SelectSectionButton onClick={() => pushModal(modal)}>
<S.SelectSectionButton onClick={() => pushModal(modal, 'slideLeft')}>
<S.TypoWrapper>
<Typo20 $weight='700'>{title}</Typo20>
</S.TypoWrapper>
Expand Down
25 changes: 12 additions & 13 deletions src/pages/RegisterPage/Register/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import * as S from './styles'
import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { createRegister } from '~apis/register/createRegister'
import AddOwnerAvatar from '~assets/add-dog-picture.svg'
import { ActionButton } from '~components/Button/ActionButton'
import GenderSelectButton from '~components/GenderSelectButton'
import { Input } from '~components/Input'
import Toast from '~components/Toast'
import { FAMILY_ROLE } from '~constants/familyRole'
import { useGeolocation } from '~hooks/useGeolocation'
import FamilyRoleChoiceModal from '~modals/PositionChoiceModal'
import RegisterAvatarModal from '~modals/RegisterAvatarModal'
import { useModalStore } from '~stores/modalStore'
import { ActionButton } from '~components/Button/ActionButton'
import FamilyRoleChoiceModal from '~modals/PositionChoiceModal'
import { useGeolocation } from '~hooks/useGeolocation'
import { useOwnerProfileStore } from '~stores/ownerProfileStore'
import { validateOwnerProfile } from '~utils/validateOwnerProfile'
import RegisterDogPage from '~pages/RegisterPage/Dog'
import Toast from '~components/Toast'
import { useToastStore } from '~stores/toastStore'
import { useSearchParams } from 'react-router-dom'
import { createRegister } from '~apis/register/createRegister'
import { FamilyRole } from '~types/common'
import { useEffect } from 'react'
import { FAMILY_ROLE } from '~constants/familyRole'
import { validateOwnerProfile } from '~utils/validateOwnerProfile'
import * as S from './styles'

export default function Register() {
const { ownerProfile, setOwnerProfile } = useOwnerProfileStore()
Expand All @@ -27,7 +26,7 @@ export default function Register() {
const [searchParams] = useSearchParams()
const email = searchParams.get('email') || ''
const provider = searchParams.get('provider') || ''

const navigate = useNavigate()
const handleNextClick = async () => {
const alertMessage = validateOwnerProfile(ownerProfile)
if (alertMessage) {
Expand Down Expand Up @@ -55,7 +54,7 @@ export default function Register() {
if (response.code === 201) {
//? 채팅 구현을 위해 임의로 추가한 부분입니다.
setOwnerProfile({ memberId: response.data.memberId })
pushModal(<RegisterDogPage />)
navigate('/register/dog')
}
} catch (error) {
showToast(error instanceof Error ? error.message : '회원가입에 실패했습니다')
Expand Down
20 changes: 13 additions & 7 deletions src/stores/modalStore.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
import { ReactNode } from 'react'
import { create } from 'zustand'

export type AnimationType = 'none' | 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight'

interface ModalItem {
id: string
content: ReactNode
animationType?: AnimationType
}

interface ModalStore {
modalList: ReactNode[]
pushModal: (modal: ReactNode) => void
modalList: ModalItem[]
pushModal: (modal: ReactNode, animationType?: AnimationType) => void
popModal: () => void
clearModal: () => void
}

export const useModalStore = create<ModalStore>((set, get) => ({
modalList: [],
pushModal: modal => {
pushModal: (content, animationType) => {
const id = `modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
set(state => ({
modalList: [...state.modalList, modal],
modalList: [...state.modalList, { id, content, animationType }],
}))
// 모달이 추가될 때 새로운 히스토리 항목 생성
if (get().modalList.length > 0) {
window.history.pushState({ modal: true }, '', window.location.href)
}
},
popModal: () => {
// 모달이 제거될 때 히스토리 뒤로가기
set(state => ({
modalList: state.modalList.slice(0, -1),
}))
},
clearModal: () => {
set({ modalList: [] })
// 모든 모달 제거 시 히스토리 초기화
const modalCount = get().modalList.length
if (modalCount) window.history.go(-modalCount)
},
Expand Down

0 comments on commit 8db29cb

Please sign in to comment.