Răsfoiți Sursa

auto complete search

tuyetnhi 1 an în urmă
părinte
comite
848bd06ff2

+ 2 - 1
package.json

@@ -90,6 +90,7 @@
     "yarn": "^1.22.17"
   },
   "dependencies": {
-    "currency.js": "^2.0.4"
+    "currency.js": "^2.0.4",
+    "usehooks-ts": "^2.9.1"
   }
 }

BIN
public/favicon.png


+ 1 - 1
public/index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-
+    <link rel="icon" href="./favicon.png" />
     <title>Metarare</title>
     <!-- Google tag (gtag.js) -->
     <script

+ 47 - 0
src/components/common/AutoCompleteSearch.scss

@@ -0,0 +1,47 @@
+@import "assets/css/_lib";
+
+.complete_box {
+    position: absolute;
+    visibility: inherit;
+    border: 1px solid #eee;
+    border-top: none;
+    box-shadow: 1px 0px 5px 0 rgba(0, 0, 0, 0.16);
+    top: 90%;
+    left: 50%;
+    width: 92%;
+    z-index: 10;
+    background-color: #fff;
+    border-bottom-left-radius: 12px;
+    border-bottom-right-radius: 12px;
+    max-height: 200px;
+    overflow-y: auto;
+    transform: translateX(-50%);
+
+    @include pc {
+        top: 66%;
+        width: 97%;
+    }
+
+    .complete_list {
+        position: relative;
+        z-index: 1;
+
+        .complete_item {
+            cursor: pointer;
+            display: block;
+            padding: 10px 16px;
+            font-size: 14px;
+            line-height: 18px;
+            color: #888;
+
+            &:hover {
+                background-color: #f5f5f5;
+            }
+        }
+    }
+}
+
+.hidden {
+    visibility: hidden;
+    z-index: -1;
+}

+ 31 - 0
src/components/common/AutoCompleteSearch.tsx

@@ -0,0 +1,31 @@
+import * as React from "react";
+import styles from "./AutoCompleteSearch.scss";
+import classNames from "classnames/bind";
+
+const cx = classNames.bind(styles);
+
+export interface IAutoCompleteSearchProps {
+  onClick: (value: string) => void;
+  show: boolean;
+  data: string[];
+}
+
+export default function AutoCompleteSearch({
+  onClick,
+  show,
+  data,
+}: IAutoCompleteSearchProps) {
+  return (
+    <div className={cx("complete_box", !show ? "hidden" : "")}>
+      <div className={cx("complete_list")}>
+        {data.map((item, index) => {
+          return (
+            <div className={cx("complete_item")} key={index} onClick={() => onClick(item)}>
+              <span className={cx("complete_text")}>{item}</span>
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}

+ 59 - 20
src/components/mobile/Search.tsx

@@ -1,47 +1,86 @@
-import React from 'react';
-import classNames from 'classnames/bind';
-import styles from './Search.scss';
+import React, { useRef, useState } from "react";
+import classNames from "classnames/bind";
+import styles from "./Search.scss";
 
-import { ReactComponent as IconSearchGray } from '@assets/img/svg/ico_search_gray.svg';
-import { ReactComponent as IconClose } from '@assets/img/svg/ico_close.svg';
-import useSearch from '@src/hooks/useSearch';
-import { useHistory } from 'react-router';
-import URLInfo from '@src/constants/URLInfo';
+import { ReactComponent as IconSearchGray } from "@assets/img/svg/ico_search_gray.svg";
+import { ReactComponent as IconClose } from "@assets/img/svg/ico_close.svg";
+import useSearch from "@src/hooks/useSearch";
+import { useHistory } from "react-router";
+import URLInfo from "@src/constants/URLInfo";
+import { useOnClickOutside } from "usehooks-ts";
+import AutoCompleteSearch from "../common/AutoCompleteSearch";
 
 const cx = classNames.bind(styles);
 
 interface IOwnProps {
   onClose: () => void;
-};
+}
 
 const Search: React.FC<IOwnProps> = ({ onClose }) => {
   const history = useHistory();
-
+  const searchRef = useRef(null);
+  const [showAutoComplete, setShowAutoComplete] = useState<boolean>(false);
   const onSearch = () => {
     history.push(URLInfo.getSearchResultUrl(keyword));
     onClose();
   };
-  const { keyword, setKeyword, onKeyPress, handleSearch } = useSearch({ onSearch });
+  const SelectTextComplete = (text: string) => {
+    history.push(URLInfo.getSearchResultUrl(text));
+    onClose();
+  };
+
+  const { keyword, setKeyword, onKeyPress, handleSearch } = useSearch({
+    onSearch,
+  });
+
+  const fakeComplete = [
+    "test1",
+    "test2",
+    "test3",
+    "test4",
+    "test5",
+    "test6",
+    "test7",
+    "test8",
+    "test9",
+    "test10",
+  ];
+
+  useOnClickOutside(searchRef, () => {
+    setShowAutoComplete(false);
+  });
 
   return (
-    <div className={cx('ly_search')}>
-      <div className={cx('input_box')}>
+    <div className={cx("ly_search")} ref={searchRef}>
+      <div className={cx("input_box")}>
         <input
           type="text"
           placeholder="작가(NFT)이름을 입력해주세요"
-          className={cx('input_search')}
+          className={cx("input_search")}
           value={keyword}
           onKeyPress={onKeyPress}
           onChange={(e) => setKeyword(e.target.value)}
+          onFocus={() => {
+            setShowAutoComplete(true);
+          }}
         />
-        <button className={cx('btn_search')} onClick={handleSearch}>
-          <span className={cx('blind')}>Search</span>
-          <IconSearchGray className={cx('ico_search')} />
+        <button className={cx("btn_search")} onClick={handleSearch}>
+          <span className={cx("blind")}>Search</span>
+          <IconSearchGray className={cx("ico_search")} />
         </button>
+        <AutoCompleteSearch
+          show={showAutoComplete}
+          data={fakeComplete}
+          onClick={(e) => {
+            setShowAutoComplete(false);
+            SelectTextComplete(e);
+            setKeyword(e);
+          }}
+        />
       </div>
-      <button className={cx('btn_close')} onClick={onClose}>
-        <span className={cx('blind')}>Close</span>
-        <IconClose className={cx('ico_close')} />
+      <button className={cx("btn_close")} onClick={onClose}>
+        <span className={cx("blind")}>Close</span>
+        <IconClose className={cx("ico_close")} />
       </button>
     </div>
   );

+ 41 - 8
src/components/pc/Header.scss

@@ -16,13 +16,16 @@
         z-index: 20;
     }
 }
+
 .logo {
     flex: 0 0 auto;
     margin-right: 16px;
+
     .link_logo {
         display: block;
         padding: 17px 0;
     }
+
     .ico_logo {
         overflow: hidden;
         width: 40px;
@@ -31,20 +34,24 @@
         border-radius: 8px;
         vertical-align: top;
     }
+
     .name {
         font-family: 'Retorica', 'Barun';
         font-size: 28px;
         line-height: 40px;
     }
 }
+
 .gnb {
     display: flex;
     flex: 1 1 auto;
 }
+
 .search_area {
     position: relative;
     flex: 1 1 auto;
     margin: 19px 10px 0 0;
+  
     .input_search {
         display: block;
         width: 100%;
@@ -57,38 +64,46 @@
         font-size: 14px;
         line-height: 18px;
         outline: none;
+
         &::placeholder {
             color: #888;
         }
+
         &:focus {
             background-color: #fff;
             border: 1px solid #16dccf;
             color: #000;
         }
     }
+
     .btn_search {
         position: absolute;
-        top: 0; 
+        top: 0;
         right: 0;
         padding: 10px 16px;
     }
+
     .ico_search {
         vertical-align: top;
-    }   
+    }
 }
-.ico_share{
+
+.ico_share {
     width: 24px;
     height: 24px;
     margin-top: 5px;
     vertical-align: top;
 }
+
 .menu_list {
     white-space: nowrap;
     margin-top: 19px;
+
     .menu_item {
         position: relative;
         display: inline-block;
         vertical-align: top;
+
         .menu {
             display: block;
             padding: 0 20px;
@@ -96,6 +111,7 @@
             font-size: 14px;
             line-height: 36px;
         }
+
         .btn_community {
             padding: 0 20px;
             color: #888;
@@ -104,12 +120,14 @@
             vertical-align: top;
             font-family: 'Retorica', 'Barun';
         }
+
         .ico_arr {
             width: 24px;
             height: 24px;
             margin-top: 5px;
             vertical-align: top;
         }
+
         .ly_community {
             position: absolute;
             top: 40px;
@@ -120,6 +138,7 @@
             background-color: #fff;
             box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
             z-index: 10;
+
             .dimmed {
                 position: fixed;
                 top: 0;
@@ -127,41 +146,49 @@
                 width: 100%;
                 height: 100vh;
             }
+
             .community_list {
                 position: relative;
                 z-index: 1;
             }
+
             .link_community {
                 display: block;
                 padding: 0 16px;
                 line-height: 42px;
                 letter-spacing: -0.42px;
             }
+
             .link_community_number {
                 display: block;
                 padding: 0 16px;
                 line-height: 20px;
                 letter-spacing: -0.42px;
             }
+
             .community_sns {
                 margin-top: 8px;
                 text-align: center;
             }
+
             .link_sns {
                 display: inline-block;
                 padding: 8px 10px 16px;
                 vertical-align: top;
             }
+
             .link_sns_u {
                 display: inline-block;
-                padding: 4px 10px ;
+                padding: 4px 10px;
                 vertical-align: top;
             }
+
             .ico_sns {
                 width: 24px;
                 height: 24px;
                 vertical-align: top;
             }
+
             .ico_sns_facebook {
                 width: 21px;
                 height: 21px;
@@ -170,12 +197,14 @@
         }
     }
 }
+
 .btn_area {
     .link_activity {
         display: inline-block;
         padding: 25px 8px;
         vertical-align: top;
     }
+
     .link {
         display: inline-block;
         height: 36px;
@@ -187,27 +216,31 @@
         line-height: 36px;
         vertical-align: top;
     }
+
     .link_make {
         color: #fff;
-        background: rgb(249,72,160);
-        background: linear-gradient(90deg, rgba(249,72,160,1) 0%, rgba(120,156,187,1) 70%, rgba(22,220,207,1) 100%);
-        font-size:12px;
+        background: rgb(249, 72, 160);
+        background: linear-gradient(90deg, rgba(249, 72, 160, 1) 0%, rgba(120, 156, 187, 1) 70%, rgba(22, 220, 207, 1) 100%);
+        font-size: 12px;
         font-family: 'Retorica', 'Barun';
     }
+
     .link_login {
         min-width: 76px;
         border: 1px solid #ddd;
         line-height: 34px;
         box-sizing: border-box;
-        font-size:12px;
+        font-size: 12px;
         font-family: 'Retorica', 'Barun';
     }
+
     .btn_profile {
         width: 40px;
         height: 40px;
         margin: 17px 0 0 8px;
         border-radius: 8px;
         vertical-align: top;
+
         .img_profile {
             border-radius: 8px;
         }

+ 35 - 4
src/components/pc/Header.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useState, useRef } from "react";
 import classNames from "classnames/bind";
 import styles from "./Header.scss";
 import { Link } from "react-router-dom";
@@ -18,19 +18,25 @@ import useLogin from "@src/hooks/useLogin";
 import { useHistory } from "react-router";
 import { fetchUserInfo } from "@src/store/reducers/UserReducer";
 import { YoutubeIcon } from "../common/icon/youtube";
+import { useOnClickOutside} from "usehooks-ts";
+import AutoCompleteSearch from "../common/AutoCompleteSearch";
 
 const cx = classNames.bind(styles);
 
 const Header: React.FC = () => {
+  const searchRef = useRef(null)
   const history = useHistory();
   const dispatch = useDispatch();
-
+  const [showAutoComplete, setShowAutoComplete] = useState<boolean>(false);
   const { userInfo, isUserInfoLoaded } = useSelector((store) => store.user);
   const onSearch = () => {
     history.push(URLInfo.getSearchResultUrl(keyword));
   };
+  const SelectTextComplete = (text: string) => {
+    history.push(URLInfo.getSearchResultUrl(text));
+  };
   const { keyword, setKeyword, onKeyPress, handleSearch } = useSearch({
-    onSearch
+    onSearch,
   });
   const [showCommunity, setShowCommunity] = useState(false);
   const [showProfile, setShowProfile] = useState<boolean>(false);
@@ -48,6 +54,23 @@ const Header: React.FC = () => {
     }
   };
 
+  const fakeComplete = [
+    "test1",
+    "test2",
+    "test3",
+    "test4",
+    "test5",
+    "test6",
+    "test7",
+    "test8",
+    "test9",
+    "test10",
+  ];
+
+  useOnClickOutside(searchRef, () => {
+    setShowAutoComplete(false);
+  });
+
   return (
     <div className={cx("header_wrap")}>
       <div className={cx("header_group")}>
@@ -58,7 +81,7 @@ const Header: React.FC = () => {
           </Link>
         </h1>
         <div className={cx("gnb")}>
-          <div className={cx("search_area")}>
+          <div className={cx("search_area")} ref={searchRef}>
             <input
               type="text"
               placeholder="작품(NFT)이름을 입력해주세요"
@@ -66,11 +89,19 @@ const Header: React.FC = () => {
               value={keyword}
               onKeyPress={onKeyPress}
               onChange={(e) => setKeyword(e.target.value)}
+              onFocus={() => {
+                setShowAutoComplete(true);
+              }}
             />
             <button className={cx("btn_search")} onClick={handleSearch}>
               <span className={cx("blind")}>Search</span>
               <IconSearchGray className={cx("ico_search")} />
             </button>
+            <AutoCompleteSearch show={showAutoComplete} data={fakeComplete} onClick={(e) => {
+              setShowAutoComplete(false);
+              SelectTextComplete(e);
+              setKeyword(e);
+            }} />
           </div>
           <ul className={cx("menu_list")}>
             <li className={cx("menu_item")}>

+ 30 - 0
src/index.scss

@@ -7,4 +7,34 @@ body {
 }
 body::-webkit-scrollbar {
   display: none; /* Chrome, Safari, Opera*/
+}
+
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+@media screen and (max-width: 1024px) {
+  ::-webkit-scrollbar {
+    width: 3px;
+    height: 3px;
+  }
+}
+  
+
+/* Track */
+::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 10px; 
+}
+ 
+/* Handle */
+::-webkit-scrollbar-thumb {
+  background: #888; 
+  border-radius: 10px;
+}
+
+/* Handle on hover */
+::-webkit-scrollbar-thumb:hover {
+  background: #555; 
 }

+ 5 - 0
yarn.lock

@@ -8910,6 +8910,11 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
+usehooks-ts@^2.9.1:
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.9.1.tgz#953d3284851ffd097432379e271ce046a8180b37"
+  integrity sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==
+
 util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"