-
Notifications
You must be signed in to change notification settings - Fork 0
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
Feat/메인 홈 페이지 작업 #53
Changes from all commits
906c095
59f1fa4
3657068
e6fbd7e
5fd83fb
9a36a74
a1ffa8b
914fe84
df95298
350fa5e
f5a5ad5
73b9a2b
5205166
00a9f8f
7100699
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; |
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; |
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 = () => { | ||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p4; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅎㅎ TODO 에 있었군요!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그리구 이 Category 관련 모달(?) 나오는 것도 Component 화 해도 좋을 것 같습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3; There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
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; |
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; |
There was a problem hiding this comment.
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으로 구현하는 방식 중 뭐가 좋을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
새로고침해도 유지되는 filter라고 생각해서 queryParam으로 구현했습니다! 명세서에도 확정된 부분은 아니라서 변경은 상관없을거 같아요!
만약 유지되야하는 상태라면 queryParam 사용이 좋을거 같아요!