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

좋아요한 템플릿 목록 페이지 생성 및 캐로셀 스크롤로 변경 #872

Merged
merged 9 commits into from
Oct 24, 2024
1 change: 1 addition & 0 deletions frontend/src/api/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export const QUERY_KEY = {
MEMBER_NAME: 'memberName',
TAG_LIST: 'tagList',
TEMPLATE: 'template',
LIKED_TEMPLATE: 'likedTemplate',
TEMPLATE_LIST: 'templateList',
};
17 changes: 17 additions & 0 deletions frontend/src/api/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ export const getTemplateList = async ({
throw new Error(response.detail);
};

export const getLikedTemplateList = async ({
page = 1,
size = PAGE_SIZE,
sort = DEFAULT_SORTING_OPTION.key,
}: TemplateListRequest) => {
const queryParams = new URLSearchParams({
sort,
page: page.toString(),
size: size.toString(),
});

const response = await apiClient.get(`${END_POINTS.LIKED_TEMPLATES}?${queryParams.toString()}`);
const data = response.json();

return data;
};

export const getTemplateExplore = async ({
sort = DEFAULT_SORTING_OPTION.key,
page = 1,
Expand Down
16 changes: 10 additions & 6 deletions frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useAuth } from '@/hooks/authentication/useAuth';
import { usePressESC } from '@/hooks/usePressESC';
import { useScrollDisable } from '@/hooks/useScrollDisable';
import { useLogoutMutation } from '@/queries/authentication/useLogoutMutation';
import { END_POINTS } from '@/routes';
import { ROUTE_END_POINT } from '@/routes/endPoints';
import { trackClickNewTemplate } from '@/service/amplitude';

import { theme } from '../../style/theme';
Expand Down Expand Up @@ -52,7 +52,7 @@ const Header = ({ headerRef }: { headerRef: React.RefObject<HTMLDivElement> }) =
return;
}

navigate(END_POINTS.TEMPLATES_UPLOAD);
navigate(ROUTE_END_POINT.TEMPLATES_UPLOAD);
};

return (
Expand All @@ -62,9 +62,13 @@ const Header = ({ headerRef }: { headerRef: React.RefObject<HTMLDivElement> }) =
<S.HeaderMenu menuOpen={isMenuOpen}>
<S.NavContainer>
{!isChecking && isLogin && memberId && (
<NavOption route={END_POINTS.memberTemplates(memberId)} name='내 템플릿' />
<>
<NavOption route={ROUTE_END_POINT.memberTemplates(memberId)} name='내 템플릿' />
<NavOption route={ROUTE_END_POINT.memberLikedTemplates(memberId)} name={`좋아요한 템플릿`} />
</>
)}
<NavOption route={END_POINTS.TEMPLATES_EXPLORE} name='구경가기' />
<NavOption route={ROUTE_END_POINT.TEMPLATES_EXPLORE} name='구경가기' />

<ContactUs />
</S.NavContainer>
<S.NavContainer>
Expand Down Expand Up @@ -106,7 +110,7 @@ const Logo = () => {
const isLandingPage = location.pathname === '/';

return (
<Link to={END_POINTS.HOME}>
<Link to={ROUTE_END_POINT.HOME}>
<Flex align='center' gap='0.5rem'>
<CodeZapLogo aria-label='로고 버튼' />
<Heading.XSmall color={isLandingPage ? theme.color.light.primary_500 : theme.color.light.secondary_800}>
Expand Down Expand Up @@ -150,7 +154,7 @@ const LogoutButton = () => {
};

const LoginButton = () => (
<Link to={END_POINTS.LOGIN}>
<Link to={ROUTE_END_POINT.LOGIN}>
<Button variant='text' size='medium' weight='bold' hoverStyle='none'>
로그인
</Button>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { Outlet, useLocation } from 'react-router-dom';

import { Footer, Header } from '@/components';
import { Footer, Header, ScrollTopButton } from '@/components';
import { useHeaderHeight } from '@/hooks/useHeaderHeight';
import { NotFoundPage } from '@/pages';

Expand Down Expand Up @@ -36,6 +36,7 @@ const Layout = () => {
)}
</QueryErrorResetBoundary>
</S.Wrapper>
<ScrollTopButton />
<Footer />
</S.LayoutContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';

export const NoSearchResultsContainer = styled.div`
export const NoResultsContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/NoResults/NoResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PropsWithChildren } from 'react';

import { ZapzapCuriousLogo } from '@/assets/images';
import { theme } from '@/style/theme';

import { Text } from '..';
import * as S from './NoResults.style';

const NoResults = ({ children }: PropsWithChildren) => (
<S.NoResultsContainer>
<ZapzapCuriousLogo width={48} height={48} />
<Text.Large color={theme.color.light.secondary_700}>{children}</Text.Large>
</S.NoResultsContainer>
);

export default NoResults;
14 changes: 0 additions & 14 deletions frontend/src/components/NoSearchResults/NoSearchResults.tsx

This file was deleted.

26 changes: 24 additions & 2 deletions frontend/src/components/ScrollTopButton/ScrollTopButton.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,42 @@ import styled from '@emotion/styled';

import { theme } from '@/style/theme';

export const ScrollTopButton = styled.button`
export const ScrollTopButtonContainer = styled.button<{ isVisible: boolean; isTemplateUpload: boolean }>`
cursor: pointer;

position: fixed;
right: 2rem;
bottom: 2rem;
bottom: ${({ isTemplateUpload }) => (isTemplateUpload ? '4rem' : '2rem')};
transform: translateY(${({ isVisible }) => (isVisible ? '0' : '20px')});

display: flex;
align-items: center;
justify-content: center;

padding: 0.75rem;

visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
opacity: ${({ isVisible }) => (isVisible ? 1 : 0)};
background-color: ${theme.color.light.primary_500};
border: none;
border-radius: 100%;

transition: all 0.3s ease-in-out;

&:hover {
transform: ${({ isVisible }) => (isVisible ? 'translateY(-5px)' : 'translateY(20px)')};
}
`;

export const Sentinel = styled.div`
pointer-events: none;

position: absolute;
top: 450px;
left: 0;

width: 100%;
height: 1px;

opacity: 0;
`;
71 changes: 62 additions & 9 deletions frontend/src/components/ScrollTopButton/ScrollTopButton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,69 @@
import { useEffect, useState, useRef } from 'react';
import { useLocation } from 'react-router-dom';

import { ArrowUpIcon } from '@/assets/images';
import { useWindowWidth } from '@/hooks';
import { END_POINTS } from '@/routes';
import { BREAKING_POINT } from '@/style/styleConstants';
import { scroll } from '@/utils';

import * as S from './ScrollTopButton.style';

const ScrollTopButton = () => (
<S.ScrollTopButton
onClick={() => {
scroll.top('smooth');
}}
>
<ArrowUpIcon aria-label='맨 위로' />
</S.ScrollTopButton>
);
const ScrollTopButton = () => {
const location = useLocation();
const [isVisible, setIsVisible] = useState(false);
const sentinelRef = useRef<HTMLDivElement>(null);

const windowWidth = useWindowWidth();

const isMobile = windowWidth <= BREAKING_POINT.MOBILE;
const isTemplateUpload = isMobile && location.pathname === END_POINTS.TEMPLATES_UPLOAD;

useEffect(() => {
const sentinel = sentinelRef.current;

if (!sentinel) {
return;
}

const observerOptions: IntersectionObserverInit = {
root: null,
rootMargin: '0px',
threshold: [0, 1.0],
};

const observerCallback: IntersectionObserverCallback = (entries) => {
// entries[0].isIntersecting이 false일 때 (sentinel이 화면에서 벗어났을 때) 버튼 보여줌
const entry = entries[0];
const shouldShow = !entry.isIntersecting || entry.intersectionRatio < 1;

void setIsVisible(shouldShow);
};

const observer = new IntersectionObserver(observerCallback, observerOptions);

observer.observe(sentinel);

// eslint-disable-next-line consistent-return
return () => {
observer.disconnect();
};
}, []);
Comment on lines +22 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이렇게 로직을 쓰는군요?!


return (
<>
<S.Sentinel ref={sentinelRef} aria-hidden='true' />
<S.ScrollTopButtonContainer
isVisible={isVisible}
isTemplateUpload={isTemplateUpload}
onClick={() => {
scroll.top('smooth');
}}
>
<ArrowUpIcon aria-label='맨 위로' />
</S.ScrollTopButtonContainer>
</>
);
};

export default ScrollTopButton;
26 changes: 14 additions & 12 deletions frontend/src/components/TemplateCard/TemplateCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,21 @@ const TemplateCard = ({ template }: Props) => {
</Flex>

<Link to={END_POINTS.template(template.id)}>
<S.EllipsisTextWrapper>
<Text.XLarge color={theme.color.light.secondary_900} weight='bold'>
{title}
</Text.XLarge>
</S.EllipsisTextWrapper>
<Flex direction='column' gap='0.5rem'>
<S.EllipsisTextWrapper>
<Text.XLarge color={theme.color.light.secondary_900} weight='bold'>
{title}
</Text.XLarge>
</S.EllipsisTextWrapper>
Comment on lines +63 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 gap이 추가된걸까요?!


<S.EllipsisTextWrapper>
{description ? (
<Text.Medium color={theme.color.light.secondary_600}>{description}</Text.Medium>
) : (
<S.BlankDescription />
)}
</S.EllipsisTextWrapper>
<S.EllipsisTextWrapper>
{description ? (
<Text.Medium color={theme.color.light.secondary_600}>{description}</Text.Medium>
) : (
<S.BlankDescription />
)}
</S.EllipsisTextWrapper>
</Flex>
</Link>
</Flex>

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export { default as Footer } from './Footer/Footer';
export { default as CategoryDropdown } from './CategoryDropdown/CategoryDropdown';
export { default as CategoryGuide } from './CategoryGuide/CategoryGuide';
export { default as NewCategoryInput } from './NewCategoryInput/NewCategoryInput';
export { default as NoSearchResults } from './NoSearchResults/NoSearchResults';
export { default as NoResults } from './NoResults/NoResults';
export { default as Textarea } from './Textarea/Textarea';
export { default as ContactUs } from './ContactUs/ContactUs';
export { default as Toggle } from './Toggle/Toggle';
Expand Down
Empty file.
40 changes: 40 additions & 0 deletions frontend/src/pages/ForbiddenPage/ForbiddenPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { TigerLogo } from '@/assets/images';
import { Button, Flex, Heading, Text } from '@/components';
import { useCustomNavigate } from '@/hooks';
import { theme } from '@/style/theme';

interface props {
resetError?: () => void;
}

const ForbiddenPage = ({ resetError }: props) => {
const navigate = useCustomNavigate();

return (
<Flex direction='column' gap='3rem' margin='2rem 0 0 0' justify='center' align='center'>
<TigerLogo aria-label='호랑이 로고' />
<Heading.XLarge color={theme.color.light.primary_500}>403 ERROR</Heading.XLarge>
<Flex direction='column' gap='2rem' align='center'>
<Text.XLarge color={theme.color.light.primary_500} weight='bold'>
죄송합니다. 해당 페이지에 접근할 수 있는 권한이 없습니다.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소하지만 '죄송합니다.' 표현이 없어도 될 것 같다는 생각이네요 ㅎㅎ

</Text.XLarge>
<Flex direction='column' justify='center' align='center' gap='1rem'>
<Text.Medium color={theme.color.light.secondary_600} weight='bold'>
입력하신 주소가 정확한지 다시 확인하거나 홈으로 이동해주세요.
</Text.Medium>
</Flex>
</Flex>
<Button
weight='bold'
onClick={() => {
resetError && resetError();
navigate('/');
}}
>
<Text.Medium color={theme.color.light.white}>홈으로 이동</Text.Medium>
</Button>
</Flex>
);
};

export default ForbiddenPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import styled from '@emotion/styled';

export const PageTitle = styled.div`
align-items: center;
justify-content: center;

width: 100%;
padding: 5rem 1rem;

white-space: nowrap;
`;

export const TemplateExplorePageContainer = styled.div<{ cols: number }>`
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(${({ cols }) => cols}, minmax(0, 1fr));

width: 100%;
max-width: 80rem;
`;

export const MainContainer = styled.main`
display: flex;

@media (max-width: 1376px) {
gap: clamp(1rem, calc(0.0888 * 100vw - 3.2618rem), 4.375rem);
}

@media (max-width: 768px) {
gap: 0;
}
`;

export const TemplateListSectionWrapper = styled.div`
position: relative;
width: 100%;
`;
Loading
Loading