From 9cd274212a8d9ced7af491ec59eaed03f59a46d0 Mon Sep 17 00:00:00 2001 From: Alper Pacin Date: Tue, 3 Oct 2023 00:54:17 +0300 Subject: [PATCH] feat: searchbar slice --- components/Dropdown/Dropdown.jsx | 50 ------------ components/GameDropdown/game-dropdown.jsx | 68 ++++++++++++++++ .../game-dropdown.module.css} | 10 +-- components/RegionDropdown/region-dropdown.jsx | 69 ++++++++++++++++ .../RegionDropdown/region-dropdown.module.css | 80 ++++++++++++++++++ components/SearchBar/SearchBar.jsx | 66 ++++++++------- components/Sidebar/Sidebar.jsx | 81 +++++++++++-------- hooks/useOutsideClick.js | 22 +++++ package-lock.json | 1 + package.json | 1 + pages/_app.js | 43 ++++++---- pages/valorant.js | 25 ++++++ public/images/brand-logo.svg | 16 ++++ public/json/platform-api-routes.js | 12 ++- store/middleware/routeChangeMiddleware .js | 27 +++++++ store/slices/searchBarSlice.js | 62 ++++++++++++++ store/store.js | 5 ++ styles/_scrollbar.css | 42 ++++++++++ styles/_sidebar.css | 16 +--- styles/globals.css | 1 + 20 files changed, 547 insertions(+), 150 deletions(-) delete mode 100644 components/Dropdown/Dropdown.jsx create mode 100644 components/GameDropdown/game-dropdown.jsx rename components/{Dropdown/Dropdown.module.css => GameDropdown/game-dropdown.module.css} (90%) create mode 100644 components/RegionDropdown/region-dropdown.jsx create mode 100644 components/RegionDropdown/region-dropdown.module.css create mode 100644 hooks/useOutsideClick.js create mode 100644 pages/valorant.js create mode 100644 public/images/brand-logo.svg create mode 100644 store/middleware/routeChangeMiddleware .js create mode 100644 store/slices/searchBarSlice.js create mode 100644 styles/_scrollbar.css diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx deleted file mode 100644 index 43045a9..0000000 --- a/components/Dropdown/Dropdown.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useState } from "react"; -import classes from "./Dropdown.module.css"; -import Image from "next/image"; - -const Dropdown = ({ items }) => { - const [isOpen, setOpen] = useState(false); - const [selectedItem, setSelectedItem] = useState(items[0]); - - const toggleDropdown = () => setOpen(!isOpen); - - const handleItemClick = (item) => { - setOpen(false); - setSelectedItem(item); - }; - - return ( -
-
- {`selected -
-
- {items.map((item) => ( -
handleItemClick(item)} - key={item.id} - > - {`selected -
- ))} -
-
- ); -}; - -export default Dropdown; diff --git a/components/GameDropdown/game-dropdown.jsx b/components/GameDropdown/game-dropdown.jsx new file mode 100644 index 0000000..1ad9137 --- /dev/null +++ b/components/GameDropdown/game-dropdown.jsx @@ -0,0 +1,68 @@ +import { updateGameOpen, updateGameValue } from "@/store/slices/searchBarSlice"; +import classes from "./game-dropdown.module.css"; +import Image from "next/image"; +import { useDispatch } from "react-redux"; +import { useRef } from "react"; +import useOutsideClick from "@/hooks/useOutsideClick"; +import { Skeleton } from "../ui/skeleton"; + +const Dropdown = ({ items, searchBarState }) => { + const dispatch = useDispatch(); + const ref = useRef(null); + + const closeDropdown = () => { + dispatch(updateGameOpen(false)); + }; + + useOutsideClick(ref, null, closeDropdown); + + const toggleDropdown = () => { + dispatch(updateGameOpen(!searchBarState.game.open)); + }; + + const handleItemClick = (item) => { + dispatch(updateGameOpen(false)); + dispatch(updateGameValue(item)); + }; + + return ( +
+
+ {searchBarState.game.value ? ( + {`selected + ) : ( + + )} +
+
+ {items.map((item) => ( +
handleItemClick(item)} + key={item.id} + > + {`selected +
+ ))} +
+
+ ); +}; + +export default Dropdown; diff --git a/components/Dropdown/Dropdown.module.css b/components/GameDropdown/game-dropdown.module.css similarity index 90% rename from components/Dropdown/Dropdown.module.css rename to components/GameDropdown/game-dropdown.module.css index 3841e1c..079ee36 100644 --- a/components/Dropdown/Dropdown.module.css +++ b/components/GameDropdown/game-dropdown.module.css @@ -1,5 +1,5 @@ .dropdown { - width: 100%; + width: 2.5rem; height: 2.5rem; } @@ -22,19 +22,13 @@ position: absolute; top: 100%; left: 0; - width: 3rem; + width: 6rem; z-index: -1; pointer-events: none; transition: 0.1s ease-in-out all; padding-top: 1rem; } -@media (min-width: 640px) { - .dropdown-body { - width: 6rem; - } -} - .dropdown-body.open { pointer-events: all; opacity: 1; diff --git a/components/RegionDropdown/region-dropdown.jsx b/components/RegionDropdown/region-dropdown.jsx new file mode 100644 index 0000000..fb90592 --- /dev/null +++ b/components/RegionDropdown/region-dropdown.jsx @@ -0,0 +1,69 @@ +import classes from "./region-dropdown.module.css"; +import { useDispatch } from "react-redux"; +import { + updateRegionOpen, + updateRegionValue, +} from "@/store/slices/searchBarSlice"; +import { useRef } from "react"; +import useOutsideClick from "@/hooks/useOutsideClick"; +import { Skeleton } from "../ui/skeleton"; + +const Dropdown = ({ items, searchBarState }) => { + const dispatch = useDispatch(); + const ref = useRef(null); + + const closeDropdown = () => { + dispatch(updateRegionOpen(false)); + }; + + useOutsideClick(ref, null, closeDropdown); + + const toggleDropdown = () => { + dispatch(updateRegionOpen(!searchBarState.region.open)); + }; + + const handleItemClick = (item) => { + dispatch(updateRegionOpen(false)); + dispatch(updateRegionValue(item)); + }; + + return ( +
+ {searchBarState.region.value ? ( +
+ + {searchBarState.region.value?.label} + +
+ ) : ( + + )} + +
+ {items.map((item) => ( +
handleItemClick(item)} + key={item.code} + > + + {item.label} + +
+ ))} +
+
+ ); +}; + +export default Dropdown; diff --git a/components/RegionDropdown/region-dropdown.module.css b/components/RegionDropdown/region-dropdown.module.css new file mode 100644 index 0000000..126bad7 --- /dev/null +++ b/components/RegionDropdown/region-dropdown.module.css @@ -0,0 +1,80 @@ +.dropdown { + width: 2.5rem; + height: 2.5rem; +} + +.dropdown-header { + width: 100%; + height: 100%; + cursor: pointer; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.dropdown-header img { + padding: 0.5rem; +} + +.dropdown-body { + background-color: rgba(23, 26, 34, 0.3); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + opacity: 0; + position: absolute; + top: 100%; + left: 0; + width: 6rem; + z-index: -1; + pointer-events: none; + transition: 0.1s ease-in-out all; + padding-top: 0.5rem; + max-height: 15rem; + overflow-y: auto; +} + +.dropdown-body.open { + pointer-events: all; + opacity: 1; + z-index: 1; +} + +.dropdown-item { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + margin: 0.25rem 0; +} + +.dropdown-item span { + flex: 1; + text-align: center; +} + +.dropdown-item:hover { + cursor: pointer; +} + +.dropdown-item-dot { + opacity: 0; + color: #91a5be; + transition: all 0.2s ease-in-out; +} + +.dropdown-item-dot.selected { + opacity: 1; +} + +.icon { + font-size: 13px; + color: #91a5be; + transform: rotate(0deg); + transition: all 0.2s ease-in-out; +} + +.icon.open { + transform: rotate(90deg); +} diff --git a/components/SearchBar/SearchBar.jsx b/components/SearchBar/SearchBar.jsx index 4d576ff..a30f823 100644 --- a/components/SearchBar/SearchBar.jsx +++ b/components/SearchBar/SearchBar.jsx @@ -1,42 +1,44 @@ import { Input } from "@/components/ui/input"; -import Image from "next/image"; +import { useRouter } from "next/router"; import { useEffect, useRef, useState } from "react"; - import { useForm } from "react-hook-form"; import { AiOutlineClockCircle, AiOutlineStar } from "react-icons/ai"; -import PLATFORM_LIST from "@/public/json/platform-api-routes.js"; +import { + PLATFORM_LIST_LOL, + PLATFORM_LIST_VAL, +} from "@/public/json/platform-api-routes.js"; import GAME_LIST from "@/public/json/game-platforms"; -import Link from "next/link"; -import Dropdown from "../Dropdown/Dropdown"; +import GameDropdown from "../GameDropdown/game-dropdown"; +import RegionDropdown from "../RegionDropdown/region-dropdown"; +import { useDispatch, useSelector } from "react-redux"; +import { updateSearchOpen } from "@/store/slices/searchBarSlice"; +import useOutsideClick from "@/hooks/useOutsideClick"; -const SearchBar = ({ icon }) => { +const SearchBar = (props) => { + const router = useRouter(); + const dispatch = useDispatch(); + const searchBarState = useSelector((state) => state.searchBar); const inputRef = useRef(null); const ref = useRef(null); - const [focused, setFocused] = useState(false); const [activeTab, setActiveTab] = useState("recent"); const { register, handleSubmit } = useForm(); - const [selectedRegion, setSelectedRegion] = useState(); + + const dropdownItems = + router.pathname.includes("/valorant") && + searchBarState.game.value && + searchBarState.game.value.id === "valorant" + ? PLATFORM_LIST_VAL + : PLATFORM_LIST_LOL; const handleFocus = () => { - setFocused(true); + dispatch(updateSearchOpen(true)); }; - //for listening outside clicks - useEffect(() => { - const handleOutsideClick = (e) => { - if ( - !ref?.current?.contains(e.target) && - !inputRef?.current?.contains(e.target) - ) { - setFocused(false); - } - }; + const closeSearchDropdown = () => { + dispatch(updateSearchOpen(false)); + }; - document.addEventListener("click", handleOutsideClick, false); - return () => { - document.removeEventListener("click", handleOutsideClick, false); - }; - }, []); + useOutsideClick(inputRef, ref, closeSearchDropdown); const onSubmit = (data) => console.log(data); @@ -48,11 +50,17 @@ const SearchBar = ({ icon }) => {
-
- +
+ +
+
+
@@ -85,9 +93,9 @@ const SearchBar = ({ icon }) => {
setFocused(true)} + onFocus={() => dispatch(updateSearchOpen(true))} className={`${ - focused + searchBarState.search.open ? "h-48 sm:h-64 translate-y-0 opacity-100" : "h-0 translate-y--2 opacity-0" } overflow-hidden transition-all duration-500 bg-slate-100 rounded-b-sm absolute w-full top-full flex flex-col lg:flex-row border-[1px] border-gray-200`} diff --git a/components/Sidebar/Sidebar.jsx b/components/Sidebar/Sidebar.jsx index bab7a71..3a22bd2 100644 --- a/components/Sidebar/Sidebar.jsx +++ b/components/Sidebar/Sidebar.jsx @@ -1,58 +1,71 @@ "use client"; - +import { createElement } from "react"; import { BsGearFill } from "react-icons/bs"; import TftLogo from "../../public/images/tft-logo.svg"; import ValorantLogo from "../../public/images/valorant-logo.svg"; import LolLogo from "../../public/images/lol-logo.svg"; -import Logo from "../../public/images/icon.svg"; import Link from "next/link"; import { useTranslation } from "next-i18next"; +import BrandLogo from "@/public/images/brand-logo.svg"; +import { useRouter } from "next/router"; + +const SIDEBAR_ICONS = [ + { + logo: LolLogo, + size: 28, + text: "League of Legends", + href: "/", + }, + { + logo: ValorantLogo, + size: 28, + text: "Valorant", + href: "/valorant", + }, + { + logo: TftLogo, + size: 28, + text: "Teamfight Tactics", + href: "/tft", + }, + { + logo: BsGearFill, + size: 22, + textKey: "settings", // Using translation key here + href: "/settings", + }, +]; const SideBar = () => { + const router = useRouter(); const { t } = useTranslation("sidebar"); return (
- } text="TargetKill" href="/" /> +
+ +
- } - text="League of Legends" - href="/league-of-legends" - /> - } - text="Valorant" - href="/valorant" - /> - } - text="Teamfight Tactics" - href="/teamfight-tactics" - /> - } - text={t("settings")} - href="/settings" - /> + {SIDEBAR_ICONS.map((item) => ( + + ))}
); }; -const SideBarIcon = ({ href, icon, text = "tooltip 💡" }) => ( - -
- {icon} - {text} -
- -); - -const SideBarMainIcon = ({ href, icon, text = "tooltip 💡" }) => ( +const SideBarIcon = ({ href, icon, text = "tooltip 💡", isActive }) => ( -
+
{icon} {text}
diff --git a/hooks/useOutsideClick.js b/hooks/useOutsideClick.js new file mode 100644 index 0000000..acfd19f --- /dev/null +++ b/hooks/useOutsideClick.js @@ -0,0 +1,22 @@ +import { useEffect } from "react"; + +function useOutsideClick(ref1, ref2, callback) { + useEffect(() => { + function handleClickOutside(event) { + if ( + ref1.current && + !ref1.current.contains(event.target) && + (!ref2 || + (ref2 && ref2.current && !ref2.current.contains(event.target))) + ) { + callback(); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [ref1, ref2, callback]); +} +export default useOutsideClick; diff --git a/package-lock.json b/package-lock.json index e7a156d..0253f84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "react-icons": "^4.10.1", "react-redux": "^8.1.2", "react-responsive": "^9.0.2", + "redux-thunk": "^2.4.2", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.6" diff --git a/package.json b/package.json index 9a884ba..03b17a8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "react-icons": "^4.10.1", "react-redux": "^8.1.2", "react-responsive": "^9.0.2", + "redux-thunk": "^2.4.2", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.6" diff --git a/pages/_app.js b/pages/_app.js index da3b268..61d1465 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -3,8 +3,10 @@ import { appWithTranslation } from "next-i18next"; import "@/styles/globals.css"; import { Noto_Sans } from "next/font/google"; -import { Provider } from "react-redux"; +import { Provider, useDispatch } from "react-redux"; import store from "@/store/store"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; const natoSans = Noto_Sans({ subsets: ["latin"], @@ -12,20 +14,33 @@ const natoSans = Noto_Sans({ display: "swap", }); -const App = ({ Component, pageProps }) => { - return ( - - - +function InnerApp({ Component, pageProps }) { + const dispatch = useDispatch(); + const router = useRouter(); + + useEffect(() => { + dispatch({ + type: "ROUTE_CHANGED", + payload: router.pathname, + }); + }, [router.pathname]); - - - + return ( + + + + ); -}; +} + +const App = ({ Component, pageProps }) => ( + + + +); export default appWithTranslation(App); diff --git a/pages/valorant.js b/pages/valorant.js new file mode 100644 index 0000000..e4a563d --- /dev/null +++ b/pages/valorant.js @@ -0,0 +1,25 @@ +import axios from "axios"; +import SearchBar from "@/components/SearchBar/SearchBar"; +import GenericLayout from "@/layout/generic-layout"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import { useTranslation } from "next-i18next"; + +export default function Home() { + return ( + +
+
+
+ +
+
+ ); +} + +export async function getServerSideProps({ locale }) { + return { + props: { + ...(await serverSideTranslations(locale, ["home", "sidebar"])), + }, + }; +} diff --git a/public/images/brand-logo.svg b/public/images/brand-logo.svg new file mode 100644 index 0000000..c4abd01 --- /dev/null +++ b/public/images/brand-logo.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/public/json/platform-api-routes.js b/public/json/platform-api-routes.js index 09449ee..3e1e743 100644 --- a/public/json/platform-api-routes.js +++ b/public/json/platform-api-routes.js @@ -1,4 +1,4 @@ -const PLATFORM_LIST = [ +export const PLATFORM_LIST_LOL = [ { code: "NA1", label: "NA", url: "na1.api.riotgames.com" }, { code: "EUW1", label: "EUW", url: "euw1.api.riotgames.com" }, { code: "EUN1", label: "EUN", url: "eun1.api.riotgames.com" }, @@ -16,4 +16,12 @@ const PLATFORM_LIST = [ { code: "VN2", label: "VN", url: "vn2.api.riotgames.com" }, ]; -export default PLATFORM_LIST; +export const PLATFORM_LIST_VAL = [ + { code: "NA", label: "NA", url: "na.api.riotgames.com" }, + { code: "EU", label: "EU", url: "eu.api.riotgames.com" }, + { code: "LATAM", label: "LATAM", url: "latam.api.riotgames.com" }, + { code: "BR", label: "BR", url: "br.api.riotgames.com" }, + { code: "KR", label: "KR", url: "kr.api.riotgames.com" }, + { code: "AP", label: "AP", url: "ap.api.riotgames.com" }, + { code: "ESPORTS", label: "ESPORTS", url: "esports.api.riotgames.com" }, +]; diff --git a/store/middleware/routeChangeMiddleware .js b/store/middleware/routeChangeMiddleware .js new file mode 100644 index 0000000..5eaf5d2 --- /dev/null +++ b/store/middleware/routeChangeMiddleware .js @@ -0,0 +1,27 @@ +import { + updateGameValue, + updateRegionValue, +} from "@/store/slices/searchBarSlice"; +import { + PLATFORM_LIST_LOL, + PLATFORM_LIST_VAL, +} from "@/public/json/platform-api-routes.js"; +import GAME_LIST from "@/public/json/game-platforms"; + +const routeChangeMiddleware = (store) => (next) => (action) => { + if (action.type === "ROUTE_CHANGED") { + const pathname = action.payload; + + if (pathname.includes("/valorant")) { + store.dispatch(updateRegionValue(PLATFORM_LIST_VAL[0])); + store.dispatch(updateGameValue(GAME_LIST[1])); + } else { + store.dispatch(updateRegionValue(PLATFORM_LIST_LOL[0])); + store.dispatch(updateGameValue(GAME_LIST[0])); + } + } + + return next(action); +}; + +export default routeChangeMiddleware; diff --git a/store/slices/searchBarSlice.js b/store/slices/searchBarSlice.js new file mode 100644 index 0000000..38c06a1 --- /dev/null +++ b/store/slices/searchBarSlice.js @@ -0,0 +1,62 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const searchbarSlice = createSlice({ + name: "searchBar", + initialState: { + region: { + open: false, + value: null, + }, + game: { + open: false, + value: null, + }, + search: { + open: false, + value: null, + }, + }, + reducers: { + updateRegionValue(state, action) { + state.region.value = action.payload; + }, + updateRegionOpen(state, action) { + state.region.open = action.payload; + if (action.payload === true) { + state.game.open = false; + state.search.open = false; + } + }, + updateGameValue(state, action) { + state.game.value = action.payload; + }, + updateGameOpen(state, action) { + state.game.open = action.payload; + if (action.payload === true) { + state.region.open = false; + state.search.open = false; + } + }, + updateSearchValue(state, action) { + state.search.value = action.payload; + }, + updateSearchOpen(state, action) { + state.search.open = action.payload; + if (action.payload === true) { + state.region.open = false; + state.game.open = false; + } + }, + }, +}); + +export const { + updateRegionValue, + updateRegionOpen, + updateGameValue, + updateGameOpen, + updateSearchValue, + updateSearchOpen, +} = searchbarSlice.actions; + +export default searchbarSlice; diff --git a/store/store.js b/store/store.js index f6846ce..e0c9d86 100644 --- a/store/store.js +++ b/store/store.js @@ -1,12 +1,17 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit"; import userSlice from "./slices/userSlice"; +import searchbarSlice from "./slices/searchBarSlice"; +import routeChangeMiddleware from "./middleware/routeChangeMiddleware "; const reducer = combineReducers({ user: userSlice.reducer, + searchBar: searchbarSlice.reducer, }); const store = configureStore({ reducer: reducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(routeChangeMiddleware), }); export default store; diff --git a/styles/_scrollbar.css b/styles/_scrollbar.css new file mode 100644 index 0000000..5a53d7b --- /dev/null +++ b/styles/_scrollbar.css @@ -0,0 +1,42 @@ +/* Total scrollbar */ +scrollbar, +scrollbar * { + width: 10px !important; + height: 10px !important; +} + +/* Track */ +scrollbar-track { + background: #f1f1f1 !important; +} + +/* Handle */ +scrollbar-thumb { + background-color: #888 !important; +} + +/* Handle on hover */ +scrollbar-thumb:hover { + background-color: #555 !important; +} + +/* Total scrollbar */ +::-webkit-scrollbar { + width: 10px; /* width of the entire scrollbar */ + height: 10px; /* height of the horizontal scrollbar */ +} + +/* Track */ +::-webkit-scrollbar-track { + background: #f1f1f1; /* color of the tracking area */ +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #888; /* scrollbar handle color */ +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #555; /* scrollbar handle hover color */ +} diff --git a/styles/_sidebar.css b/styles/_sidebar.css index 1a61f38..5e55609 100644 --- a/styles/_sidebar.css +++ b/styles/_sidebar.css @@ -4,19 +4,9 @@ .sidebar-icon { @apply relative flex items-center justify-center h-12 w-10 sm:w-12 mt-2 mb-2 mx-auto - hover:bg-accent - text-accent hover:text-white - hover:rounded-xl rounded-3xl - transition-all duration-100 ease-linear - cursor-pointer; - } - - .sidebar-icon-main { - @apply relative flex items-center justify-center - h-12 w-10 sm:w-12 mt-2 mb-2 mx-auto - hover:bg-red-500 - text-red-500 hover:text-black - hover:rounded-xl rounded-3xl + hover:bg-secondary + text-white + hover:rounded-xl rounded-xl transition-all duration-100 ease-linear cursor-pointer; } diff --git a/styles/globals.css b/styles/globals.css index 09596c3..62d2260 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -3,6 +3,7 @@ @tailwind utilities; @import "_sidebar.css"; +@import "_scrollbar.css"; @layer base { :root {