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

인기 토픽 배너 수정 및 url 로 필터 정보 저장하도록 변경 #866

Merged
merged 9 commits into from
Oct 23, 2024
Binary file added frontend/src/assets/images/topic_algorithm.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_android.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_java.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_js.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_kt.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_precourse.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_react.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_spring.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/topic_wooteco.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const CopyButton = styled(Button)`
`;

export const SourceCodeWrapper = styled.div<{ isOpen: boolean }>`
overflow: hidden;
overflow: scroll;
max-height: ${({ isOpen }) => (isOpen ? '1000rem' : '0')};
animation: ${({ isOpen }) => (!isOpen ? 'collapse' : 'expand')} 0.7s ease-in-out forwards;
`;
41 changes: 23 additions & 18 deletions frontend/src/pages/MyTemplatesPage/hooks/useFilteredTemplateList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect } from 'react';

import { useDropdown, useQueryParams } from '@/hooks';
import { useAuth } from '@/hooks/authentication';
Expand All @@ -16,11 +16,11 @@ interface Props {
export const useFilteredTemplateList = ({ memberId: passedMemberId }: Props) => {
const { queryParams, updateQueryParams } = useQueryParams();

const [selectedCategoryId, setSelectedCategoryId] = useState<number>(queryParams.category);
const [selectedTagIds, setSelectedTagIds] = useState<number[]>(queryParams.tags);
const { keyword, debouncedKeyword, handleKeywordChange } = useSearchKeyword(queryParams.keyword);
const selectedCategoryId = queryParams.category;
const selectedTagIds = queryParams.tags;
const page = queryParams.page;
const { currentValue: sortingOption, ...dropdownProps } = useDropdown(getSortingOptionByValue(queryParams.sort));
const [page, setPage] = useState<number>(queryParams.page);
const { keyword, debouncedKeyword, handleKeywordChange } = useSearchKeyword(queryParams.keyword);

const { memberInfo } = useAuth();
const memberId = passedMemberId ?? memberInfo.memberId;
Expand All @@ -42,39 +42,44 @@ export const useFilteredTemplateList = ({ memberId: passedMemberId }: Props) =>
const totalPages = templateData?.totalPages || 0;

useEffect(() => {
updateQueryParams({ keyword: debouncedKeyword, sort: sortingOption.value, page });
}, [debouncedKeyword, sortingOption, page, updateQueryParams]);
if (queryParams.sort === sortingOption.value) {
return;
}

updateQueryParams({ sort: sortingOption.value, page: FIRST_PAGE });
}, [queryParams.sort, sortingOption, updateQueryParams]);

useEffect(() => {
if (queryParams.keyword === debouncedKeyword) {
return;
}

updateQueryParams({ keyword: debouncedKeyword, page: FIRST_PAGE });
}, [queryParams.keyword, debouncedKeyword, updateQueryParams]);

const handlePageChange = (page: number) => {
scroll.top('smooth');

setPage(page);
updateQueryParams({ page });
};

const handleCategoryMenuClick = useCallback(
(selectedCategoryId: number) => {
updateQueryParams({ category: selectedCategoryId });

setSelectedCategoryId(selectedCategoryId);

handlePageChange(FIRST_PAGE);
updateQueryParams({ category: selectedCategoryId, page: FIRST_PAGE });
},
[updateQueryParams],
);

const handleTagMenuClick = useCallback(
(selectedTagIds: number[]) => {
updateQueryParams({ tags: selectedTagIds });

setSelectedTagIds(selectedTagIds);
handlePageChange(FIRST_PAGE);
updateQueryParams({ tags: selectedTagIds, page: FIRST_PAGE });
},
[updateQueryParams],
);

const handleSearchSubmit = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handlePageChange(FIRST_PAGE);
updateQueryParams({ page: FIRST_PAGE });
}
};

Expand Down
39 changes: 29 additions & 10 deletions frontend/src/pages/TemplateExplorePage/TemplateExplorePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Link } from 'react-router-dom';

Expand Down Expand Up @@ -28,14 +28,21 @@ import { useHotTopic } from './hooks';
import { TemplateListSectionLoading } from '../MyTemplatesPage/components';
import * as S from './TemplateExplorePage.style';

const FIRST_PAGE = 1;

const getGridCols = (windowWidth: number) => (windowWidth <= 1024 ? 1 : 2);

const TemplateExplorePage = () => {
useTrackPageViewed({ eventName: '[Viewed] 구경가기 페이지' });

const { queryParams, updateQueryParams } = useQueryParams();

const [page, setPage] = useState<number>(queryParams.page);
const page = queryParams.page;

const handlePage = (page: number) => {
updateQueryParams({ page });
};

const [keyword, handleKeywordChange] = useInput(queryParams.keyword);

const debouncedKeyword = useDebounce(keyword, 300);
Expand All @@ -45,20 +52,32 @@ const TemplateExplorePage = () => {
const { selectedTagIds, selectedHotTopic, selectTopic } = useHotTopic();

useEffect(() => {
updateQueryParams({ keyword: debouncedKeyword, sort: sortingOption.value, page });
}, [debouncedKeyword, sortingOption, page, updateQueryParams]);
if (queryParams.sort === sortingOption.value) {
return;
}

updateQueryParams({ sort: sortingOption.value, page: FIRST_PAGE });
}, [queryParams.sort, sortingOption, updateQueryParams]);

useEffect(() => {
if (queryParams.keyword === debouncedKeyword) {
return;
}

updateQueryParams({ keyword: debouncedKeyword, page: FIRST_PAGE });
}, [queryParams.keyword, debouncedKeyword, updateQueryParams]);

const handleSearchSubmit = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
setPage(1);
handlePage(FIRST_PAGE);
}
};

return (
<Flex direction='column' gap='4rem' align='flex-start' css={{ paddingTop: '5rem' }}>
<Flex direction='column' justify='flex-start' gap='1rem' width='100%'>
<Heading.Medium color='black'>
🔥 지금 인기있는 토픽 {selectedHotTopic && `- ${selectedHotTopic} 보는 중`}
{selectedHotTopic ? `🔥 [ ${selectedHotTopic} ] 보는 중` : '🔥 지금 인기있는 토픽'}
</Heading.Medium>
<HotTopicCarousel selectTopic={selectTopic} selectedHotTopic={selectedHotTopic} />
</Flex>
Expand Down Expand Up @@ -91,7 +110,7 @@ const TemplateExplorePage = () => {
>
<TemplateList
page={page}
setPage={setPage}
handlePage={handlePage}
sortingOption={sortingOption}
keyword={debouncedKeyword}
tagIds={selectedTagIds}
Expand All @@ -115,13 +134,13 @@ export default TemplateExplorePage;

const TemplateList = ({
page,
setPage,
handlePage,
sortingOption,
keyword,
tagIds,
}: {
page: number;
setPage: (page: number) => void;
handlePage: (page: number) => void;
sortingOption: SortingOption;
keyword: string;
tagIds: number[];
Expand All @@ -144,7 +163,7 @@ const TemplateList = ({

const handlePageChange = (page: number) => {
scroll.top();
setPage(page);
handlePage(page);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const CarouselItem = styled.li`

width: 18rem;
height: 9rem;
margin: 0.25rem 0;

@media (max-width: 768px) {
width: 9rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BREAKING_POINT } from '@/style/styleConstants';
import * as S from './Carousel.style';

interface CarouselItem {
id: number;
id: string;
content: React.ReactNode;
}

Expand Down Expand Up @@ -86,8 +86,8 @@ const Carousel = ({ items }: Props) => {
</S.CarouselButton>
<S.CarouselViewport>
<S.CarouselList translateX={translateX} transitioning={isTransitioning}>
{displayItems.map((item) => (
<S.CarouselItem key={item.id}>{item.content}</S.CarouselItem>
{displayItems.map((item, idx) => (
<S.CarouselItem key={item.id + idx}>{item.content}</S.CarouselItem>
))}
</S.CarouselList>
</S.CarouselViewport>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,67 @@
import styled from '@emotion/styled';

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

export const Topic = styled.button<{ background: string; border: string; isSelected: boolean }>`
cursor: pointer; /* 커서 스타일 추가 */
cursor: pointer;

position: relative;

display: flex;
align-items: flex-end;

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

background-color: ${({ background }) => background};
border: ${({ isSelected, border }) => (isSelected ? `2px solid ${border}` : 'none')};
background-image: url(${({ background }) => background});
background-repeat: no-repeat;
background-position: center;
background-size: cover;
border-radius: 12px;
box-shadow: ${({ isSelected, border }) => (isSelected ? `0 0 0 2px ${border}` : 'none')};

transition: box-shadow 0.2s ease-in-out;

&:hover {
border: 2px solid ${({ border }) => border};
&::after {
pointer-events: none;
content: '';

position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;

background-color: rgba(0, 0, 0, 0);
border-radius: 12px;

transition: background-color 0.3s;
}

&:hover::after {
background-color: rgba(0, 0, 0, 0.2);
}
`;

export const Content = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: flex-start;
`;

export const Title = styled.div`
width: fit-content;
padding: 0.25rem 0.5rem;
border: 1px solid ${theme.color.light.secondary_800};
border-radius: 8px;
`;

export const Description = styled.div`
display: flex;
flex-direction: column;
gap: 0.25rem;
align-items: flex-start;
justify-content: flex-start;
`;
Original file line number Diff line number Diff line change
@@ -1,47 +1,54 @@
import { Text } from '@/components';
import { TAG_COLORS } from '@/style/tagColors';
import { useWindowWidth } from '@/hooks';
import { HOT_TOPIC } from '@/service/hotTopic';
import { BREAKING_POINT } from '@/style/styleConstants';
import { theme } from '@/style/theme';

import { Carousel } from '../';
import * as S from './HotTopicCarousel.style';

const HOT_TOPIC = [
{ key: 1, content: '우테코', tagIds: [359], color: TAG_COLORS[3] },
{ key: 2, content: '프리코스', tagIds: [364], color: TAG_COLORS[4] },
{ key: 4, content: '자바스크립트', tagIds: [41, 211, 329, 351, 249, 360], color: TAG_COLORS[0] },
{ key: 3, content: '자바', tagIds: [73, 358, 197], color: TAG_COLORS[1] },
{ key: 5, content: '코틀린', tagIds: [361, 363], color: TAG_COLORS[2] },
{ key: 8, content: '스프링', tagIds: [14, 198], color: TAG_COLORS[7] },
{ key: 7, content: '리액트', tagIds: [50, 289, 318], color: TAG_COLORS[6] },
{ key: 8, content: '안드로이드', tagIds: [362], color: TAG_COLORS[8] },
{ key: 9, content: '알고리즘', tagIds: [261, 316, 253, 144, 143], color: TAG_COLORS[5] },
];

interface Props {
selectTopic: ({ tagIds, content }: { tagIds: number[]; content: string }) => void;
selectTopic: ({ tagIds, topic }: { tagIds: number[]; topic: string }) => void;
selectedHotTopic: string;
}

const HotTopicCarousel = ({ selectTopic, selectedHotTopic }: Props) => (
<Carousel
items={HOT_TOPIC.map(({ key, content, tagIds, color }) => ({
id: key,
content: (
<S.Topic
isSelected={selectedHotTopic === content}
background={color.background}
border={color.border}
onClick={() => {
selectTopic({ tagIds, content });
}}
>
<Text.Large color={theme.color.light.secondary_800} weight='bold'>
{content}
</Text.Large>
</S.Topic>
),
}))}
/>
);
const HotTopicCarousel = ({ selectTopic, selectedHotTopic }: Props) => {
const windowWidth = useWindowWidth();
const isMobile = windowWidth <= BREAKING_POINT.MOBILE;

return (
<Carousel
items={HOT_TOPIC.map(({ topic, description, subDescription, tagIds, color, bg }) => ({
id: topic,
content: (
<S.Topic
isSelected={selectedHotTopic === topic}
background={bg}
border={color}
onClick={() => {
selectTopic({ tagIds, topic });
}}
>
<S.Content>
<S.Title>
<Text.Large color={theme.color.light.secondary_800} weight='bold'>
{topic}
</Text.Large>
</S.Title>
{!isMobile && (
<S.Description>
<Text.Small color={theme.color.light.secondary_800} weight='bold'>
{description}
</Text.Small>
<Text.Small color={theme.color.light.secondary_800}>{subDescription}</Text.Small>
</S.Description>
)}
</S.Content>
</S.Topic>
),
}))}
/>
);
};

export default HotTopicCarousel;
Loading
Loading