반응형 네비게이션바, 사이드바, 검색창 만들기

조승윤·2022년 7월 5일

인터넷에 올라와있는 네비게이션바, 사이드바중 기능이나 디자인 부분이 찰떡같이 맘에 드는게 없어 내 스타일대로 만들어보았다. 디자인에 신경을 안쓴것같지만 깔끔한걸 좋아해서 나름 엄청 신경쓴 디자인이다 ㅎㅎ....

메뉴, 하위 그룹, 클릭 시 이동하는 url 등은 스테틱하게 프론트에서 직접 넣어도 되지만 관리자 사이트를 통해 관리하고 싶었다.

사이드바에 필요한 데이터 특성상 한번 받아오면 불필요하게 다시 받아올 필요가 없어 useQuery를 사용하여 불필요하게 불러오는 일이 없도록 구현했다


피씨 화면에서 상단 네비게이션 바, 가운데와 우측에 로고, 장바구니, 검색 버튼 등을 추가할 수 있지만 깔끔한 디자인을 위해 빼버렸다.


모바일화면


사이드바 on


클릭시 하위메뉴 노출


검색버튼 클릭시 나타나는 창이다 인풋과 서치영역 바깥을 클릭하면 검색 되지않고 닫힌다


스크롤시 상단고정

폴더구조

import React, { useState, useRef, useEffect } from "react";
import { useQuery } from "react-query";
import axios from "axios";
import { Link } from "react-router-dom";
import Submenu from "./submenu";
import Hiddensearch from "./hiddensearch";

const Navbar = () => {
  const sidebar_ref = useRef();
  const [menuToggle, setMenuToggle] = useState(false);
  const [hidden_search, set_hidden_search] = useState(false);
  const showSidebar = () => setMenuToggle(!menuToggle);
  
  const handleClickOutside = (event) => {
    if (sidebar_ref && !sidebar_ref.current.contains(event.target)) {
      setMenuToggle(false);
    } else {
      setMenuToggle(true);
    }
  };
  
  useEffect(() => {
    if (hidden_search) {
      return;
    }
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  });
  
  const search_page_on = () => {
    set_hidden_search(true);
  };
  
  const get_Sidebar_data = useQuery(
    ["SidebarData"],
    async () => {
      const data = await axios.get("url" + "/board");
      return data.data;
    },
    {
      staleTime: Infinity,
      cacheTime: Infinity,
    }
  );
  return (
    <>
      {hidden_search ? <Hiddensearch set_hidden_search={set_hidden_search} hidden_search={hidden_search} setSidebar={setMenuToggle} /> : ""}
      <div className="head_bar">
        {/* 모바일상단*/}
        <div className="monile_flex">
          <Link to="#" className="mobile_head_icon">
            <span onClick={showSidebar}>버거</span>
          </Link>
          <Link className="NavText" to="/">
            untitled
          </Link>
          <Link to="/cart" className="mobile_head_icon">
            icon
          </Link>
        </div>
        {/* 피씨상단*/}
        <div className="pc_flex">
          <div>
            {get_Sidebar_data?.data?.map((item) => (
              <Link to={item.bgr_key} className="pc_menu" key={item.bgr_order}>
                {item.bgr_name}
              </Link>
            ))}
          </div>
          <div>
            <Link to="/" className="pc_menu">
              untitled
            </Link>
          </div>
          <div>
            <Link to="/" className="pc_menu">
              icon
            </Link>
          </div>
        </div>
      </div>
      {/* ///// */}
      <div className={menuToggle ? "sidebar sideon" : "sidebar"} ref={sidebar_ref}>
        <div className="SidebarWrap">
          <div className="side_top_box">
            <div className="side_wrap">
              <span>
                <Link to="/login">Login</Link>
              </span>
              <span>
                <Link to="/join">Join us</Link>
              </span>
              <span>
                <Link to="/cart">Cart</Link>
              </span>
            </div>
          </div>
          {get_Sidebar_data?.data?.map((item, index) => (
            <Submenu item={item} key={index} showSidebar={showSidebar} />
          ))}
          <div className="static_menu" onClick={search_page_on}>
            <Link to="#">search</Link>
          </div>
        </div>
      </div>
    </>
  );
};

export default Navbar;
import React, { useState } from "react";
import { Link } from "react-router-dom";

const Submenu = ({ item, showSidebar }) => {
  const [subnav, setSubnav] = useState(false);

  const showSubnav = (e) => {
    setSubnav((subnav) => !subnav);
  };

  return (
    <>
      <Link to={item.brd_key || "#"} onClick={item.Boards ? showSubnav : showSidebar} className="submenu_atag">
        <div>
          <span className="SidebarLabel">{item.bgr_name}</span>
        </div>
      </Link>
      {subnav &&
        item.Boards.map((item, index) => {
          return (
            <Link to={item.brd_key} key={index} onClick={showSidebar} className="DropdownLink">
              <span className="SidebarLabel">{item.brd_name}</span>
            </Link>
          );
        })}
    </>
  );
};

export default Submenu;

hiddensearch.js

import React, { useState, useRef, useEffect } from "react";

const Hiddensearch = ({ set_hidden_search, hidden_search, setSidebar }) => {
  const [inputText, setinputText] = useState("");
  const ref = useRef();
  const shadow_click = (e) => {
    if (ref.current?.contains(e.target) === false) {
      set_hidden_search(!hidden_search);
    }
  };
  useEffect(() => {
    document.addEventListener("mousedown", shadow_click);
    return () => {
      document.removeEventListener("mousedown", shadow_click);
    };
  }, []);

  const input_handle = (e) => {
    setinputText(e.target.value);
  };
  const search_button = () => {
    if (inputText === "") {
      alert("검색어를 입력해주세요");
    } else {
      // history.push("/search/" + encodeURIComponent(inputText) + "/1"); 검색시사용
      set_hidden_search(!hidden_search);
      setSidebar(false);
    }
  };

  return (
    <div className="search_container">
      <div className="H_searchFormWrap" ref={ref} style={{ margin: "0 auto", width: "250px", marginTop: "50px" }}>
        <input className="H_search_input" onChange={input_handle}></input>
        <span className="H_search_button" onClick={search_button}>
          search
        </span>
      </div>
    </div>
  );
};

export default Hiddensearch;

css

.Header{
     position: fixed;
     top: 0;
     left: 0;
     z-index: 99;
     height: 70px;
     width: 100%;
     min-width: 100%;
}
 .head_bar {
     background: white;
     border-bottom: 1px solid #444444;
     width: 100%;
     min-width: 100%;
}
 .pc_flex{
     height: 70px;
     display: flex;
     justify-content: space-between;
}
 .pc_flex >div{
     margin: auto 2px;
}
 @media screen and (max-width: 768px) {
     .pc_flex {
         display: none;
    }
}
 .monile_flex{
     height: 70px;
     display: none;
     justify-content: space-between;
}
 .monile_flex > a{
     margin: auto 2px;
}
 @media screen and (max-width: 768px) {
     .monile_flex {
         display: flex;
    }
}
 .mobile_head_icon {
     margin-left: 1rem;
     margin-right: 1rem;
     font-size: 2rem;
     color: black;
}
 .pc_menu {
     color: black;
     margin: 10px;
     cursor: pointer;
     font-size:15px;
}
 .sidebar{
     display: none;
     background: white;
     width: 75%;
     height: 100vh;
     justify-content: center;
     position: fixed;
     left: -100%;
     top: 0;
     transition: 350ms;
     z-index: 100;
}
 .sideon{
     box-shadow:rgba(0, 0, 0, 0.5) 0 0 0 9999px;
     left: 0%;
}
 @media screen and (max-width: 768px) {
     .sidebar {
         display: flex;
    }
}
 .SidebarWrap {
     width: 100%;
}
/* 서브메뉴 */
 .SidebarLabel {
     margin-left: 16px;
}
 .submenu_atag{
     display: flex;
     justify-content: space-between;
     align-items: center;
     padding: 10px;
     height: 10px;
     text-decoration: none;
     font-size: 16px;
}
 .submenu_atag:hover{
     cursor: pointer;
}
 .DropdownLink {
     background: white;
     height: 30px;
     padding-left: 4rem;
     display: flex;
     align-items: center;
     text-decoration: none;
     font-size: 13px;
}
 .DropdownLink:hover {
     cursor: pointer;
}
 .side_top_box {
     width: 100%;
     height: 70px;
     display: flex;
     align-items: center;
}
 .side_wrap {
     margin-left: 25px;
}
 .side_wrap > span {
     margin-right: 15px;
     font-weight: 550;
     cursor: pointer;
}
 .static_menu {
     margin-left: 16px;
     display: flex;
     justify-content: space-between;
     align-items: center;
     padding: 10px;
     height: 10px;
     text-decoration: none;
     font-size: 16px;
}
/* 히든서치 */
 .search_container {
     padding-top: 200px;
     position: fixed;
     width: 100%;
     height: 100%;
     z-index: 1000;
     background: rgba(255, 255, 255, 0.9);
}
 .H_search_input {
     width: 200px;
     background: rgba(255, 255, 255, 0.9);
     border: 0;
     padding: 0;
     height: 25px;
     font-size: 20px;
     background: transparent;
}
 .H_search_button {
     width: 50px;
     border: 0px;
     color: #000;
     font-size: 12px;
     margin-top: 5px;
     padding-left: 10px;
}
 .H_search_input:focus {
     outline: none;
}
 .H_searchFormWrap {
     border-bottom: 1px solid #000;
}

0개의 댓글