Skip to content

Commit

Permalink
Merge pull request #1551 from woowacourse/feat/1540-filtering
Browse files Browse the repository at this point in the history
feat: 아티클 북마크&필터링 기능 추가
  • Loading branch information
hae-on authored Sep 20, 2023
2 parents 68aa0ff + d9501c1 commit 7a63321
Show file tree
Hide file tree
Showing 17 changed files with 457 additions and 64 deletions.
15 changes: 14 additions & 1 deletion frontend/src/apis/articles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { client } from '.';
import { ArticleRequest, MetaOgRequest, MetaOgResponse } from '../models/Article';
import {
ArticleBookmarkPutRequest,
ArticleRequest,
MetaOgRequest,
MetaOgResponse,
} from '../models/Article';

export const requestGetArticles = () => client.get(`/articles`);

Expand All @@ -8,3 +13,11 @@ export const requestPostArticles = (body: ArticleRequest) => client.post('/artic
export const requestGetMetaOg = ({ url }: MetaOgRequest) => {
return client.get<MetaOgResponse>(`/meta-og?url=${url}`);
};

export const requestPutArticleBookmark = ({ articleId, bookmark }: ArticleBookmarkPutRequest) => {
return client.put(`/articles/${articleId}/bookmark`, { checked: bookmark });
};

export const requestGetFilteredArticle = (course: string, bookmark: boolean) => {
return client.get(`/articles?course=${course}&onlyBookmarked=${bookmark}`);
};
60 changes: 42 additions & 18 deletions frontend/src/components/Article/Article.style.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import { COLOR } from '../../constants';

export const Container = styled.li`
width: 100%;
height: 340px;
padding: 20px;
border-radius: 15px;
height: 100%;
padding: 10px;
border-radius: 8px;
background-color: #ffffff;
list-style: none;
Expand All @@ -20,16 +21,16 @@ export const ThumbnailWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
aspect-ratio: 16/9;
width: 100%;
height: 154px;
border-radius: 15px;
margin-bottom: 20px;
margin-bottom: 10px;
`;

export const Thumbnail = styled.img`
width: 100%;
height: 154px;
border-radius: 15px;
height: 100%;
border-radius: 8px;
object-fit: cover;
`;

Expand All @@ -41,34 +42,57 @@ export const ArticleInfoContainer = styled.div`
padding: 10px;
`;

export const UserName = styled.p`
width: 250px;
margin: 0;
color: ${COLOR.DARK_GRAY_400};
font-size: 14px;
export const ArticleInfoWrapper = styled.div`
display: flex;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
font-size: 14px;
`;

export const UserName = styled.p`
margin: 0;
color: ${COLOR.DARK_GRAY_400};
`;

export const Title = styled.p`
width: 250px;
width: 100%;
height: 50px;
margin: 0;
color: ${COLOR.BLACK_900};
font-size: 16px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
overflow: hidden;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
`;

export const BookmarkWrapper = styled.div`
width: 100%;
display: flex;
justify-content: flex-end;
`;

export const ArticleBookmarkButtonStyle = css`
width: initial;
background-color: transparent;
text-align: right;
& > img {
width: 2.3rem;
height: 2.3rem;
}
`;

export const CreatedAt = styled.span`
width: 100%;
margin-top: 16px;
color: ${COLOR.DARK_GRAY_400};
text-align: right;
font-size: 16px;
font-size: 12px;
font-weight: 700;
`;
34 changes: 31 additions & 3 deletions frontend/src/components/Article/Article.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
import * as Styled from './Article.style';
import type { ArticleType } from '../../models/Article';
import Scrap from '../Reaction/Scrap';
import { useRef, useState } from 'react';
import { usePutArticleBookmarkMutation } from '../../hooks/queries/article';
import debounce from '../../utils/debounce';

const Article = ({ id, title, userName, url, createdAt, imageUrl, isBookMarked }: ArticleType) => {
const bookmarkRef = useRef(false);
const [bookmark, setBookmark] = useState(isBookMarked);
const { mutate: putBookmark } = usePutArticleBookmarkMutation();

const toggleBookmark: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();

bookmarkRef.current = !bookmarkRef.current;
setBookmark((prev) => !prev);

debounce(() => {
putBookmark({ articleId: id, bookmark: bookmarkRef.current });
}, 300);
};

const Article = ({ title, userName, url, createdAt, imageUrl }: ArticleType) => {
return (
<Styled.Container>
<Styled.Anchor href={url} target="_blank" rel="noopener noreferrer">
<Styled.ThumbnailWrapper>
<Styled.Thumbnail src={imageUrl} />
</Styled.ThumbnailWrapper>
<Styled.ArticleInfoContainer>
<Styled.UserName>{userName}</Styled.UserName>
<Styled.ArticleInfoWrapper>
<Styled.UserName>{userName}</Styled.UserName>
<Styled.CreatedAt>{createdAt.split(' ')[0]}</Styled.CreatedAt>
</Styled.ArticleInfoWrapper>
<Styled.Title>{title}</Styled.Title>
<Styled.CreatedAt>{createdAt}</Styled.CreatedAt>
<Styled.BookmarkWrapper>
<Scrap
scrap={bookmark}
onClick={toggleBookmark}
cssProps={Styled.ArticleBookmarkButtonStyle}
/>
</Styled.BookmarkWrapper>
</Styled.ArticleInfoContainer>
</Styled.Anchor>
</Styled.Container>
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/components/Article/ArticleBookmarkFIlter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import styled from '@emotion/styled';

interface ArticleBookmarkFilterProps {
checked: boolean;
handleCheckBookmark: React.ChangeEventHandler<HTMLInputElement>;
}

const ArticleBookmarkFilter = ({ checked, handleCheckBookmark }: ArticleBookmarkFilterProps) => {
return (
<ArticleBookmarkFilterContainer>
<label>
<input type="checkbox" checked={checked} onChange={handleCheckBookmark} />
북마크한 아티클
</label>
</ArticleBookmarkFilterContainer>
);
};

export default ArticleBookmarkFilter;

const ArticleBookmarkFilterContainer = styled.div`
display: flex;
align-items: center;
margin-left: 10px;
width: 150px;
height: 100%;
font-size: 1.5rem;
`;
12 changes: 6 additions & 6 deletions frontend/src/components/Article/ArticleList.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import MEDIA_QUERY from '../../constants/mediaQuery';

export const Container = styled.ul`
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 1fr;
gap: 30px 64px;
gap: 10px;
${MEDIA_QUERY.xl} {
gap: 30px 40px;
${MEDIA_QUERY.lg} {
grid-template-columns: repeat(3, 1fr);
}
${MEDIA_QUERY.lg} {
${MEDIA_QUERY.md} {
grid-template-columns: repeat(2, 1fr);
}
${MEDIA_QUERY.md} {
${MEDIA_QUERY.sm} {
grid-template-columns: repeat(1, 1fr);
}
`;
11 changes: 5 additions & 6 deletions frontend/src/components/Article/ArticleList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as Styled from './ArticleList.style';
import Article from './Article';
import { useGetRequestArticleQuery } from '../../hooks/queries/article';
import { ArticleType } from '../../models/Article';

const ArticleList = () => {
const { data: articles, isLoading, isError } = useGetRequestArticleQuery();

if (isLoading) return <div>loading...</div>;
if (isError) return <div>error...</div>;
interface ArticleListProps {
articles: ArticleType[];
}

const ArticleList = ({ articles }: ArticleListProps) => {
return (
<Styled.Container>
{articles?.map((article) => (
Expand Down
23 changes: 21 additions & 2 deletions frontend/src/components/Controls/SelectBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ interface SelectOption {
label: string;
}

/**
FIXME: value props type SelectOption['value'] 로 변경되어야 함.
아래 예시처럼 type을 좁힐 수 없는 문제가 있음.
const CATEGORY_OPTIONS = [
{ value: '', label: '전체보기' },
{ value: 'frontend', label: '프론트엔드' },
{ value: 'backend', label: '백엔드' },
{ value: 'android', label: '안드로이드' },
];
type CategoryOptions = typeof CATEGORY_OPTIONS[number];
->type CategoryOptions = {
value: string;
label: string;
}
위 처럼 value type을 좁힐 수 없음.
*/
interface SelectBoxProps {
isMulti?: boolean;
options: SelectOption[];
Expand All @@ -46,6 +65,7 @@ interface SelectBoxProps {
}

const SelectBox: React.VFC<SelectBoxProps> = ({
isClearable = true,
isMulti = false,
options,
placeholder,
Expand Down Expand Up @@ -75,15 +95,14 @@ const SelectBox: React.VFC<SelectBoxProps> = ({
`}
>
<Select
isClearable={true}
isClearable={isClearable}
isMulti={isMulti}
options={options}
placeholder={placeholder}
onChange={onChange}
styles={selectStyles}
defaultValue={defaultOption}
value={value}
// theme={(theme) => ({ ...theme, colors: { ...theme.colors, primary: 'transparent' } })}
/>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Reaction/Scrap.styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css } from '@emotion/react';
import { COLOR } from '../../enumerations/color';

export const ScrapButtonStyle = css`
export const DefaultScrapButtonStyle = css`
flex-direction: column;
padding: 0;
width: fit-content;
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/Reaction/Scrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { Button, BUTTON_SIZE } from '..';

import scrappedIcon from '../../assets/images/scrap_filled.svg';
import unScrapIcon from '../../assets/images/scrap.svg';
import { ScrapButtonStyle } from './Scrap.styles';
import { DefaultScrapButtonStyle } from './Scrap.styles';
import { SerializedStyles } from '@emotion/react';

interface Props {
scrap: boolean;
onClick: MouseEventHandler<HTMLButtonElement>;
cssProps?: SerializedStyles;
}

const Scrap = ({ scrap, onClick }: Props) => {
const Scrap = ({ scrap, onClick, cssProps }: Props) => {
const scrapIcon = scrap ? scrappedIcon : unScrapIcon;
const scrapIconAlt = scrap ? '스크랩 취소' : '스크랩';

Expand All @@ -20,7 +22,7 @@ const Scrap = ({ scrap, onClick }: Props) => {
size={BUTTON_SIZE.X_SMALL}
icon={scrapIcon}
alt={scrapIconAlt}
cssProps={ScrapButtonStyle}
cssProps={cssProps ?? DefaultScrapButtonStyle}
onClick={onClick}
/>
);
Expand Down
26 changes: 18 additions & 8 deletions frontend/src/hooks/queries/article.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { UserContext } from '../../contexts/UserProvider';
import { requestGetArticles, requestPostArticles } from '../../apis/articles';
import { ArticleType } from '../../models/Article';
import {
requestGetFilteredArticle,
requestPostArticles,
requestPutArticleBookmark,
} from '../../apis/articles';
import { ArticleType, CourseFilter } from '../../models/Article';
import { ERROR_MESSAGE } from '../../constants';
import { SUCCESS_MESSAGE } from '../../constants/message';

const QUERY_KEY = {
articles: 'articles',
filteredArticles: 'filteredArticles',
};

export const useGetRequestArticleQuery = () => {
return useQuery<ArticleType[]>([QUERY_KEY.articles], async () => {
const response = await requestGetArticles();
export const useGetFilteredArticleQuery = (course: string, bookmark: boolean) => {
return useQuery<ArticleType[]>([QUERY_KEY.filteredArticles], async () => {
const response = await requestGetFilteredArticle(course, bookmark);

return response.data;
});
Expand All @@ -22,11 +25,18 @@ export const usePostArticlesMutation = () => {

return useMutation(requestPostArticles, {
onSuccess: () => {
queryClient.invalidateQueries([QUERY_KEY.articles]);
queryClient.invalidateQueries([QUERY_KEY.filteredArticles]);
alert(SUCCESS_MESSAGE.CREATE_ARTICLE);
},
onError: () => {
alert(ERROR_MESSAGE.DEFAULT);
},
});
};

export const usePutArticleBookmarkMutation = () => {
return useMutation(requestPutArticleBookmark, {
onSuccess: () => {},
onError: () => {},
});
};
Loading

0 comments on commit 7a63321

Please sign in to comment.