ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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">&times;</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">&times;</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">&times;</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}>
                    &times;
                </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;
        }
        
    }

     

     

     

     

     

     

     

     

     

     

     

     

Designed by Tistory.