const CartContext = createContext();
const CART_ITEMS = 'Cart_Items';
const fromLocalStorage = () => {
const cartItems = localStorage.getItem(CART_ITEMS);
return cartItems ? JSON.parse(cartItems) : [];
};
export const CartContextProvider = ({ children }) => {
const [cartItems, setCartItems] = useState(() => fromLocalStorage());
useEffect(() => {
localStorage.setItem(CART_ITEMS, JSON.stringify(cartItems));
}, [cartItems]);
return (
<CartContext.Provider value={{ cartItems, setCartItems }}>
{children}
</CartContext.Provider>
);
};
export const useCartContext = () => useContext(CartContext);
export default function Search({ mobileSearch }) {
const [keyword, setKeyword] = useState(''); // 사용자가 입력하는 키워드
const [searchItems, setSearchItems] = useState([]); // 검색된 리스트
const [selectedItem, setSelectedItem] = useState(-1); // 키보드로 선택된 아이템
const hasSearchItems = searchItems.length > 0; // 검색된 리스트가 있을 경우
const inputRef = useRef();
const ulRef = useRef();
const navigate = useNavigate();
const api = new ProductsApi();
// 모든 상품 가져와서 필요한 데이터로만 배열 만들기
const { data: keyItems } = useQuery([], async () => {
return api.getProducts('all').then((products) =>
products.map((product) => ({
id: product.id,
title: product.title,
category: product.category.includes('clothing')
? 'fashion'
: product.category,
}))
);
});
//사용자가 keyword를 완성할때까지 기다렸다 이벤트 (연속이벤트 최소화)
useEffect(() => {
const debounce = setTimeout(() => {
if (!keyword) return;
const found = keyItems.filter(
(item) =>
item.title.toLowerCase().includes(keyword.toLowerCase()) === true
);
setSearchItems(found);
}, 500);
return () => {
clearTimeout(debounce);
};
}, [keyword, keyItems]);
//상세페이지 이동 후, 상태 초기화
const handleinit = () => {
setSearchItems([]);
setKeyword('');
setSelectedItem(-1);
};
//마우스 이벤트
const handleKeyDown = (event) => {
if (searchItems.length === 0) return;
switch (event.key) {
case 'ArrowDown':
if (selectedItem < searchItems.length - 1) {
setSelectedItem((prev) => prev + 1);
Array.from(ulRef.current.children)[selectedItem + 1].scrollIntoView({
block: 'end',
});
}
break;
case 'ArrowUp':
if (selectedItem > 0) {
setSelectedItem((prev) => prev - 1);
Array.from(ulRef.current.children)[selectedItem - 1].scrollIntoView({
block: 'nearest',
});
}
break;
case 'Enter':
if (selectedItem !== -1) {
const item = searchItems[selectedItem];
navigate(`/${item.category}/product/${item.id}`);
handleinit();
}
break;
}
};
//input 외 클릭시 Search 닫기
const onCloseSearch = (event) => {
if (event.activeElement !== inputRef.current) {
setSearchItems([]);
}
};
//이벤트는 계속 렌더링 되지 않아도됨
useEffect(() => {
if (!hasSearchItems) return;
document.addEventListener('click', onCloseSearch);
}, [hasSearchItems]);
return (
<div className={`${styles.search} ${mobileSearch ? styles.active : null}`}>
<input
type='text'
placeholder='검색'
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
ref={inputRef}
onKeyDown={handleKeyDown}
/>
{hasSearchItems && keyword && (
<div className={styles.search_list}>
<ul ref={ulRef}>
{searchItems.map(({ id, category, title }, index) => (
<li
key={id}
onClick={handleinit}
className={selectedItem === index ? styles.active : ''}
>
<Link to={`/${category}/product/${id}`}>{title}</Link>
</li>
))}
</ul>
</div>
)}
</div>
);
}
제목 | 설명 |
---|---|
구현 사항 | -Fake Store API 데이터 사용하여 쇼핑몰 구현 -키보드 KEY DOWN 이벤트 검색영역에 적용 -context 를 사용한 전역상태 관리 (장바구니,테마) -API 로딩시 스켈레톤 구현 |
라이브러리 | react-query, react-responsive-carousel, react-router-dom, sass |
css 및 반응형 | SASS+Post CSS사용 , 반응형 구현 |
배포 주소 | Netlify https://sunny-trello.netlify.app/ |
소스 코드 | Github https://github.com/heysunny612/ts-trello |