-
아마존 E-commerce 클론 -27) Search Box ,Search Filter기능 , Side bar, sort & Filter 만들기NODE.JS 2021. 5. 29. 22:42
우선 serchbox component를 만들자.
components>SearchBox.js
import React, { useState } from 'react' function SearchBox(props) { const [name, setName] = useState('') const submitHandler=(e)=>{ e.preventDefault(); props.history.push(`/search/name/${name}`) } return ( <form onSubmit={submitHandler} className="search"> <div className="row"> <input type="text" name="q" id = "q" onChange={e=>setName(e.target.value)} /> <button className="primary" type="submit"><i className="fa fa-search"></i></button> </div> </form> ) } export default SearchBox
app.js에 추가하는데 route의 render를 이용해서 history기능도 사용할 수 있도록 한다.
<div> <Link className="brand" to="/">amazona</Link> </div> <div> <Route render = {({history})=>( <SearchBox history={history}></SearchBox>)}></Route> </div>
style추가
/* Search */ .search button{ border-radius:0 0.5rem 0.5rem 0; border-right: none; margin-right: 0.5rem; } .search input{ border-radius: 0.5rem 0 0 0.5rem; border-right: none; margin-left: 0.5rem; }
검색하면 주소가 잘 이동한다.
이제 SearchScreen.js를 만들어준다.
상품리스트를 가져와서 보여주는 전체적인 템플릿을 만든다.
import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router' import { listProducts } from '../actions/productActions' import LoadingBox from '../components/LoadingBox'; import MessageBox from '../components/MessageBox'; import Product from '../components/Product'; function SearchScreen(props) { const {name = 'all'} = useParams(); const productList = useSelector(state => state.productList) const {loading, error, products} = productList const dispatch = useDispatch() useEffect(() => { dispatch(listProducts({name:name!=='all'? name: ''})) }, [dispatch,name]) return ( <div> <div className="row top"> {loading? <LoadingBox></LoadingBox> : error? <MessageBox variant='danger'>{error}</MessageBox> : <div> {products.length} Results </div> } </div> <div className="row"> <div className="co-1"> <h3>Department</h3> <ul> <li>Caterory 1</li> </ul> </div> <div className="col-3"> {loading? <LoadingBox></LoadingBox> : error? <MessageBox variant='danger'>{error}</MessageBox> : <div> {products.length===0 && <MessageBox>No Product Found</MessageBox>} <div className="row center"> {products.map(product=>( <Product key={product._id} product = {product}/> ))} </div> </div> } </div> </div> </div> ) } export default SearchScreen
app.js에 라우트를 추가해준다.
import SearchScreen from './screens/SearchScreen'; <Route path="/search/name/:name?" component={SearchScreen} exact></Route>
이제 상품이 일치하는 상품만 나오도록 설정하자.
productActions.js
export const listProducts = ({seller='', name=''}) =>async(dispatch)=>{ dispatch({ type:PRODUCT_LIST_REQUEST }); try{ const {data} = await Axios.get(`/api/products?seller=${seller}&name=${name}`); dispatch({type:PRODUCT_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_LIST_FAIL, payload:error.message}); } }
productRouter.js에 다음과 같은 검사항목을 추가해준다.
productRouter.get('/' ,expressAsyncHandler(async(req, res)=>{ const seller = req.query.seller || '' const name = req.query.name || '' const sellerFilter = seller? {seller} :{}; const nameFilter = name? {name:{$regex:name, $options:'i'}} :{}; const products = await Product.find({...sellerFilter, ...nameFilter}).populate('seller', 'seller.name seller.logo'); res.send(products); }));
이제 카테고리별로 나타내도록 필터를 만들자.
먼저 productrouter에 api를 만든다.
(list 아래에 만들기)
productRouter.get('/categories' ,expressAsyncHandler(async(req, res)=>{ const categories = await Product.find().distinct('category') res.send(categories) }));
productConstants로 먼저 가보자.
export const PRODUCT_CATEGORY_LIST_REQUEST = 'PRODUCT_CATEGORY_LIST_REQUEST'; export const PRODUCT_CATEGORY_LIST_SUCCESS = 'PRODUCT_CATEGORY_LIST_SUCCESS'; export const PRODUCT_CATEGORY_LIST_FAIL = 'PRODUCT_CATEGORY_LIST_FAIL';
productActions
productlist와 비슷하므로 복사해서 수정해준다.
export const listProductsCategories = () =>async(dispatch)=>{ dispatch({ type:PRODUCT_CATEGORY_LIST_REQUEST }); try{ const {data} = await Axios.get(`/api/products/categories`); dispatch({type:PRODUCT_CATEGORY_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_CATEGORY_LIST_FAIL, payload:error.message}); } }
productReducers
export const productCategoryListReducer = (state = { loading:true, categories: []}, action)=>{ switch(action.type){ case PRODUCT_CATEGORY_LIST_REQUEST: return {loading: true}; case PRODUCT_CATEGORY_LIST_SUCCESS: return {loading:false, categories:action.payload}; case PRODUCT_CATEGORY_LIST_FAIL: return {loading:false, error:action.payload}; default: return state; } }
store.js
const reducer = combineReducers({ productList: productListReducer, productDetails : productDetailsReducer, cart:cartReducer, userSignin: userSigninReducer, userRegister: userRegisterReducer, orderCreate:orderCreateReducer, orderDetails:orderDetailsReducer, orderPay: orderPayReducer, orderMineList:orderMineListReducer, userDetails:userDetailsReducer, userUpdateProfile:userUpdateProfileReducer, productCreate:productCreateReducer, productUpdate:productUpdateReducer, productDelete:productDeleteReducer, orderList:orderListReducer, orderDelete:orderDeleteReducer, orderDeliver:orderDeliverReducer, userList:userListReducer, userDelete:userDeleteReducer, userUpdate:userUpdateReducer, userTopSellersList:userTopSellerListReducer, productCategoryList:productCategoryListReducer })
App.js안에 필터항목을 나타내보자.(sidebar로 나타낼 것이다)
import { listProductsCategories } from './actions/productActions'; useEffect(() => { dispatch(listProductsCategories()) }, [])
searchScreen에도 보이도록 하자.
const {name = 'all', category='all'} = useParams(); const productCategoryList = useSelector(state => state.productCategoryList) const {loading:loadingCategories, error:errorCategories, categories} = productCategoryList useEffect(() => { dispatch(listProducts({name:name!=='all'? name: '', category:category !=='all'? category:''})) }, [category, dispatch,name]) useEffect(()=>{ dispatch(listProductsCategories()) },[dispatch]) const getFilterUrl = (filter) =>{ const filterCategory = filter.category ||category; const filterName = filter.name ||name; return `/search/category/${filterCategory}/name/${filterName}` } <div className="row top"> <div className="col-1"> <h3>Department</h3> {loadingCategories? <LoadingBox></LoadingBox> : errorCategories? <MessageBox variant='danger'>{errorCategories}</MessageBox> : ( <ul> {categories.map(c=> ( <li key = {c}> <Link to ={getFilterUrl({category:c})} className={c===category? 'active':''}>{c}</Link> </li> )) } </ul> )} </div>
app.js에 루트 추가해준다.
<Route path="/search/category/:category" component={SearchScreen} exact></Route> <Route path="/search/category/:category/name/:name" component={SearchScreen} exact></Route>
productActions에서 다음과 같이 카테고리 항목도 추가해준다.
export const listProducts = ({seller='', name='', category = ''}) =>async(dispatch)=>{ dispatch({ type:PRODUCT_LIST_REQUEST }); try{ const {data} = await Axios.get(`/api/products?seller=${seller}&name=${name}&category=${category}`); dispatch({type:PRODUCT_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_LIST_FAIL, payload:error.message}); } }
라우터도 수정해준다.
productRouter.get('/' ,expressAsyncHandler(async(req, res)=>{ const seller = req.query.seller || '' const name = req.query.name || '' const category = req.query.category || '' const sellerFilter = seller? {seller} :{}; const nameFilter = name? {name:{$regex:name, $options:'i'}} :{}; const categoryFilter = category? {category} :{}; const products = await Product.find({...sellerFilter, ...nameFilter, ...categoryFilter}).populate('seller', 'seller.name seller.logo'); res.send(products); }));
이제 app.js에 카테고리 사이드바를 넣자.
const [sidebarIsOpen, setSidebarIsOpen] = useState(false) return ( <BrowserRouter > <div className="grid-container"> <header className="row"> <div> <button type="button" className="open-sidebar" onClick={()=>setSidebarIsOpen(true)}> <i className="fa fa-bars"></i> </button> <Link className="brand" to="/">amazona</Link> ... </header> <aside className={sidebarIsOpen? 'open': ''}> <ul className="categories"> <li> <strong>Categories</strong> <button type="button" className="close-sidebar" onClick={()=>setSidebarIsOpen(false)}> <i className="fa fa-close"></i> </button> </li> </ul> </aside> <main>
스타일링하자.
/* Aside */ aside { position: fixed; width: 30rem; height: 100%; background-color: #efefef; z-index: 1000 ; transform: translate(-30rem); transition: all 0.5s; } aside.open{ transform: translate(0) ; } button.open-sidebar{ font-size: 3rem; padding:0.2rem 0.5rem; margin:0 0.5rem; background:none; color: #ffffff; cursor:pointer } button.open-sidebar:hover{ border-color: #ffffff; } aside ul{ padding:0; list-style: none; } aside li{ display: flex; justify-content: space-between; padding:1rem; } button.close-sidebar{ padding:0.3rem 0.8rem; } /* Image */
이제 카테고리가 보이도록하자.
searchScreen의 내용을 가지고 온다.
const [sidebarIsOpen, setSidebarIsOpen] = useState(false) const productCategoryList = useSelector(state => state.productCategoryList) const {loading:loadingCategories, error:errorCategories, categories} = productCategoryList useEffect(() => { dispatch(listProductCategories()) }, [dispatch]) ... <aside className={sidebarIsOpen? 'open': ''}> <ul className="categories"> <li> <strong>Categories</strong> <button type="button" className="close-sidebar" onClick={()=>setSidebarIsOpen(false)}> <i className="fa fa-close"></i> </button> </li> {loadingCategories? <LoadingBox></LoadingBox> : errorCategories? <MessageBox variant='danger'>{errorCategories}</MessageBox> : ( <ul> {categories.map(c=> ( <li key = {c}> <Link to ={`/search/category/${c}`} onClick={()=>setSidebarIsOpen(false)} >{c}</Link> </li> )) } </ul> )} </ul> </aside>
이제 상품 정렬순서기능과 골라내기 기능을 만들어보자. (sort&filter)
searchscreen에 템플릿을 잡아준다.
<div className="col-1"> <div> <h3>Department</h3> {loadingCategories? <LoadingBox></LoadingBox> : errorCategories? <MessageBox variant='danger'>{errorCategories}</MessageBox> : ( <ul> {categories.map(c=> ( <li key = {c}> <Link to ={getFilterUrl({category:c})} className={c===category? 'active':''}>{c}</Link> </li> )) } </ul> )} </div> <div> <h3>Price</h3> <ul> {prices.map(p=>( <li key={p.name}> <Link className={`${p.min}-${p.max}`=== `${min}-${max}`? 'active':''} to={getFilterUrl({min:p.min, max:p.max})} >{p.name}</Link> </li> ))} </ul> </div> </div>
frontend>utils.js를 만들어준다.
export const prices = [{ name:'Any', min:0, max:0 },{ name:`$1 to $10`, min:1, max:10 },{ name:`$10 to $100`, min:10, max:100 },{ name:`$100 to $1000`, min:100, max:1000 } ] export const ratings = [ { name:'4starts & up', rating:4 }, { name:'3starts & up', rating:3 }, { name:'2starts & up', rating:2 }, { name:'1starts & up', rating:1 }, ]
다시 search screen와서 작성해준다.
import { prices } from '../utils'; const {name = 'all', category='all', min=0, max=0} = useParams(); useEffect(() => { dispatch(listProducts({name:name!=='all'? name: '', category:category !=='all'? category:'', min, max})) }, [category, dispatch,name,min, max]) const getFilterUrl = (filter) =>{ const filterCategory = filter.category ||category; const filterName = filter.name ||name; const filterMin = filter.min? filter.min :filter.min===0? 0:min; const filterMax = filter.max? filter.max :filter.max===0? 0:max; return `/search/category/${filterCategory}/name/${filterName}/min/${filterMin}/max/${filterMax}` } <div> <h3>Price</h3> <ul> {prices.map(p=>( <li key={p.name}> <Link className={`${p.min}-${p.max}`=== `${min}-${max}`? 'active':''} to={getFilterUrl({min:p.min, max:p.max})} >{p.name}</Link> </li> ))} </ul> </div>
app.js에 루트 추가
<Route path="/search/category/:category/name/:name/min/:min/max/:max" component={SearchScreen} exact></Route>
이제 actions을 업데이트 해준다.
(필터 추가)
여기서 max를 0으로 해놓는 이유는 라우터에서 0과 비교해놓을 것이기 때문이다.
export const listProducts = ({seller='', name='', category = '', min=0, max=0}) =>async(dispatch)=>{ dispatch({ type:PRODUCT_LIST_REQUEST }); try{ const {data} = await Axios.get(`/api/products?seller=${seller}&name=${name}&category=${category}&min=${min}&max=${max}`); dispatch({type:PRODUCT_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_LIST_FAIL, payload:error.message}); } }
라우터도 수정하자 .
productRouter.get('/' ,expressAsyncHandler(async(req, res)=>{ const seller = req.query.seller || '' const name = req.query.name || '' const category = req.query.category || '' const min = req.query.min && Number(req.query.min) !== 0? Number(req.query.min):0 const max = req.query.max && Number(req.query.max) !== 0? Number(req.query.max):0 const sellerFilter = seller? {seller} :{}; const nameFilter = name? {name:{$regex:name, $options:'i'}} :{}; const categoryFilter = category? {category} :{}; const priceFilter = min && max ? {price: {$gte:min, $lte:max}}:{}; const products = await Product.find({...sellerFilter, ...nameFilter, ...categoryFilter, ...priceFilter}).populate('seller', 'seller.name seller.logo'); res.send(products); }));
index.css
/* Search */ .search button{ border-radius:0 0.5rem 0.5rem 0; border-right: none; margin-right: 0.5rem; } .search input{ border-radius: 0.5rem 0 0 0.5rem; border-right: none; margin-left: 0.5rem; } .active{ font-weight:bold; }
이제 Rating으로 필터도 추가해보자.
searchscreen의 템플릿
<div> <h3>Price</h3> <ul> {prices.map(p=>( <li key={p.name}> <Link className={`${p.min}-${p.max}`=== `${min}-${max}`? 'active':''} to={getFilterUrl({min:p.min, max:p.max})} >{p.name}</Link> </li> ))} </ul> </div> <div> <h3>Avg. Customer Reviews</h3> <ul> {ratings.map(r=>( <li key={r.name}> <Link className={`${r.rating}`===`${rating}` 'active':''} to={getFilterUrl({rating:r.rating})} ><Rating></Rating></Link> </li> ))} </ul> </div> </div>
여기서 Rating은 component인데 여기서 조금 수정해보자.
import React from 'react' function Rating(props) { const {rating, numReviews,caption} =props; return ( <div className="rating"> <span><i className={ rating>=1? "fa fa-star": rating>=0.5?"fa fa-star-half-o":"fa fa-star-o" }></i></span> <span><i className={ rating>=2? "fa fa-star": rating>=1.5?"fa fa-star-half-o":"fa fa-star-o" }></i></span> <span><i className={ rating>=3? "fa fa-star": rating>=2.5?"fa fa-star-half-o":"fa fa-star-o" }></i></span> <span><i className={ rating>=4? "fa fa-star": rating>=3.5?"fa fa-star-half-o":"fa fa-star-o" }></i></span> <span><i className={ rating>=5? "fa fa-star": rating>=4.5?"fa fa-star-half-o":"fa fa-star-o" }></i></span> <span>{numReviews + ' reviews'}</span> {caption? <span>{caption}</span> : <span>{numReviews + 'reviews'}</span>} </div> ) } export default Rating
searchScreen.js
const {name = 'all', category='all', min=0, max=0, rating=0} = useParams(); useEffect(() => { dispatch(listProducts({name:name!=='all'? name: '', category:category !=='all'? category:'', min, max,rating})) }, [category, dispatch,name,min, max,rating]) const getFilterUrl = (filter) =>{ const filterCategory = filter.category ||category; const filterName = filter.name ||name; const filterMin = filter.min? filter.min :filter.min===0? 0:min; const filterMax = filter.max? filter.max :filter.max===0? 0:max; const filterRating = filter.rating||rating; return `/search/category/${filterCategory}/name/${filterName}/min/${filterMin}/max/${filterMax}/rating/${filterRating}` } <div> <h3>Avg. Customer Reviews</h3> <ul> {ratings.map(r=>( <li key={r.name}> <Link className={`${r.rating}`===`${rating}`? 'active':''} to={getFilterUrl({rating:r.rating})} ><Rating caption={" & up"} rating={r.rating}></Rating></Link> </li> ))} </ul> </div>
product actions
export const listProducts = ({seller='', name='', category = '', min=0, max=0, rating=0}) =>async(dispatch)=>{ dispatch({ type:PRODUCT_LIST_REQUEST }); try{ const {data} = await Axios.get(`/api/products?seller=${seller}&name=${name}&category=${category}&min=${min}&max=${max}&rating=${rating}`); dispatch({type:PRODUCT_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_LIST_FAIL, payload:error.message}); } }
productRouter
productRouter.get('/' ,expressAsyncHandler(async(req, res)=>{ const seller = req.query.seller || '' const name = req.query.name || '' const category = req.query.category || '' const min = req.query.min && Number(req.query.min) !== 0? Number(req.query.min):0 const max = req.query.max && Number(req.query.max) !== 0? Number(req.query.max):0 const rating = req.query.rating && Number(req.query.rating) !== 0? Number(req.query.rating):0 const sellerFilter = seller? {seller} :{}; const nameFilter = name? {name:{$regex:name, $options:'i'}} :{}; const categoryFilter = category? {category} :{}; const priceFilter = min && max ? {price: {$gte:min, $lte:max}}:{}; const ratingFilter = rating ? {rating: {$gte:rating}}:{}; const products = await Product.find({...sellerFilter, ...nameFilter, ...categoryFilter, ...priceFilter, ...ratingFilter}).populate('seller', 'seller.name seller.logo'); res.send(products); }));
app.js에 루트 추가(있던거에 rating만 추가)
<Route path="/search/category/:category/name/:name/min/:min/max/:max/rating/:rating" component={SearchScreen} exact></Route>
any카테고리를 넣자.
searchScreen.js
<div> <h3>Department</h3> {loadingCategories? <LoadingBox></LoadingBox> : errorCategories? <MessageBox variant='danger'>{errorCategories}</MessageBox> : ( <ul> <li> <Link to ={getFilterUrl({category:'all'})} className={'all'==='category'? 'active':''}>Any</Link> </li> {categories.map(c=> ( <li key = {c}> <Link to ={getFilterUrl({category:c})} className={c===category? 'active':''}>{c}</Link> </li> )) } </ul> )} </div>
이제 sorting을 넣어보자.
sorting은 selectbox가 될것이다.
const {name = 'all', category='all', min=0, max=0, rating=0, order='newest'} = useParams(); useEffect(() => { dispatch(listProducts({name:name!=='all'? name: '', category:category !=='all'? category:'', min, max,rating,order})) }, [category, dispatch,name,min, max,rating,order]) const getFilterUrl = (filter) =>{ const filterCategory = filter.category ||category; const filterName = filter.name ||name; const filterMin = filter.min? filter.min :filter.min===0? 0:min; const filterMax = filter.max? filter.max :filter.max===0? 0:max; const filterRating = filter.rating||rating; const sortOrder = filter.order||order; return `/search/category/${filterCategory}/name/${filterName}/min/${filterMin}/max/${filterMax}/rating/${filterRating}/order/${sortOrder}` } <div className="row"> {loading? <LoadingBox></LoadingBox> : error? <MessageBox variant='danger'>{error}</MessageBox> : (<div> {products.length} Results </div> )} <div> Sort by {' '} <select value = {order} onChange={e=>props.history.push(getFilterUrl({order:e.target.value}))}> <option value="newest">Newest Arrivals</option> <option value="lowest">Price: Low to High</option> <option value="highest">Price: High to Low</option> <option value="toprated">Avg. Customer Reviews</option> </select> </div> </div>
productActions
export const listProducts = ({seller='', name='', category = '', min=0, max=0, rating=0,order=''}) =>async(dispatch)=>{ dispatch({ type:PRODUCT_LIST_REQUEST }); try{ const {data} = await Axios.get(`/api/products?seller=${seller}&name=${name}&category=${category}&min=${min}&max=${max}&rating=${rating}&order=${order}`); dispatch({type:PRODUCT_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_LIST_FAIL, payload:error.message}); } }
productRouter
productRouter.get('/' ,expressAsyncHandler(async(req, res)=>{ const seller = req.query.seller || '' const name = req.query.name || '' const category = req.query.category || '' const min = req.query.min && Number(req.query.min) !== 0? Number(req.query.min):0 const max = req.query.max && Number(req.query.max) !== 0? Number(req.query.max):0 const rating = req.query.rating && Number(req.query.rating) !== 0? Number(req.query.rating):0 const order = req.query.order || '' const sellerFilter = seller? {seller} :{}; const nameFilter = name? {name:{$regex:name, $options:'i'}} :{}; const categoryFilter = category? {category} :{}; const priceFilter = min && max ? {price: {$gte:min, $lte:max}}:{}; const ratingFilter = rating ? {rating: {$gte:rating}}:{}; const sortOrder = order==='lowest'? {price:1} : order==='highest'? {price:-1} : order==='toprated'? {rating:-1} : {_id:-1}; const products = await Product.find({...sellerFilter, ...nameFilter, ...categoryFilter, ...priceFilter, ...ratingFilter}).populate('seller', 'seller.name seller.logo').sort(sortOrder); res.send(products); }));
app.js에 원래 있던거에 Order 추가
<Route path="/search/category/:category/name/:name/min/:min/max/:max/rating/:rating/order/:order" component={SearchScreen} exact></Route>
'NODE.JS' 카테고리의 다른 글
아마존 E-commerce 클론 -29) Google Map적용하기 (0) 2021.05.30 아마존 E-commerce 클론 -28) Rate & Review 생성하기 (0) 2021.05.30 아마존 E-commerce 클론 -26) Top Seller Carousel 추가하고, 장바구니 설정하기(동일 주문자의 상품만 장바구니에 추가가능) (0) 2021.05.29 아마존 E-commerce 클론 -25) Seller 정보페이지 만들기 (0) 2021.05.29 아마존 E-commerce 클론 -24) Seller 페이지 만들기 (Product, Order) (0) 2021.05.29