아마존 E-commerce 클론 -27) Search Box ,Search Filter기능 , Side bar, sort & Filter 만들기
우선 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>