Skip to content

Commit

Permalink
Feature/#188 - 검색 UI 구현 (#192)
Browse files Browse the repository at this point in the history
  • Loading branch information
baegyeong authored Nov 19, 2024
2 parents b5d2543 + 9c22b07 commit 1886773
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 72 deletions.
38 changes: 22 additions & 16 deletions packages/frontend/src/components/layouts/MenuList.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { type MenuItemData } from '@/constants/menuItems';
import { type MenuSection } from '@/types/menu';
import { cn } from '@/utils/cn';

interface MenuListProps {
items: MenuItemData[];
items: MenuSection[];
isHovered: boolean;
onItemClick?: (item: MenuSection) => void;
}

interface MenuItemProps {
Expand All @@ -15,28 +16,33 @@ interface MenuItemProps {
onClick?: () => void;
}

export const MenuList = ({ items, isHovered }: MenuListProps) => {
export const MenuList = ({ items, isHovered, onItemClick }: MenuListProps) => {
const navigate = useNavigate();

const handleClick = (item: MenuSection) => {
if (item.path) {
navigate(item.path);
}

onItemClick?.(item);
};

return (
<ul className="flex flex-col justify-center gap-7">
{items.map((menu) => {
const { id, icon, text, url } = menu;
return (
<MenuItem
key={id}
icon={icon}
text={text}
isHovered={isHovered}
onClick={() => url && navigate(url)}
/>
);
})}
{items.map((item) => (
<MenuItem
key={item.id}
icon={item.icon}
text={item.text}
isHovered={isHovered}
onClick={() => handleClick(item)}
/>
))}
</ul>
);
};

const MenuItem = ({ icon, text, onClick, isHovered }: MenuItemProps) => {
const MenuItem = ({ icon, text, isHovered, onClick }: MenuItemProps) => {
return (
<li className="group flex items-center gap-10" onClick={onClick}>
<button
Expand Down
91 changes: 62 additions & 29 deletions packages/frontend/src/components/layouts/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,73 @@ import { useState } from 'react';
import logoCharacter from '/logoCharacter.png';
import logoTitle from '/logoTitle.png';
import { MenuList } from './MenuList';
import { bottomMenuItems, topMenuItems } from '@/constants/menuItems';
import { Search } from './search';
import { BOTTOM_MENU_ITEMS, TOP_MENU_ITEMS } from '@/constants/menuItems';
import { useOutsideClick } from '@/hooks/useOutsideClick';
import { type MenuSection } from '@/types/menu';
import { cn } from '@/utils/cn';

export const Sidebar = () => {
const [isHovered, setIsHovered] = useState(false);
const [showSearch, setShowSearch] = useState(false);

const ref = useOutsideClick(() => {
if (showSearch) {
setShowSearch(false);
}
});

const handleMenuItemClick = (item: MenuSection) => {
if (item.text === '검색') {
setShowSearch((prev) => !prev);
}
};

return (
<nav
className={cn(
'fixed left-0 top-0 h-full cursor-pointer bg-white px-1 py-4 shadow-md',
'transition-all duration-300 ease-in-out',
isHovered ? 'w-60' : 'w-24',
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<section className="flex flex-col justify-center gap-8">
<header className="flex items-center gap-4">
<img src={logoCharacter} alt="로고 캐릭터" className="w-20" />
<img
src={logoTitle}
alt="로고 제목"
className={cn('w-24 pt-5', isHovered ? 'display' : 'hidden')}
/>
</header>
<div
className={cn(
'flex h-[calc(100vh-11rem)] flex-col justify-between pl-7',
)}
>
<MenuList items={topMenuItems} isHovered={isHovered} />
<MenuList items={bottomMenuItems} isHovered={isHovered} />
</div>
</section>
</nav>
<div ref={ref}>
<nav
className={cn(
'fixed left-0 top-0 h-full cursor-pointer bg-white px-1 py-4 shadow-md',
'transition-all duration-300 ease-in-out',
isHovered ? 'w-60' : 'w-24',
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<section className="flex flex-col justify-center gap-8">
<header className="flex items-center gap-4">
<img src={logoCharacter} alt="로고 캐릭터" className="w-20" />
<img
src={logoTitle}
alt="로고 제목"
className={cn('w-24 pt-5', isHovered ? 'display' : 'hidden')}
/>
</header>
<div
className={cn(
'flex h-[calc(100vh-11rem)]',
isHovered ? 'gap-4' : '',
)}
>
<div className="flex flex-col justify-between pl-7">
<MenuList
items={TOP_MENU_ITEMS}
isHovered={isHovered}
onItemClick={handleMenuItemClick}
/>
<MenuList items={BOTTOM_MENU_ITEMS} isHovered={isHovered} />
</div>
</div>
</section>
</nav>
<div
className={cn(
'fixed top-0 transition-all duration-300 ease-in-out',
isHovered ? 'left-60' : 'left-24',
)}
>
{showSearch && <Search className="h-screen" />}
</div>
</div>
);
};
28 changes: 28 additions & 0 deletions packages/frontend/src/components/layouts/search/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/utils/cn';

interface SearchProps {
className?: string;
}

export const Search = ({ className }: SearchProps) => {
const searchResult = [''];

return (
<div className={cn('bg-white p-10 shadow', className)}>
<h3 className="display-bold24 mb-2">검색</h3>
<p className="display-medium16 text-dark-gray mb-10">
주식을 검색하세요.
</p>
<div className="mb-8 flex gap-4">
<Input placeholder="검색어" />
<Button size="sm">검색</Button>
</div>
{searchResult.map((word) => (
// TODO: 추후 Link로 수정
<p className="text-dark-gray leading-7">{word}</p>
))}
</div>
);
};
1 change: 1 addition & 0 deletions packages/frontend/src/components/layouts/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Search';
12 changes: 7 additions & 5 deletions packages/frontend/src/components/ui/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import { cva, VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';

export const ButtonVariants = cva(
`display-bold12 border rounded shadow-black`,
`display-bold12 border rounded shadow-black py-1`,
{
variants: {
backgroundColor: {
default: 'bg-white',
default: 'bg-white hover:bg-orange',
gray: 'bg-gray',
orange: 'bg-orange',
orange: 'bg-orange hover:bg-white',
},
textColor: {
default: 'text-orange',
white: 'text-white',
default: 'text-orange hover:text-white',
white: 'text-white hover:text-orange',
},
size: {
default: 'w-24',
Expand All @@ -35,6 +35,7 @@ export interface ButtonProps
}

export const Button = ({
type = 'button',
backgroundColor,
textColor,
size,
Expand All @@ -44,6 +45,7 @@ export const Button = ({
}: ButtonProps) => {
return (
<button
type={type}
className={cn(
ButtonVariants({ backgroundColor, textColor, size }),
className,
Expand Down
19 changes: 19 additions & 0 deletions packages/frontend/src/components/ui/input/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type InputHTMLAttributes } from 'react';
import { cn } from '@/utils/cn';

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
className?: string;
}

export const Input = ({ placeholder, className, ...props }: InputProps) => {
return (
<input
placeholder={placeholder}
className={cn(
'border-dark-gray w-36 border-b focus:outline-none',
className,
)}
{...props}
/>
);
};
1 change: 1 addition & 0 deletions packages/frontend/src/components/ui/input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Input';
27 changes: 10 additions & 17 deletions packages/frontend/src/constants/menuItems.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
import { type ReactElement } from 'react';
import Bell from '@/assets/bell.svg?react';
import Home from '@/assets/home.svg?react';
import Search from '@/assets/search.svg?react';
import Stock from '@/assets/stock.svg?react';
import Theme from '@/assets/theme.svg?react';
import User from '@/assets/user.svg?react';
import { type MenuSection } from '@/types/menu';

export interface MenuItemData {
icon: ReactElement;
text: string;
id: number;
url?: string;
}

export const topMenuItems: MenuItemData[] = [
{ icon: <Search className="w-7" />, text: '검색', id: 1 },
{ icon: <Home className="w-7" />, text: '홈', id: 2, url: '/' },
{ icon: <Stock className="w-7" />, text: '주식', id: 3, url: '/stocks' },
{ icon: <Bell className="w-7" />, text: '알림', id: 4 },
export const TOP_MENU_ITEMS: MenuSection[] = [
{ id: 1, icon: <Search className="w-7" />, text: '검색' },
{ id: 2, icon: <Home className="w-7" />, text: '홈', path: '/' },
{ id: 3, icon: <Stock className="w-7" />, text: '주식', path: '/stocks' },
{ id: 4, icon: <Bell className="w-7" />, text: '알림' },
];

export const bottomMenuItems: MenuItemData[] = [
{ icon: <Theme className="w-7" />, text: '다크모드', id: 1 },
export const BOTTOM_MENU_ITEMS: MenuSection[] = [
{ id: 1, icon: <Theme className="w-7" />, text: '다크모드' },
{
id: 2,
icon: <User className="w-7" />,
text: '마이페이지',
id: 2,
url: '/my-page',
path: '/my-page',
},
];
23 changes: 23 additions & 0 deletions packages/frontend/src/hooks/useOutsideClick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useRef } from 'react';

export const useOutsideClick = (callback: () => void) => {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (!ref.current?.contains(event.target as Node)) {
callback();
}
};

document.addEventListener('mouseup', handleClickOutside);
document.addEventListener('touchend', handleClickOutside);

return () => {
document.removeEventListener('mouseup', handleClickOutside);
document.removeEventListener('touchend', handleClickOutside);
};
}, [callback]);

return ref;
};
5 changes: 1 addition & 4 deletions packages/frontend/src/pages/stock-detail/StockDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ export const StockDetail = () => {
<div className="flex flex-col gap-7">
<header className="flex gap-7">
<h1 className="display-bold24">삼성전자</h1>
<Button
type="button"
className="flex items-center justify-center gap-1"
>
<Button className="flex items-center justify-center gap-1">
<Plus /> 내 주식 추가
</Button>
</header>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import { type ReactNode } from 'react';
import { cn } from '@/utils/cn';

interface StockIndexCardProps {
Expand Down
8 changes: 8 additions & 0 deletions packages/frontend/src/types/menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type ReactNode } from 'react';

export interface MenuSection {
id: number;
icon: ReactNode;
text: string;
path?: string;
}

0 comments on commit 1886773

Please sign in to comment.