Skip to content

Commit

Permalink
Merge branch 'main' into refactor/#461
Browse files Browse the repository at this point in the history
  • Loading branch information
splitCoding authored Sep 27, 2023
2 parents 62521fe + e23e1c3 commit c073a2d
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 68 deletions.
42 changes: 31 additions & 11 deletions frontend/src/features/songs/components/CarouselItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { styled } from 'styled-components';
import emptyPlay from '@/assets/icon/empty-play.svg';
import { useAuthContext } from '@/features/auth/components/AuthProvider';
import LoginModal from '@/features/auth/components/LoginModal';
import Thumbnail from '@/features/songs/components/Thumbnail';
import useModal from '@/shared/components/Modal/hooks/useModal';
import Spacing from '@/shared/components/Spacing';
import ROUTE_PATH from '@/shared/constants/path';
import { toMinSecText } from '@/shared/utils/convertTime';
Expand All @@ -13,16 +17,32 @@ interface CarouselItemProps {
const CarouselItem = ({ votingSong }: CarouselItemProps) => {
const { id, singer, title, videoLength, albumCoverUrl } = votingSong;

const { isOpen, openModal, closeModal } = useModal();

const { user } = useAuthContext();
const isLoggedIn = !!user;

const navigate = useNavigate();
const goToPartCollectingPage = () => navigate(`${ROUTE_PATH.COLLECT}/${id}`);

return (
<Wrapper>
<CollectingLink to={`${ROUTE_PATH.COLLECT}/${id}`}>
<Album src={albumCoverUrl} />
<LoginModal
message={
'슉에서 당신만의 킬링파트를 등록해보세요!\n당신이 등록한 구간이 대표 킬링파트가 될 수 있어요!'
}
isOpen={isOpen}
closeModal={closeModal}
/>

<CollectingLink onClick={isLoggedIn ? goToPartCollectingPage : openModal}>
<Thumbnail src={albumCoverUrl} size="xl" borderRadius={4} />
<Spacing direction={'horizontal'} size={24} />
<Contents>
<Title>{title}</Title>
<Singer>{singer}</Singer>
<PlayingTime>
<img src={emptyPlay} />
<PlayIcon src={emptyPlay} />
<PlayingTimeText>{toMinSecText(videoLength)}</PlayingTimeText>
</PlayingTime>
</Contents>
Expand All @@ -38,18 +58,12 @@ const Wrapper = styled.li`
min-width: 350px;
`;

const CollectingLink = styled(Link)`
const CollectingLink = styled.a`
display: flex;
justify-content: center;
padding: 10px;
`;

const Album = styled.img`
max-width: 120px;
background-color: white;
border-radius: 4px;
`;

const Contents = styled.div`
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -91,3 +105,9 @@ const PlayingTime = styled.div`
const PlayingTimeText = styled.p`
padding-top: 2px;
`;

const PlayIcon = styled.img`
width: 16px;
height: 16px;
margin: auto;
`;
8 changes: 4 additions & 4 deletions frontend/src/features/songs/components/CollectionCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const CollectionCarousel = ({ children }: CarouselProps) => {
</CarouselWrapper>
<IndicatorWrapper aria-hidden>
{Array.from({ length: numberOfItems }, (_, idx) => (
<Dot key={idx} isActive={idx === currentIndex} />
<Dot key={idx} $isActive={idx === currentIndex} />
))}
</IndicatorWrapper>
</Wrapper>
Expand Down Expand Up @@ -91,10 +91,10 @@ const IndicatorWrapper = styled.div`
background-color: transparent;
`;

const Dot = styled.div<{ isActive: boolean }>`
const Dot = styled.div<{ $isActive: boolean }>`
width: 8px;
height: 8px;
background-color: ${({ isActive, theme: { color } }) =>
isActive ? color.primary : color.secondary};
background-color: ${({ $isActive, theme: { color } }) =>
$isActive ? color.primary : color.secondary};
border-radius: 50%;
`;
26 changes: 25 additions & 1 deletion frontend/src/features/songs/components/KillingPartTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContex
import useModal from '@/shared/components/Modal/hooks/useModal';
import useTimerContext from '@/shared/components/Timer/hooks/useTimerContext';
import useToastContext from '@/shared/components/Toast/hooks/useToastContext';
import { GA_ACTIONS, GA_CATEGORIES } from '@/shared/constants/GAEventName';
import sendGAEvent from '@/shared/googleAnalytics/sendGAEvent';
import { toPlayingTimeText } from '@/shared/utils/convertTime';
import copyClipboard from '@/shared/utils/copyClipBoard';
import formatOrdinals from '@/shared/utils/formatOrdinals';
Expand Down Expand Up @@ -49,6 +51,12 @@ const KillingPartTrack = ({
const partLength = end - start;

const copyKillingPartUrl = async () => {
sendGAEvent({
action: GA_ACTIONS.COPY_URL,
category: GA_CATEGORIES.SONG_DETAIL,
memberId: user?.memberId,
});

await copyClipboard(partVideoUrl);
showToast('영상 링크가 복사되었습니다.');
};
Expand Down Expand Up @@ -80,13 +88,29 @@ const KillingPartTrack = ({
};

const toggleTrackPlayAndStop = () => {
sendGAEvent({
action: GA_ACTIONS.PLAY,
category: GA_CATEGORIES.SONG_DETAIL,
memberId: user?.memberId,
});

if (isNowPlayingTrack) {
stopTrack();
} else {
playTrack();
}
};

const toggleLike = () => {
sendGAEvent({
action: GA_ACTIONS.LIKE,
category: GA_CATEGORIES.SONG_DETAIL,
memberId: user?.memberId,
});

toggleKillingPartLikes();
};

return (
<Container
$isNowPlayingTrack={isNowPlayingTrack}
Expand All @@ -110,7 +134,7 @@ const KillingPartTrack = ({
</FLexContainer>
<ButtonContainer>
<LikeButton
onClick={isLoggedIn ? toggleKillingPartLikes : openModal}
onClick={isLoggedIn ? toggleLike : openModal}
aria-label={`${rank}등 킬링파트 좋아요 하기`}
>
<ButtonIcon src={heartIcon} alt="" />
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/features/songs/components/Thumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,27 @@ import type { ImgHTMLAttributes, SyntheticEvent } from 'react';

interface ThumbnailProps extends ImgHTMLAttributes<HTMLImageElement> {
size?: Size;
borderRadius?: number;
}

const Thumbnail = ({ size = 'lg', ...props }: ThumbnailProps) => {
const Thumbnail = ({ size = 'lg', borderRadius = 4, ...props }: ThumbnailProps) => {
const insertDefaultJacket = ({ currentTarget }: SyntheticEvent<HTMLImageElement>) => {
currentTarget.src = defaultAlbumJacket;
};

return (
<Wrapper $size={size}>
<Wrapper $size={size} $borderRadius={borderRadius}>
<img {...props} alt="노래 앨범" aria-hidden loading="lazy" onError={insertDefaultJacket} />
</Wrapper>
);
};

export default Thumbnail;

const Wrapper = styled.div<{ $size: Size }>`
const Wrapper = styled.div<{ $size: Size; $borderRadius: number }>`
overflow: hidden;
${({ $size }) => SIZE_VARIANTS[$size]};
border-radius: 4px;
border-radius: ${({ $borderRadius }) => $borderRadius}px;
`;

const SIZE_VARIANTS = {
Expand Down
61 changes: 32 additions & 29 deletions frontend/src/features/songs/components/VoteInterface.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { styled } from 'styled-components';
import { useAuthContext } from '@/features/auth/components/AuthProvider';
import LoginModal from '@/features/auth/components/LoginModal';
import useVoteInterfaceContext from '@/features/songs/hooks/useVoteInterfaceContext';
import VideoSlider from '@/features/youtube/components/VideoSlider';
import useVideoPlayerContext from '@/features/youtube/hooks/useVideoPlayerContext';
Expand All @@ -19,10 +18,10 @@ const VoteInterface = () => {
const { videoPlayer } = useVideoPlayerContext();

const { createKillingPart } = usePostKillingPart();
const { user } = useAuthContext();
const { isOpen, openModal, closeModal } = useModal();

const isLoggedIn = !!user;
const { user } = useAuthContext();

const voteTimeText = toPlayingTimeText(partStartTime, partStartTime + interval);

const submitKillingPart = async () => {
Expand All @@ -40,39 +39,35 @@ const VoteInterface = () => {
return (
<Container>
<RegisterTitle>당신의 킬링파트를 등록하세요</RegisterTitle>
<Spacing direction="vertical" size={4} />
<Warning>같은 파트에 대한 여러 번의 등록은 한 번의 등록으로 처리됩니다.</Warning>
<Spacing direction="vertical" size={16} />
<KillingPartToggleGroup />
<Spacing direction="vertical" size={24} />
<VideoSlider />
<Spacing direction="vertical" size={16} />
<Register type="button" onClick={isLoggedIn ? submitKillingPart : openModal}>
<Register type="button" onClick={submitKillingPart}>
등록
</Register>
{isLoggedIn ? (
<Modal isOpen={isOpen} closeModal={closeModal}>
<ModalTitle>킬링파트 등록을 완료했습니다.</ModalTitle>
<ModalContent>
<Message>{voteTimeText}</Message>
<Message>파트를 공유해 보세요😀</Message>
</ModalContent>
<ButtonContainer>
<Confirm type="button" onClick={closeModal}>
확인
</Confirm>
<Share type="button" onClick={copyPartVideoUrl}>
공유하기
</Share>
</ButtonContainer>
</Modal>
) : (
<LoginModal
message={
'슉에서 당신만의 킬링파트를 등록해보세요!\n당신이 등록한 구간이 대표 킬링파트가 될 수 있어요!'
}
isOpen={isOpen}
closeModal={closeModal}
/>
)}

<Modal isOpen={isOpen} closeModal={closeModal}>
<ModalTitle>
<TitleColumn>{user?.nickname}님의</TitleColumn>
<TitleColumn>킬링파트 등록을 완료했습니다.</TitleColumn>
</ModalTitle>
<ModalContent>
<Message>{voteTimeText}</Message>
<Message>파트를 공유해 보세요😀</Message>
</ModalContent>
<ButtonContainer>
<Confirm type="button" onClick={closeModal}>
확인
</Confirm>
<Share type="button" onClick={copyPartVideoUrl}>
공유하기
</Share>
</ButtonContainer>
</Modal>
</Container>
);
};
Expand Down Expand Up @@ -110,6 +105,10 @@ const Register = styled.button`

const ModalTitle = styled.h3``;

const TitleColumn = styled.div`
text-align: center;
`;

const ModalContent = styled.div`
padding: 16px 0;
Expand Down Expand Up @@ -147,3 +146,7 @@ const ButtonContainer = styled.div`
gap: 16px;
width: 100%;
`;

const Warning = styled.div`
color: ${({ theme: { color } }) => color.subText};
`;
30 changes: 21 additions & 9 deletions frontend/src/pages/MyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import Flex from '@/shared/components/Flex';
import Spacing from '@/shared/components/Spacing';
import SRHeading from '@/shared/components/SRHeading';
import useToastContext from '@/shared/components/Toast/hooks/useToastContext';
import { GA_ACTIONS, GA_CATEGORIES } from '@/shared/constants/GAEventName';
import ROUTE_PATH from '@/shared/constants/path';
import sendGAEvent from '@/shared/googleAnalytics/sendGAEvent';
import useFetch from '@/shared/hooks/useFetch';
import fetcher from '@/shared/remotes';
import { secondsToMinSec, toPlayingTimeText } from '@/shared/utils/convertTime';
Expand Down Expand Up @@ -38,11 +40,22 @@ const MyPage = () => {
const navigate = useNavigate();

const logoutRedirect = () => {
sendGAEvent({
action: GA_ACTIONS.LOGOUT,
category: GA_CATEGORIES.MY_PAGE,
memberId: user?.memberId,
});
logout();
navigate(ROUTE_PATH.ROOT);
};

const goEditPage = () => {
sendGAEvent({
action: GA_ACTIONS.EDIT_PROFILE,
category: GA_CATEGORIES.MY_PAGE,
memberId: user?.memberId,
});

navigate(`/${ROUTE_PATH.EDIT_PROFILE}`);
};

Expand Down Expand Up @@ -181,18 +194,17 @@ type LikePartItemProps = LikeKillingPart & {
rank: number;
};

const LikePartItem = ({
songId,
albumCoverUrl,
title,
singer,
// partId,
start,
end,
}: LikePartItemProps) => {
const LikePartItem = ({ songId, albumCoverUrl, title, singer, start, end }: LikePartItemProps) => {
const { showToast } = useToastContext();
const { user } = useAuthContext();

const shareUrl = () => {
sendGAEvent({
action: GA_ACTIONS.COPY_URL,
category: GA_CATEGORIES.MY_PAGE,
memberId: user?.memberId,
});

copyClipboard(`${BASE_URL?.replace('/api', '')}/songs/${songId}`);
showToast('클립보드에 영상링크가 복사되었습니다.');
};
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ const router = createBrowserRouter([
},
{
path: `${ROUTE_PATH.COLLECT}/:id`,
element: <PartCollectingPage />,
element: (
<AuthLayout>
<PartCollectingPage />
</AuthLayout>
),
},
{
path: `${ROUTE_PATH.SONG_DETAILS}/:id/:genre`,
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/shared/constants/GAEventName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const GA_ACTIONS = {
COPY_URL: 'click_copy_part',
PLAY: 'click_play_part',
LIKE: 'click_like',
EDIT_PROFILE: 'click_edit_profile',
LOGOUT: 'click_logout',
};

export const GA_CATEGORIES = {
SONG_DETAIL: 'song_playing',
MY_PAGE: 'profile',
};

export const GA_MEMBER = {
NOT_LOGGED_IN: -1,
};
Loading

0 comments on commit c073a2d

Please sign in to comment.