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

검색 모달 컴포넌트 구현 #62

Merged
merged 9 commits into from
Nov 8, 2023
24 changes: 16 additions & 8 deletions src/components/common/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { useRef } from 'react'
import { cls } from '@/utils'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import DropdownItem from './DropdownItem'
import { PLACEMENTS, TYPES, VERTICAL_PADDING } from './constants'
import { DROPDOWN_OPTIONS, PLACEMENTS, VERTICAL_PADDING } from './constants'
import useDropdown from './hooks/useDropdown'

export interface DropdownProps {
type: 'space' | 'link' | 'search' | 'user_edit' | 'user_invite' | 'tag'
size?: 'large' | 'medium' | 'small'
placement?: 'left' | 'right'
tags?: string[]
onChange: (e?: React.MouseEvent<HTMLButtonElement>) => void
onChange: (e: React.MouseEvent<HTMLButtonElement>) => void
}

const Dropdown = ({
Expand All @@ -22,7 +22,14 @@ const Dropdown = ({
tags,
onChange,
}: DropdownProps) => {
const dropdownItems = type !== 'tag' ? TYPES[type] : tags && ['전체', ...tags]
const optionKeys =
type !== 'tag'
? Object.keys(DROPDOWN_OPTIONS[type])
: tags && ['전체', ...tags]
const optionValues =
type !== 'tag'
? Object.values(DROPDOWN_OPTIONS[type])
: tags && ['전체', ...tags]
const dropdownRef = useRef<HTMLDivElement | null>(null)
const { isOpen, setIsOpen, index, handleClick } = useDropdown({
el: dropdownRef,
Expand All @@ -38,7 +45,7 @@ const Dropdown = ({
VERTICAL_PADDING[size],
)}
onClick={() => setIsOpen(!isOpen)}>
{type === 'tag' ? dropdownItems?.[index] : dropdownItems?.[index]}
{type === 'tag' ? optionKeys?.[index] : optionKeys?.[index]}
<ChevronDownIcon className="h-5 w-5" />
</button>
<div
Expand All @@ -50,13 +57,14 @@ const Dropdown = ({
: 'hidden',
PLACEMENTS[placement],
)}>
{dropdownItems?.map((item, i) => (
{optionKeys?.map((option, i) => (
<DropdownItem
label={item}
label={option}
value={optionValues?.[i]}
active={index === i}
danger={type === 'user_edit' && i === dropdownItems.length - 1}
danger={type === 'user_edit' && i === optionKeys.length - 1}
onClick={(e) => handleClick(e, i)}
key={item}
key={option}
/>
))}
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/Dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CheckIcon } from '@heroicons/react/20/solid'

export interface DropdownItemProps {
label: string
value?: string
active?: boolean
border?: boolean
danger?: boolean
Expand All @@ -12,6 +13,7 @@ export interface DropdownItemProps {

const DropdownItem = ({
label,
value,
active = false,
border = false,
danger = false,
Expand All @@ -21,7 +23,7 @@ const DropdownItem = ({
return (
<button
type="button"
value={label}
value={value ?? label}
className={cls(
'inline-flex items-center px-2.5 py-1 text-sm',
active ? 'font-medium' : 'font-normal',
Expand Down
18 changes: 9 additions & 9 deletions src/components/common/Dropdown/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
export const TYPES = {
space: ['최신순', '즐겨찾기순'],
link: ['최신순', '좋아요순'],
search: ['스페이스', '유저'],
user_edit: ['편집 허용', '읽기 허용', '제거'],
user_invite: ['편집 허용', '읽기 허용'],
}
export const DROPDOWN_OPTIONS = {
space: { 최신순: 'recent', 즐겨찾기순: 'scrap' },
link: { 최신순: 'recent', 좋아요순: 'favorite' },
search: { 스페이스: 'space', 유저: 'user' },
user_edit: { '편집 혀용': 'edit', '읽기 허용': 'view', 제거: 'remove' },
Copy link
Contributor

Choose a reason for hiding this comment

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

편집 혀용..?

Copy link
Member Author

Choose a reason for hiding this comment

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

앗 수정했습니다! 🙈

user_invite: { '편집 허용': 'eidt', '읽기 허용': 'view' },
} as const

export const VERTICAL_PADDING = {
large: 'py-2.5',
medium: 'py-1.5',
small: 'py-0.5',
}
} as const

export const PLACEMENTS = {
left: 'left-0',
right: 'right-0',
}
} as const
2 changes: 1 addition & 1 deletion src/components/common/Dropdown/hooks/useDropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

export interface useDropdownProps {
el: React.RefObject<HTMLDivElement>
onChange: (e?: React.MouseEvent<HTMLButtonElement>) => void
onChange: (e: React.MouseEvent<HTMLButtonElement>) => void
}

const useDropdown = ({
Expand Down
17 changes: 12 additions & 5 deletions src/components/common/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
'use client'

import { useState } from 'react'
import { useCallback, useState } from 'react'
import { LinkIcon } from '@heroicons/react/20/solid'
import { BellIcon } from '@heroicons/react/24/outline'
import { MagnifyingGlassCircleIcon } from '@heroicons/react/24/outline'
import { Bars3Icon } from '@heroicons/react/24/solid'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import Button from '../Button/Button'
import SearchModal from '../SearchModal/SearchModal'
import Sidebar from '../Sidebar/Sidebar'

const Header = () => {
const pathname = usePathname()
const router = useRouter()
const searchParams = useSearchParams()
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const pathName = usePathname()
const currentPage = pathName
const isSearchModalOpen = searchParams.get('search')
const currentPage = pathname
.split(/[^a-zA-Z]/)[1] // 라우터명
.replace(/^[a-z]/, (char) => char.toUpperCase()) // 첫글자 대문자 치환

Expand All @@ -36,7 +40,9 @@ const Header = () => {
<BellIcon className="h-6 w-6 text-slate9" />
</Link>
</Button>
<Button className="flex h-8 w-8 items-center justify-center">
<Button
className="flex h-8 w-8 items-center justify-center"
onClick={() => router.push(`${pathname}?search=true`)}>
<MagnifyingGlassCircleIcon className="h-6 w-6 text-slate9" />
</Button>
<Button
Expand All @@ -47,6 +53,7 @@ const Header = () => {
</div>
</div>
{isSidebarOpen && <Sidebar onClose={() => setIsSidebarOpen(false)} />}
{isSearchModalOpen && <SearchModal onClose={() => router.back()} />}
</>
)
}
Expand Down
82 changes: 82 additions & 0 deletions src/components/common/SearchModal/SearchModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client'

import { useRef } from 'react'
import { useForm } from 'react-hook-form'
import { Dropdown } from '@/components'
import Input from '../Input/Input'
import { SEARCH_MODAL_TITLE } from './constants'
import useSearchModal from './hooks/useSearchModal'

export interface SearchModalProps {
onClose: () => void
}

export interface SearchFormValues {
search: string
target: string
}

const SearchModal = ({ onClose }: SearchModalProps) => {
const { register, setValue, setFocus, handleSubmit } =
useForm<SearchFormValues>({
defaultValues: {
search: '',
target: 'space',
},
})
const searchModalRef = useRef<HTMLDivElement>(null)
const {
trends,
handleOverlayClick,
handleTargetChange,
handleKeywordClick,
onSubmit,
} = useSearchModal({
searchModalRef,
setValue,
setFocus,
onClose,
})

return (
<div
ref={searchModalRef}
onClick={handleOverlayClick}
className="fixed left-0 right-0 top-0 z-50 mx-auto flex h-screen w-full max-w-[500px] flex-col justify-center bg-black/40 shadow-xl">
<div className="max-h-content absolute top-0 flex w-full flex-col rounded-b-xl bg-bgColor px-4 pb-4">
<form
className="flex gap-x-1.5 py-1.5"
onSubmit={handleSubmit(onSubmit)}>
<div className="shrink-0">
<Dropdown
type="search"
size="large"
onChange={handleTargetChange}
/>
</div>
<div className="grow">
<Input
{...register('search', { required: true })}
placeholder="검색어를 입력하세요."
inputButton={true}
buttonText="검색"
/>
</div>
</form>
<h2 className="py-4 font-bold text-gray9">{SEARCH_MODAL_TITLE}</h2>
<ul>
{trends.map((trend) => (
<li
className="border-b border-gray3 px-3 py-2.5 text-sm font-medium text-gray9 last:border-none"
onClick={handleKeywordClick}
key={trend.keyword}>
{trend.keyword}
</li>
))}
</ul>
</div>
</div>
)
}

export default SearchModal
1 change: 1 addition & 0 deletions src/components/common/SearchModal/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SEARCH_MODAL_TITLE = '인기 검색어'
58 changes: 58 additions & 0 deletions src/components/common/SearchModal/hooks/useSearchModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useEffect } from 'react'
import {
SubmitHandler,
UseFormSetFocus,
UseFormSetValue,
} from 'react-hook-form'
import { mock_trendData } from '@/data'
import { useRouter } from 'next/navigation'
import { SearchFormValues } from '../SearchModal'

export interface useSearchModalProps {
searchModalRef: React.RefObject<HTMLDivElement>
setValue: UseFormSetValue<SearchFormValues>
setFocus: UseFormSetFocus<SearchFormValues>
onClose: () => void
}

const useSearchModal = ({
searchModalRef,
setValue,
setFocus,
onClose,
}: useSearchModalProps) => {
const router = useRouter()
const trends = mock_trendData

useEffect(() => {
setFocus('search')
}, [setFocus])

const handleTargetChange = (e: React.MouseEvent<HTMLButtonElement>) => {
setValue('target', e.currentTarget.value)
}

const handleKeywordClick = (e: React.MouseEvent<HTMLLIElement>) => {
router.push(`/search?target=space&keyword=${e.currentTarget.innerText}`)
}

const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === searchModalRef.current) {
onClose()
}
}

const onSubmit: SubmitHandler<SearchFormValues> = (data) => {
router.push(`/search?target=${data.target}&keyword=${data.search}`)
}

return {
trends,
handleTargetChange,
handleKeywordClick,
handleOverlayClick,
onSubmit,
}
}

export default useSearchModal
2 changes: 1 addition & 1 deletion src/components/common/Tab/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ interface TabProps {

const Tab = ({ children }: TabProps) => {
return (
<div className="sticky top-[53px] flex bg-bgColor transition ease-in-out">
<div className="sticky top-[53px] z-40 flex bg-bgColor transition ease-in-out">
{children}
</div>
)
Expand Down
18 changes: 18 additions & 0 deletions src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,21 @@ export const mock_replyData = [
auth: true,
},
]

export const mock_trendData = [
{
keyword: '어쩌구',
},
{
keyword: '저쩌구',
},
{
keyword: '쏼라쏼라',
},
{
keyword: '훌라훌라',
},
{
keyword: '나하항',
},
]