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

Feat/메인 홈 페이지 작업 #53

Merged
merged 15 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import ContentBox from '@/components/ContentBox';
import FilterBox from '@/components/FilterBox';
import { Suspense } from 'react';

const Home = () => {
return <main className='bg-[#D9D9D9]'></main>;
return (
<main className='bg-surface'>
<header className='p-40'>
<h1 className='text-text heading-3xl-bd'>내 북마크</h1>
</header>
{/* filter area
TODO : Suspense fallback Component
*/}
<Suspense>
<FilterBox />
</Suspense>
{/* 컨텐츠 영역 */}
<ContentBox />
</main>
);
};

export default Home;
24 changes: 24 additions & 0 deletions src/components/ContentBox/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import { Button } from '../common/Button';
import Icon from '../common/Icon';

const ContentBox = () => {
return (
<section>
{/* Empty Content */}
<div className='mx-auto translate-y-3/4 w-fit flex flex-col items-center'>
<Icon name='bookmark_add' className='w-24 h-24 text-icon-minimal mb-[14px]' />
<h2 className='heading-lg-bd text-text mb-6'>북마크를 추가해 볼까요?</h2>
<p className='body-md text-text-sub mb-24'>
북마크 파일을 끌어당기거나 북마크 추가 버튼을 눌러 등록해 보세요
</p>
<Button type='outline' size='small' onClick={() => alert('북마크 추가 모달')}>
<Button.Label>북마크 추가</Button.Label>
</Button>
</div>
</section>
);
};

export default ContentBox;
153 changes: 153 additions & 0 deletions src/components/FilterBox/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use client';

import useQueryString from '@/hooks/useQueyString';
import { cn } from '@/lib/utils';
import { ChangeEvent, useState } from 'react';
import TabList from '../TabList';
import { Button } from '../common/Button';
import Divider from '../common/Divider';
import Icon from '../common/Icon';
import { Textfield } from '../common/Textfield';
import TextfieldInput from '../common/Textfield/ui/TextfieldInput';
import TextfieldInputWrapper from '../common/Textfield/ui/TextfieldInputWrapper';

const TAB_LIST = [
{
title: '레퍼런스',
value: '레퍼런스',
count: 8,
},
{
title: '아이데이션',
value: '아이데이션',
count: 22,
},
{
title: '기타',
value: '기타',
count: 5,
},
];

const FilterBox = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

p4;
성은님!
Filter query Param으로 구현하신 이유가 있을까요?
state로 관리하는 것과 query Param으로 구현하는 방식 중 뭐가 좋을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

새로고침해도 유지되는 filter라고 생각해서 queryParam으로 구현했습니다! 명세서에도 확정된 부분은 아니라서 변경은 상관없을거 같아요!

만약 유지되야하는 상태라면 queryParam 사용이 좋을거 같아요!

const { queryParam, updateQueryString } = useQueryString();

const isLikeChecked = queryParam.get('like-check') === 'true'; // 종아요 항목 표시
const viewType = queryParam.get('view') ?? 'grid'; // list 타입 grid | list

const [isOpenCategory, setIsOpenCategory] = useState<boolean>(false); // 카테고리 모달 오픈
const [category, setCategory] = useState<string>(''); // 카테고리 텍스트
const [isError, setIsError] = useState<boolean>(false); // 카테고리 에러

const handleChangeCategory = (e: ChangeEvent<HTMLInputElement>) => {
if (isError) {
setIsError(false);
}
setCategory(e.target.value);
};

/**
* TODO :
* - 카테고리 등록 API
* - 유효성 검증 case 추가 필요
* - 카테고리 영역 밖 크릭 or esc 닫기
*/
const handleAddCategory = () => {
if (!category) {
alert('카테고리를 입력해주세요.');
}
if (category === '전체') {
setIsError(true);
return;
}
// category API

alert('카테고리가 추가되었어요.');
};

return (
<div>
<div className='px-40 flex justify-between'>
<TabList tabs={TAB_LIST} />
<div className='relative'>
<Button
type='text'
size='medium'
onClick={() => setIsOpenCategory((prev) => !prev)}
className='p-0 pb-16 text-icon hover:text-secondary-hover'
>
<Icon name='plus_circle' className='w-16 h-16' />
<Button.Label className='label-md-bold text-inherit'>카테고리 추가</Button.Label>
</Button>
{isOpenCategory && (
<div className='absolute right-0 top-[calc(100%-8px)] p-8 grid grid-cols-[300px_1fr] gap-8 bg-surface rounded-xl shadow-layer'>
<Textfield
value={category}
onChange={handleChangeCategory}
placeholder='새 카테고리 추가'
isInvalid={isError}
>
<TextfieldInputWrapper>
<TextfieldInput />
{isError && (
<Icon name='warningTriangle_f' className='w-16 h-16 text-icon-critical' />
)}
Comment on lines +82 to +94
Copy link
Contributor

Choose a reason for hiding this comment

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

p4;
Category 추가 영역 밖에 클릭하면 닫히는 동작도 추가하면 좋을 것 같아요!

Copy link
Contributor

Choose a reason for hiding this comment

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

ㅎㅎ TODO 에 있었군요!!

Copy link
Contributor

Choose a reason for hiding this comment

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

그리구 이 Category 관련 모달(?) 나오는 것도 Component 화 해도 좋을 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵넵! 컴포넌트화 할 수 있는 부분들은 구조화해서 구분하겠습니다!

</TextfieldInputWrapper>
</Textfield>
<Button type='primary' size='large' onClick={handleAddCategory}>
<Button.Label>추가</Button.Label>
</Button>
</div>
)}
</div>
</div>
<Divider />
<div className='px-40 py-16 flex justify-between bg-surface-minimal'>
<Button
type='text'
size='medium'
onClick={() => updateQueryString('like-check', String(!isLikeChecked))}
Copy link
Contributor

Choose a reason for hiding this comment

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

p3;
이쪽 like 쪽 클릭을 해도 동작을 안하는 것 같습니다!
확인 한번 부탁드립니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵 확인해보겠습니다!

className='p-0'
>
<Icon
name='check_circle'
className={cn(['w-16 h-16 text-icon-minimal', isLikeChecked && 'text-icon-primary'])}
/>
<Button.Label className={cn(['text-text-minimal', isLikeChecked && 'text-primary'])}>
좋아요 항목만 표시
</Button.Label>
</Button>
<div className='flex items-center gap-12'>
<span className='cursor-pointer mr-4 flex items-center gap-4 label-md text-text-minimal'>
최신순 <Icon name='chevronDown_s' className='w-20 h-20 ' />
</span>
<button onClick={() => updateQueryString('view', 'grid')}>
<Icon
name='grid'
className={cn([
'w-20 h-20 text-icon-minimal hover:text-icon',
viewType === 'grid' && 'text-icon',
])}
/>
</button>
<button onClick={() => updateQueryString('view', 'list')}>
<Icon
name='list'
className={cn([
'w-20 h-20 text-icon-minimal hover:text-icon',
viewType === 'list' && 'text-icon',
])}
/>
</button>
<Divider direction='vertical' className='h-1/2 mx-2' />
<span className='cursor-pointer flex items-center gap-4 label-md text-text-minimal'>
<Icon name='setting' className='w-20 h-20' /> 편집
</span>
</div>
</div>
<Divider />
</div>
);
};

export default FilterBox;
41 changes: 41 additions & 0 deletions src/components/TabList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import useQueryString from '@/hooks/useQueyString';
import { cn } from '@/lib/utils';

interface ITabList {
tabs?: Array<{ title: string; value: string; count: number }>;
}

const TabList = ({ tabs = [] }: ITabList) => {
const { queryParam, updateQueryString } = useQueryString();
const queryTab = queryParam.get('tab') ?? '전체';

return (
<>
<ul className='h-40 flex gap-28 overflow-x-scroll whitespace-nowrap'>
<li
className={cn([
'cursor-pointer flex gap-4 label-md-bold text-text',
queryTab === '전체' && 'border-b-2 border-divide-on',
])}
onClick={() => updateQueryString('tab', '전체')}
>
전체 <span className='text-primary'>0</span>
</li>
{tabs.map((tab) => (
<li
key={tab.value}
className={cn([
'cursor-pointer flex gap-4 label-md-bold text-text',
queryTab === tab.value && 'border-b-2 border-divide-on',
])}
onClick={() => updateQueryString('tab', tab.value)}
>
{tab.title} <span className='text-primary'>{tab.count}</span>
</li>
))}
</ul>
</>
);
};

export default TabList;
10 changes: 8 additions & 2 deletions src/components/common/Button/ui/ButtonLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import { useButtonState } from '../modules/ButtonStateContext';

export interface IButtonLabel extends PropsWithChildren {
// TODO: 나중에 필요한 props 있으면 추가!
className?: string;
}

const ButtonLabel = ({ children }: IButtonLabel) => {
const ButtonLabel = ({ children, className }: IButtonLabel) => {
// TODO(훈석): isLoading 상태에 따라 스타일 변경 필요
const { size, type, isDisabled } = useButtonState();

return <label className={cn(buttonLabelVariants({ size, type, isDisabled }))}>{children}</label>;
return (
<label className={cn([buttonLabelVariants({ size, type, isDisabled })], className)}>
{children}
</label>
);
};

export const buttonLabelVariants = cva(['cursor-pointer'], {
Expand All @@ -27,6 +32,7 @@ export const buttonLabelVariants = cva(['cursor-pointer'], {
outline: 'text-text-secondary',
secondary: 'text-text-secondary',
critical: 'text-text-on',
text: 'text-text-secondary',
},
isDisabled: {
true: 'cursor-default text-text-disabled',
Expand Down
1 change: 1 addition & 0 deletions src/components/common/Button/ui/ButtonMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const buttonMainVariants = cva(['flex justify-center items-center gap-4']
'bg-surface border-[1px] border-solid border-border hover:bg-action-secondary-hover active:bg-action-secondary-pressed',
secondary: 'hover:bg-action-secondary-hover active:bg-action-secondary-pressed',
critical: 'bg-critical hover:bg-critical-hover active:bg-critical-pressed',
text: 'bg-transparent',
},
isDisabled: {
true: '',
Expand Down
9 changes: 6 additions & 3 deletions src/components/common/Icon/lib/bookmark_add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/components/common/Icon/lib/check_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions src/components/common/Icon/lib/grid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/components/common/Icon/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

export { default as bell_s } from './bell_s.svg';
export { default as bookmark_add } from './bookmark_add.svg';
export { default as check_circle } from './check_circle.svg';
export { default as checkIndeterminate_f } from './check_indeterminate.svg';
export { default as checkOff_f } from './check_off.svg';
export { default as checkOn_f } from './check_on.svg';
Expand All @@ -19,15 +20,18 @@ export { default as chevronRightDouble } from './chevron-right-double.svg';
export { default as chevronDown_s } from './chevron_down.svg';
export { default as filePlus } from './file_plus.svg';
export { default as google } from './google.svg';
export { default as grid } from './grid.svg';
export { default as home04_s } from './home_04.svg';
export { default as link_03 } from './link_03.svg';
export { default as list } from './list.svg';
export { default as logout } from './logout.svg';
export { default as mail } from './mail.svg';
export { default as packit_full_logo } from './packit_full_logo.svg';
export { default as packit_logo } from './packit_logo.svg';
export { default as placeholder_s } from './placeholder.svg';
export { default as plus_square } from './plus-square.svg';
export { default as plus_s } from './plus.svg';
export { default as plus_circle } from './plus_circle.svg';
export { default as searchSm_s } from './search_sm.svg';
export { default as setting } from './setting.svg';
export { default as user_s } from './user.svg';
Expand Down
10 changes: 10 additions & 0 deletions src/components/common/Icon/lib/list.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/components/common/Icon/lib/plus_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/hooks/useQueyString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';

const useQueryString = () => {
const router = useRouter();
const pathname = usePathname();
const queryParam = useSearchParams();
const searchParam = new URLSearchParams(queryParam);

// 쿼리 업데이트
const updateQueryString = (type: string, value: string) => {
if (searchParam.has(type)) {
searchParam.delete(type);
}

if (value) {
searchParam.append(type, value);
}

router.replace(`${pathname}?${searchParam.toString()}`);
};

return { queryParam, updateQueryString };
};

export default useQueryString;
Loading