-
Social Media 만들기 - 6) search 기능 만들기 (Header)NODE.JS 2021. 7. 6. 15:52
이제 앞서 만들어놓은 페이지들을 만들자.
pages > message.js, notify.js, discover.js를 만들어준다.
profile은 pages>profile>[id].js파일을 만들어준다.
이제 만들어진 페이지들을 로그인이 되어있지 않다면 route로 이동하지 못하도록 변경할 것이다.
PageRender.js만들어 놓은 것을 src>custom>폴더 안으로 옮기도록 한다.
auth를 useSelector로 가지고오고, auth.token이 있다면 페이지를 렌더하도록 설정한다. (없으면 렌더 안함)
import React from 'react' import { useSelector } from 'react-redux'; import {useParams} from 'react-router-dom'; import NotFound from '../components/NotFound' const generatePage = (pageNum)=>{ const component = ()=> require(`../pages/${pageNum}`).default try { return React.createElement(component()) } catch (err){ return <NotFound /> } } const PageRender = () => { const {page, id} = useParams() const {auth} = useSelector(state => state) let pageNum = ""; if(auth.token){ if(id){ pageNum = `${page}/[id]` }else{ pageNum = `${page}` } } return generatePage(pageNum) } export default PageRender
로그인이 안되었을 때, register 페이지는 갈 수 있도록 app.js에 register 를 추가해준다.
import Register from './pages/register'; <Route exact path="/register" component={Register}/> </div> </div> </Router> ); } export default App;
privateRouter.js를 customRouter에 만들어준다.
import React from 'react' import { Route, Redirect } from 'react-router-dom'; const privateRouter = (props) => { const firstLogin = localStorage.getItem('firstLogin'); return firstLogin ? <Route {...props} /> : <Redirect to= "/"/> } export default privateRouter
app.js에 privateRouter를 추가해준다.
page render는 privateRouter로 지정해준다.
import PrivateRouter from './customRouter/PrivateRouter'; <PrivateRouter exact path="/:page" component={PageRender}/> <PrivateRouter exact path="/:page/:id" component={PageRender}/>
css도 옮겨준다.
styles>globa.css에 index.css내용을 옮기고 index.css를 삭제해준다.
index.js파일에도 import './styles/global.css'해준다.
global.css
* { padding: 0; box-sizing: border-box; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } #theme { display: none; } #theme:checked ~ .App { filter: invert(1) } .App { width: 100%; min-height: 100vh; background: white; } .main{ max-width: 1000px; width: 100%; margin:auto; } img{ object-fit: cover; } .avatar { width: 30px ; height: 30px ; border-radius: 50% ; border: 2px solid black; } /* ------AUTH--------- */ @import url("./auth.css"); /* ------LOADING--------- */ @import url("./loading.css"); /* ------LOADING--------- */ @import url("./header.css");
Avatar css를 만들고
global에서 옮겨준다.
.avatar { width: 30px ; height: 30px ; border-radius: 50% ; border: 2px solid black; }
/* ------AVATAR--------- */ @import url("./avatar.css");
avatar.css를 수정해준다.
.big-avatar { width: 40px ; height: 40px ; border-radius: 50% ; border: 2px solid black; } .medium-avatar { width: 30px ; height: 30px ; border-radius: 50% ; border: 2px solid black; } .small-avatar { width: 20px ; height: 20px ; border-radius: 50% ; border: 2px solid black; }
Avatar.js로 가서 size를 prop으로 받도록 설정하고
header.js에서 prop으로 사이즈를 넘겨주자.
import React from 'react' import { useSelector } from 'react-redux' const Avatar = ({src, size}) => { const {theme} = useSelector(state => state) return ( <img src={src} alt="avatar" className={size} style={{filter: `${theme ? 'invert(1)': 'invert(0)'}`}}/> ) } export default Avatar
<Avatar src={auth.user.avatar} size = "medium-avatar"/>
component>header폴더를 만들고 Header.js를 옮겨준다.
또 header 폴더안에 Menu.js를 만들고 Header.js안의 메뉴 부분을 옮겨준다.
import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { Link, useLocation } from 'react-router-dom' import Avatar from '../Avatar'; import { logout } from '../../redux/actions/authActions'; const Menu = () => { const navLinks = [ {label:'Home', icon:'home', path:'/'}, {label:'Message', icon:'near_me', path:'/message'}, {label:'Discover', icon:'explore', path:'/discover'}, {label:'Notify', icon:'favorite', path:'/notify'}, ] const {auth, theme} = useSelector(state => state) const dispatch = useDispatch() const {pathname} = useLocation() const isActive = (pn) =>{ if(pn ===pathname){ return 'active' } } return ( <div className="menu" > <ul className="navbar-nav flex-row"> { navLinks.map((link, index)=>( <li className={`nav-item px-2 ${isActive(link.path)}`} key={index}> <Link className="nav-link" to={link.path}> <span className="material-icons">{link.icon}</span> </Link> </li> )) } <li className="nav-item dropdown" style={{opacity: 1}} > <span className="nav-link dropdown-toggle" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <Avatar src={auth.user.avatar} size = "medium-avatar"/> </span> <div className="dropdown-menu" aria-labelledby="navbarDropdown"> <Link className="dropdown-item" to={`/profile/${auth.user._id}`}>Profile</Link> <label className="dropdown-item" htmlFor="theme">{theme ? "Light mode":'Dark mode'}</label> <div className="dropdown-divider"></div> <Link className="dropdown-item" to="/" onClick={()=>dispatch(logout())}>Logout</Link> </div> </li> </ul> </div> ) } export default Menu
Header.js안에 Menu 넣어준다.
import React from 'react' import {Link} from 'react-router-dom'; import Menu from './Menu'; const Header = () => { return ( <nav className="navbar navbar-expand-lg navbar-light bg-light justify-content-between align-middle"> <Link to="/"> <h1 className="navbar-brand text-uppercase p-0 m-0" >Social Network</h1></Link> <Menu/> </nav> ) } export default Header
header>Search.js를 만들어준다.
Header.js에 넣어준다.
header에 div.header도 추가해준다.
import React from 'react' import {Link} from 'react-router-dom'; import Menu from './Menu'; import Search from './Search'; const Header = () => { return ( <div className="header bg-light"> <nav className="navbar navbar-expand-lg navbar-light bg-light justify-content-between align-middle"> <Link to="/"> <h1 className="navbar-brand text-uppercase p-0 m-0" >Social Network</h1> </Link> {/* <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span className="navbar-toggler-icon"></span> </button> */} <Search/> <Menu/> </nav> </div> ) } export default Header
Search.js
import React, { useState } from 'react' const Search = () => { const [search, setSearch] = useState('') return ( <form className="search_form"> <input type="text" name = "search" value={search} id="search" onChange={e=>setSearch(e.target.value.toLowerCase().replace(/ /g, ''))} /> <div className="search_icon"> <span className="material-icons">search</span> <span>Search</span> </div> <div className="close_search">×</div> </form> ) } export default Search
styles>header.css
.header{ width: 100%; min-height: 70px; position: sticky; top: 0; left: 0; z-index: 2; box-shadow: 0 0 10px #ddd; } .header .navbar{ width: 100%; height: 100%; } .header .logo h1{ font-size: 2rem; } /* Header Menu */ .header .menu li{ opacity: 0.5; } .header .menu li.active{ opacity: 1; } .header .menu .material-icons{ font-size: 30px; } .header .menu .dropdown-menu{ position: absolute; left: inherit; right: 0; } .header .menu labe{ cursor: pointer; } /* Header Search */ .header .search_form{ position: relative; } .header .search_form #search{ background:#fafafa; border: 1px solid #ddd; min-width: 250px; width: 100%; outline: none; text-indent: 5px; border-radius: 3px; } .header .search_form .search_icon{ position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 12px; pointer-events: none; } .header .search_form .search_icon .material-icons{ font-size: 14px; transform: translateY(3px); } .header .search_form .close_search{ position: absolute; top: 0; right: 5px; cursor: pointer; font-weight:900; color: crimson; }
search.js 에도 serch문구에 opacity를 준다.
import React, { useState } from 'react' const Search = () => { const [search, setSearch] = useState('') return ( <form className="search_form"> <input type="text" name = "search" value={search} id="search" onChange={e=>setSearch(e.target.value.toLowerCase().replace(/ /g, ''))} /> <div className="search_icon" style={{opacity: search? 0 : 0.3}}> <span className="material-icons">search</span> <span>Search</span> </div> <div className="close_search">×</div> </form> ) } export default Search
이제 검색된 사용자를 찾는 것을 진행한다.
middleware>auth.js를 만들어준다.
유저가 존재하는지 확인한다.
const Users = require("../models/userModel") const jwt = require('jsonwebtoken') const auth = async(req, res, next)=>{ try{ const token = req.header("Authorization") if(!token) return res.status(500).json({msg:"Invalid Authentication"}) const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET) if(!decoded) return res.status(500).json({msg:"Invalid Authentication"}) const user = await Users.findOne({_id:decoded.id}) req.user = user next() }catch(err){ return res.status(500).json({msg:err.message}) } } module.exports = auth
Controller>userCtrl.js
사용자를 찾는 쿼리를 작성한다.
const Users = require('../models/userModel') const userCtrl={ searchUser: async(req, res)=>{ try{ const users = await Users.find({username:{$reger:req.query.username}}) .limit(10).select("fullname username avatar") res.json({users}) }catch(err){ return res.status(500).json({msg:err.message}) } } } module.exports = userCtrl
routers>userRouter.js
const router = require('express').Router() const auth = require("../middleware/auth") const userCtrl = require("../controllers/userCtrl") router.get('/search', auth, userCtrl.searchUser) module.exports = router
serve.js
// Routes app.use('/api', require('./routes/authRouter')) app.use('/api', require('./routes/userRouter'))
search.js에서 이제 API이용해서 정보를 가져올 것이다.
import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { getDataAPI } from '../../utils/fetchData' import { GLOBALTYPES } from '../../redux/actions/globalTypes' import {Link} from 'react-router-dom' import UserCard from '../UserCard' const Search = () => { const [search, setSearch] = useState('') const [users, setUsers] = useState([]) const {auth} = useSelector(state => state) const dispatch = useDispatch() useEffect(() => { if(search&&auth.token){ getDataAPI(`search?username=${search}`, auth.token) .then(res=>setUsers(res.data.users)) .catch(err=>{ dispatch({ type:GLOBALTYPES.ALERT, payload:{error:err.response.data.msg} }) }) } }, [search, auth.token, dispatch]) return ( <form className="search_form"> <input type="text" name = "search" value={search} id="search" onChange={e=>setSearch(e.target.value.toLowerCase().replace(/ /g, ''))} /> <div className="search_icon" style={{opacity: search? 0 : 0.3}}> <span className="material-icons">search</span> <span>Search</span> </div> <div className="close_search">×</div> <div className="users"> { users.map(user=>( <Link key={user._id} to ={`/profile/${user._id}`}> <UserCard /> </Link> )) } </div> </form> ) } export default Search
components>UserCard.js
import React from 'react' const UserCard = () => { return ( <div> UserCard </div> ) } export default UserCard
usercard를 편집해주자.
import React from 'react' import Avatar from './Avatar' const UserCard = ({user}) => { return ( <div className="d-flex p-2 align-item-center"> <Avatar src={user.avatar} size="big-avatar"/> <div className="ml-1"> <span className="d-block">{user.username}</span> <small style={{opacity:0.7}}>{user.fullname}</small> </div> </div> ) } export default UserCard
search에서 border를 prop으로 넘겨주고,
usercard에서 이를 처리해서 경계박스를 주자.
<UserCard user={user} border = "border"/>
import React from 'react' import Avatar from './Avatar' const UserCard = ({user, border}) => { return ( <div className={`d-flex p-2 align-item-center ${border}`}> <Avatar src={user.avatar} size="big-avatar"/> <div className="ml-1" style={{transform:'translateY(-2px)'}}> <span className="d-block">{user.username}</span> <small style={{opacity:0.7}}>{user.fullname}</small> </div> </div> ) } export default UserCard
header.css에서 user position을 absolute로 줘서 박스가 늘어나지 않게 하자.
.header .search_form .users{ position: absolute; width: 100%; min-width: 250px; background: #fafafa; min-height: calc(100vh-150px); overflow: auto; margin-top: 3px; }
버튼을 누르면 입력내용과 users[]가 사라지도록설정하자.
import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { getDataAPI } from '../../utils/fetchData' import { GLOBALTYPES } from '../../redux/actions/globalTypes' import {Link} from 'react-router-dom' import UserCard from '../UserCard' const Search = () => { const [search, setSearch] = useState('') const [users, setUsers] = useState([]) const {auth} = useSelector(state => state) const dispatch = useDispatch() const handleClose= () =>{ setSearch('') setUsers([]) } useEffect(() => { if(search&&auth.token){ getDataAPI(`search?username=${search}`, auth.token) .then(res=>setUsers(res.data.users)) .catch(err=>{ dispatch({ type:GLOBALTYPES.ALERT, payload:{error:err.response.data.msg} }) }) }else{ setUsers([]) } }, [search, auth.token, dispatch]) return ( <form className="search_form"> <input type="text" name = "search" value={search} id="search" onChange={e=>setSearch(e.target.value.toLowerCase().replace(/ /g, ''))} /> <div className="search_icon" style={{opacity: search? 0 : 0.3}}> <span className="material-icons">search</span> <span>Search</span> </div> <div className="close_search" style={{opacity: users.length===0? 0:1}} onClick = {handleClose}> × </div> <div className="users"> { search && users.map(user=>( <Link key={user._id} to ={`/profile/${user._id}`} onClick={handleClose}> <UserCard user={user} border = "border"/> </Link> )) } </div> </form> ) } export default Search
x 누르면 다 사라진다.
이제 header.css에서 media 를 줘서 반응형 웹사이트로 만들자.
/* Responsive */ @media (max-width: 768px){ .header .menu{ position: fixed; bottom: 0; left: 0; width: 100%; box-shadow: 0 0 10px #ddd; z-index: 2; } .header .menu .navbar-nav{ display: flex; justify-content: space-around; align-items: center; } .header .menu .dropdown-menu{ bottom: 100%; top: auto; } .header .search_form{ width: 100%; } .header .logo{ margin: auto; } .header .logo h1{ font-size: 1.5rem; } }
'NODE.JS' 카테고리의 다른 글
Social Media 만들기 - 8) follow, unfollow 기능 만들기 (0) 2021.07.10 Social Media 만들기 - 7) userInfoProfile , edit profile 기능 만들기 (0) 2021.07.07 Social Media 만들기 - 5) logout 기능 만들기 (Header) (0) 2021.07.06 Social Media 만들기 - 4) Register 기능 만들기 (0) 2021.07.06 Social Media 만들기 - 3) Redux , RefreshToken (Login 기능 만들기) (0) 2021.07.05