-
Notifications
You must be signed in to change notification settings - Fork 2
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
Feat/#499 등록 ui 개선 및 등록 페이지 리팩터링 #508
Changes from 87 commits
10bd732
7d55392
c973da2
6be7ada
436abdb
9e2cfd5
913e23b
f08767c
b2af6e2
3e58646
aa0b9a7
905d31e
be1a18a
228ceac
6b939c5
2e1795b
0cd252e
607ec8f
e054219
e00119b
eb8e960
709cdd5
bd956bd
8db7a4c
8b2ff86
251865d
3be1447
271da9a
0d83fa9
b85f8b8
dfa6528
ed1a762
7ec406c
7780231
598fe13
20e8cdf
71f5453
318b79b
f4f6145
ac59d22
bd026d0
225a688
df40820
a1f8881
c32c44d
676ad40
b9f75b9
192451b
1f72890
78eead5
3784a9a
adf5d4c
b63c47e
ee81ed7
f681587
ce3d7c1
9800dca
7c4d0d7
22c605f
d15f4bf
f7b923f
4225145
482ca98
33e8f6a
c6df2b8
92e7542
ef4142e
a4f1934
dcd0b5e
c1dfbb1
6505614
f1a42ff
9a490d8
dd52fc7
92afe46
c3711ba
89b2b9c
69e4614
ad9a407
44fd17a
1f22eca
7251648
d119c11
e77d50a
7065662
615cb57
f21a517
c4c16f1
572d1b8
07bb3a1
6825ec0
63a8bcc
f2baad4
50b2a2e
955b6d3
5b8d48b
2634432
0e55af8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { useNavigate } from 'react-router-dom'; | ||
import styled from 'styled-components'; | ||
import { useAuthContext } from '@/features/auth/components/AuthProvider'; | ||
import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; | ||
import { usePostKillingPart } from '@/features/killingParts/remotes/usePostKillingPart'; | ||
import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; | ||
import useModal from '@/shared/components/Modal/hooks/useModal'; | ||
import Modal from '@/shared/components/Modal/Modal'; | ||
import Spacing from '@/shared/components/Spacing'; | ||
import { toPlayingTimeText } from '@/shared/utils/convertTime'; | ||
|
||
const RegisterPart = () => { | ||
const { isOpen, openModal, closeModal } = useModal(); | ||
const { user } = useAuthContext(); | ||
const { interval, partStartTime, songId } = useCollectingPartContext(); | ||
const video = useVideoPlayerContext(); | ||
const { createKillingPart } = usePostKillingPart(); | ||
const navigate = useNavigate(); | ||
|
||
// 현재 useMutation 훅이 response 객체를 리턴하지 않고 내부적으로 처리합니다. | ||
// 때문에 컴포넌트 단에서 createKillingPart 성공 여부에 따라 등록 완료 만료를 처리를 할 수 없어요! | ||
// 현재 비로그인 시에 등록을 누르면 두 개의 모달이 뜹니다.정 | ||
const submitKillingPart = async () => { | ||
video.pause(); | ||
await createKillingPart(songId, { startSecond: partStartTime, length: interval }); | ||
navigate(-1); | ||
}; | ||
|
||
const voteTimeText = toPlayingTimeText(partStartTime, partStartTime + interval); | ||
|
||
return ( | ||
<> | ||
<RegisterButton type="submit" onClick={openModal}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이제 이 버튼을 누르면 비디오 퍼즈 + 모달이 열려야 자연스러울듯 해요. 현재는 -1로 이동하기전 노래를 끄는 함수가 실행됩니당 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 반영완료! |
||
등록 | ||
</RegisterButton> | ||
<Modal isOpen={isOpen} closeModal={closeModal}> | ||
<ModalTitle> | ||
<TitleColumn>{user?.nickname}님의 파트 저장</TitleColumn> | ||
</ModalTitle> | ||
<ModalContent> | ||
<Message> | ||
<Part>{voteTimeText}</Part> | ||
</Message> | ||
<Spacing direction="vertical" size={6} /> | ||
<Message>나만의 파트로 등록하시겠습니까?</Message> | ||
</ModalContent> | ||
<ButtonContainer> | ||
<Confirm type="button" onClick={closeModal}> | ||
취소 | ||
</Confirm> | ||
<Share type="button" onClick={submitKillingPart}> | ||
등록 | ||
</Share> | ||
</ButtonContainer> | ||
</Modal> | ||
</> | ||
); | ||
}; | ||
|
||
export default RegisterPart; | ||
|
||
const RegisterButton = styled.button` | ||
width: 100%; | ||
margin-top: auto; | ||
padding: 8px 11px; | ||
|
||
font-weight: 700; | ||
color: ${({ theme: { color } }) => color.white}; | ||
letter-spacing: 6px; | ||
|
||
background-color: ${({ theme: { color } }) => color.primary}; | ||
border-radius: 6px; | ||
|
||
@media (min-width: ${({ theme }) => theme.breakPoints.md}) { | ||
padding: 11px 15px; | ||
|
||
font-size: 18px; | ||
} | ||
`; | ||
|
||
const ModalTitle = styled.h3``; | ||
|
||
const TitleColumn = styled.div` | ||
text-align: center; | ||
`; | ||
|
||
const ModalContent = styled.div` | ||
padding: 16px 0; | ||
|
||
font-size: 16px; | ||
color: #b5b3bc; | ||
text-align: center; | ||
white-space: pre-line; | ||
`; | ||
|
||
const Message = styled.div``; | ||
|
||
const Button = styled.button` | ||
height: 36px; | ||
color: ${({ theme: { color } }) => color.white}; | ||
border-radius: 10px; | ||
`; | ||
|
||
const Confirm = styled(Button)` | ||
flex: 1; | ||
background-color: ${({ theme: { color } }) => color.secondary}; | ||
`; | ||
|
||
const Share = styled(Button)` | ||
flex: 1; | ||
background-color: ${({ theme: { color } }) => color.primary}; | ||
`; | ||
|
||
const ButtonContainer = styled.div` | ||
display: flex; | ||
gap: 16px; | ||
width: 100%; | ||
`; | ||
|
||
const Part = styled.span` | ||
padding: 6px 11px; | ||
|
||
color: white; | ||
letter-spacing: 1px; | ||
|
||
background-color: ${({ theme: { color } }) => color.disabled}; | ||
border-radius: 10px; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import React, { forwardRef } from 'react'; | ||
import styled from 'styled-components'; | ||
|
||
interface SoundWaveProps { | ||
length: number; | ||
progressWidth: number; | ||
} | ||
// eslint-disable-next-line react/display-name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 SoundWave.displayName =SoundWave There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const SoundWave = forwardRef<HTMLDivElement, SoundWaveProps>( | ||
({ length, progressWidth }, boxRef) => { | ||
const stretchWaveHeight = | ||
(activeHeight: string, inactiveHeight: string) => (dom: HTMLDivElement | null) => { | ||
if (!dom || !boxRef || typeof boxRef === 'function') return; | ||
if (boxRef.current?.scrollLeft) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 제약되는 상황이 아니라면 얼리리턴으로 댑스를 줄여도 좋겠네요! |
||
const boxPos = | ||
boxRef.current?.scrollLeft + boxRef.current?.clientWidth / 2 - progressWidth / 2; | ||
|
||
const containerRightEdge = boxPos + progressWidth; | ||
const itemRightEdge = dom.offsetLeft; | ||
|
||
if (itemRightEdge >= boxPos && itemRightEdge <= containerRightEdge) { | ||
dom.style.height = activeHeight; | ||
} else { | ||
dom.style.height = inactiveHeight; | ||
} | ||
} | ||
}; | ||
|
||
return Array.from({ length }, (_, index) => ( | ||
<React.Fragment key={index}> | ||
<LongBar ref={stretchWaveHeight('25px', '20px')} /> | ||
<ShortBar ref={stretchWaveHeight('17px', '12px')} /> | ||
</React.Fragment> | ||
)); | ||
} | ||
); | ||
|
||
export default SoundWave; | ||
|
||
const LongBar = styled.div` | ||
z-index: 2; | ||
left: 50%; | ||
|
||
width: 4px; | ||
height: 24px; | ||
|
||
background-color: ${({ theme: { color } }) => color.white}; | ||
border-radius: 5px; | ||
|
||
transition: height 0.2s ease; | ||
`; | ||
|
||
const ShortBar = styled.div` | ||
z-index: 2; | ||
|
||
width: 4px; | ||
height: 15px; | ||
|
||
background-color: ${({ theme: { color } }) => color.white}; | ||
border-radius: 5px; | ||
|
||
transition: height 0.2s ease; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import styled, { css, keyframes } from 'styled-components'; | ||
import playIcon from '@/assets/icon/fill-play.svg'; | ||
import pauseIcon from '@/assets/icon/pause.svg'; | ||
import pinIcon from '@/assets/icon/pin.svg'; | ||
import playStreamIcon from '@/assets/icon/play-stream.svg'; | ||
import removeIcon from '@/assets/icon/remove.svg'; | ||
import useCollectingPartContext from '@/features/killingParts/hooks/useCollectingPartContext'; | ||
import usePin from '@/features/killingParts/hooks/usePin'; | ||
import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext'; | ||
import Flex from '@/shared/components/Flex/Flex'; | ||
import { toMinSecText } from '@/shared/utils/convertTime'; | ||
|
||
const VideoBadges = () => { | ||
const { partStartTime, isPlayingEntire, toggleEntirePlaying } = useCollectingPartContext(); | ||
const { | ||
pinList, | ||
isPinListEmpty, | ||
activePinIndex, | ||
pinContainerRef, | ||
pinAnimationRef, | ||
addPin, | ||
deletePin, | ||
playPin, | ||
} = usePin(); | ||
const video = useVideoPlayerContext(); | ||
|
||
const partStartTimeText = toMinSecText(partStartTime); | ||
const isPaused = video.playerState === YT.PlayerState.PAUSED; | ||
const videoPlay = () => { | ||
if (isPlayingEntire) { | ||
video.play(); | ||
} else { | ||
video.seekTo(partStartTime); | ||
} | ||
}; | ||
|
||
const videoPause = () => { | ||
video.pause(); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Flex $gap={14} $justify="flex-end"> | ||
<StartBadge> | ||
<img src={playStreamIcon} style={{ marginRight: '4px' }} alt="" /> | ||
{partStartTimeText} | ||
</StartBadge> | ||
<Badge as="button" onClick={addPin} $isActive={!isPinListEmpty}> | ||
<img src={pinIcon} alt="나만의 파트 임시 저장" /> | ||
</Badge> | ||
<Badge as="button" type="button" onClick={isPaused ? videoPlay : videoPause}> | ||
<img src={isPaused ? playIcon : pauseIcon} alt={'재생 혹은 정지'} /> | ||
</Badge> | ||
<Badge as="button" type="button" $isActive={isPlayingEntire} onClick={toggleEntirePlaying}> | ||
전체 듣기 | ||
</Badge> | ||
</Flex> | ||
<PinFlex $gap={4} ref={pinContainerRef}> | ||
{!isPinListEmpty && ( | ||
<DeleteBadge as="button" onClick={deletePin}> | ||
<img src={removeIcon} alt="나만의 파트 임시 저장 삭제하기" /> | ||
</DeleteBadge> | ||
)} | ||
<PinInner $gap={4} ref={pinContainerRef}> | ||
{pinList.map((pin, index) => ( | ||
<PinBadge | ||
key={pin.text + pinAnimationRef.current} | ||
as="button" | ||
onClick={playPin(pin.partStartTime, pin.interval)} | ||
$isActive={index === activePinIndex} | ||
$isNew={index === 0 && index === activePinIndex} | ||
> | ||
{pin.text} | ||
</PinBadge> | ||
))} | ||
</PinInner> | ||
</PinFlex> | ||
</> | ||
); | ||
}; | ||
export default VideoBadges; | ||
|
||
const PinFlex = styled(Flex)` | ||
width: 100%; | ||
`; | ||
|
||
const PinInner = styled(Flex)` | ||
overflow-x: scroll; | ||
width: calc(100% - 44px); | ||
`; | ||
|
||
const Badge = styled.span<{ $isActive?: boolean }>` | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
|
||
min-width: 40px; | ||
height: 30px; | ||
padding: 0 10px; | ||
|
||
font-size: 14px; | ||
color: ${({ theme: { color } }) => color.white}; | ||
text-align: center; | ||
|
||
background-color: ${({ theme: { color }, $isActive }) => | ||
$isActive ? color.magenta700 : color.disabled}; | ||
border-radius: 40px; | ||
|
||
transition: | ||
background-color 0.2s ease-in, | ||
box-shadow 0.2s ease; | ||
|
||
&:active { | ||
box-shadow: 0 0 0 1px inset white; | ||
} | ||
|
||
@media (min-width: ${({ theme }) => theme.breakPoints.md}) { | ||
font-size: 16px; | ||
} | ||
`; | ||
|
||
const StartBadge = styled(Badge)` | ||
margin-right: auto; | ||
letter-spacing: 1px; | ||
`; | ||
|
||
const slideFirstItem = keyframes` | ||
from { | ||
opacity: 0; | ||
transform: translateX(-30px); | ||
|
||
} | ||
to { | ||
opacity: 1; | ||
transform: translateX(0); | ||
} | ||
`; | ||
|
||
const slideRestItems = keyframes` | ||
from { | ||
transform: translateX(-15px); | ||
} | ||
to { | ||
transform: translateX(0); | ||
} | ||
`; | ||
|
||
const PinBadge = styled(Badge)<{ $isActive?: boolean; $isNew?: boolean }>` | ||
z-index: ${({ $isActive }) => ($isActive ? 1 : 0)}; | ||
|
||
width: 50px; | ||
margin-right: 4px; | ||
|
||
font-size: 12px; | ||
color: black; | ||
white-space: nowrap; | ||
|
||
opacity: ${({ $isActive }) => ($isActive ? 1 : 0.5)}; | ||
background-color: ${({ theme: { color }, $isActive }) => | ||
$isActive ? color.magenta700 : color.disabledBackground}; | ||
border: none; | ||
border-radius: 4px; | ||
|
||
transition: background-color 0.5s ease-in-out; | ||
animation: ${({ $isNew }) => | ||
$isNew | ||
? css` | ||
${slideFirstItem} 1s forwards | ||
` | ||
: css` | ||
${slideRestItems} 0.5s forwards | ||
`}; | ||
`; | ||
|
||
const DeleteBadge = styled(Badge)` | ||
min-width: 30px; | ||
height: 30px; | ||
margin-right: 10px; | ||
padding: 0; | ||
|
||
border-radius: 50%; | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💬 -1 은 뒤로가기의 의미이니 '듣기 페이지로 이동' 이라는 의미를 조금 더 확실히 해주면 어떨까요?
💬 추가로 -1은 어떤 페이지에서 접속하든 이전 히스토리로 이동하기 때문에, 이에대한 이슈도 있을 것 같아요.
songId도 사용할 수 있으니 명시해도 좋을 것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장르는 ALL 때릴생각을 하고 이야기했었는데, 우코의 생각이 더 좋은것 같아요! ㅎㅎ
대화를 통해 해결했습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[대화 정리]