diff --git a/app/actions.ts b/app/actions.ts
index 2e7dac1..cb0c21f 100644
--- a/app/actions.ts
+++ b/app/actions.ts
@@ -568,3 +568,34 @@ export async function paginationParamsToURL(params: {}) {
return url;
}
+
+export async function fetchSearchData(prevState: any, formData: FormData) {
+ const session = await getSession(true);
+
+ if (!session.id) return redirect('/');
+
+ let searchText = formData.get('search');
+
+ let searchData = await fetch(
+ `${process.env.REACT_APP_API_URL}/search?searchKey=${searchText}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': `${process.env.REACT_APP_ORIGIN_URL}`,
+ Authorization: `Bearer ${session.accessToken}`,
+ },
+ }
+ );
+
+ if (!searchData?.ok) {
+ throw new Error('Error from server on search!');
+ }
+
+ searchData = await searchData.json();
+
+ return {
+ status: 'success',
+ search: searchData,
+ };
+}
diff --git a/app/globals.css b/app/globals.css
index 0254628..9e1c292 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -125,6 +125,10 @@
/* TOAST */
--toast-error-bg: var(--yellow-200);
--toast-error-border: 50, 70%, 64%;
+
+ /* SEARCH BAR */
+ --search-bar-background: 0, 0%, 97%;
+ --search-bar-foreground: 0, 0%, 15%;
}
[data-theme='dark'] {
@@ -254,6 +258,10 @@
/* TOAST */
--toast-error-bg: var(--yellow-200);
--toast-error-border: 50, 70%, 64%;
+
+ /* SEARCH BAR */
+ --search-bar-background: 240, 6%, 10%;
+ --search-bar-foreground: 0, 0%, 75%;
}
* {
@@ -272,7 +280,7 @@ body {
}
[data-theme='dark'] ::selection,
-::-moz-selection {
+[data-theme='dark'] ::-moz-selection {
background-color: hsla(var(--accent-color), 0.24);
}
diff --git a/components/NavBar/NavBar.tsx b/components/NavBar/NavBar.tsx
index bb6c21c..f41c700 100644
--- a/components/NavBar/NavBar.tsx
+++ b/components/NavBar/NavBar.tsx
@@ -9,6 +9,7 @@ import HamburgerMobile from './HamburgerMobile/HamburgerMobile';
import ModeSwitcher from './ModeSwitcher/ModeSwitcher';
import styles from './NavBar.module.css';
import Routes from './Routes/Routes';
+import SearchButton from './SearchButton/SearchButton';
import ThemeSwitcher from './ThemeSwitcher/ThemeSwitcher';
import UserLogged from './UserLogged/UserLogged';
@@ -31,6 +32,7 @@ export default function NavBar() {
{/* Donate */}
+
{cookieMode?.value &&
}
diff --git a/components/NavBar/SearchButton/SearchButton.module.css b/components/NavBar/SearchButton/SearchButton.module.css
new file mode 100644
index 0000000..87ca94f
--- /dev/null
+++ b/components/NavBar/SearchButton/SearchButton.module.css
@@ -0,0 +1,15 @@
+.searchButton {
+ position: relative;
+ height: 1.85rem;
+ width: 1.9rem;
+}
+
+.searchButton img {
+ object-fit: contain;
+ cursor: pointer;
+}
+
+[data-theme='dark'] .searchButton img {
+ -webkit-filter: invert(75%);
+ filter: invert(75%);
+}
diff --git a/components/NavBar/SearchButton/SearchButton.tsx b/components/NavBar/SearchButton/SearchButton.tsx
new file mode 100644
index 0000000..eadb2e8
--- /dev/null
+++ b/components/NavBar/SearchButton/SearchButton.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import SearchBar from '@/components/SearchBar/SearchBar';
+import searchIcon from '@/public/icons/search.svg';
+import { AnimatePresence } from 'framer-motion';
+import Image from 'next/image';
+import { useEffect, useState } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import styles from './SearchButton.module.css';
+
+export default function SearchButton() {
+ const [isSeachBarOpen, setIsSeachBarOpen] = useState(false);
+ useHotkeys('ctrl+k', (e) => {
+ e.preventDefault();
+ setIsSeachBarOpen((prev) => !prev);
+ });
+
+ return (
+ <>
+
setIsSeachBarOpen((prev) => !prev)}
+ >
+
+
+
+ {isSeachBarOpen &&
}
+
+ >
+ );
+}
diff --git a/components/SearchBar/SearchBar.module.css b/components/SearchBar/SearchBar.module.css
new file mode 100644
index 0000000..a189cbc
--- /dev/null
+++ b/components/SearchBar/SearchBar.module.css
@@ -0,0 +1,204 @@
+.container {
+ width: 100dvw;
+ height: 100dvh;
+ position: fixed;
+ padding: 9rem 0;
+ inset: 0;
+ background-color: hsla(0, 0%, 0%, 0.8);
+ z-index: 5;
+ overflow: auto;
+}
+
+.body {
+ width: 50vw;
+ height: fit-content;
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ position: relative;
+ margin: auto;
+ gap: 1rem 0;
+}
+
+.bar {
+ position: relative;
+ width: 100%;
+ height: 4rem;
+ border-radius: 0.6rem;
+ border: 0;
+ font-family: inherit;
+ padding: 0.5rem 2rem;
+ font-size: 1.32rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0 1.8rem;
+ background-color: hsla(var(--search-bar-background));
+ color: hsla(var(--search-bar-foreground));
+}
+
+.bar input {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ font-family: inherit;
+ font-size: inherit;
+ background-color: transparent;
+}
+
+.bar input::placeholder {
+ color: #333;
+}
+
+.bar input:focus-visible {
+ outline: 0;
+}
+
+.bar .icon {
+ height: 60%;
+ aspect-ratio: 1;
+ position: relative;
+}
+
+.bar .icon img {
+ object-fit: contain;
+}
+
+.content {
+ width: 100%;
+ height: fit-content;
+ padding: 2rem 1.4rem 1.6rem 1.4rem;
+ background-color: hsla(var(--search-bar-background));
+ color: hsla(var(--search-bar-foreground));
+ border-radius: 0.6rem;
+ border: 0;
+ display: flex;
+ flex-flow: column;
+ gap: 0.6rem 0;
+}
+
+.content .header {
+ font-weight: 700;
+ font-size: 1.52rem;
+ padding: 0 0.6rem;
+ letter-spacing: 0.02rem;
+}
+
+.content .list {
+ display: flex;
+ flex-flow: column;
+ /* gap: 0.4rem 0; */
+ font-size: 1.15rem;
+ font-weight: 400;
+ color: hsla(0, 0%, 53%, 1);
+}
+
+.content .list .item {
+ height: 1.5rem;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.3rem 0.6rem;
+ gap: 0.7rem;
+ cursor: pointer;
+ border-radius: 0px 5rem 5rem 50vh;
+ color: inherit;
+ box-sizing: content-box;
+ transition: all 0.2s ease-out;
+}
+
+.content .list .item:hover {
+ background: linear-gradient(90deg, transparent 0%, hsla(0, 0%, 0%, 0.03) 20%);
+}
+
+[data-theme='dark'] .content .list .item:hover {
+ background: linear-gradient(
+ 90deg,
+ transparent 0%,
+ hsla(0, 0%, 100%, 0.05) 18%
+ );
+}
+
+.content .list .item .name {
+ /* width: 100%; */
+ display: inline-flex;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ /* mask: linear-gradient(90deg, hsla(0, 0%, 100%) 95%, transparent 100%); */
+}
+
+.content .list .item span {
+ color: hsla(var(--accent-color));
+}
+
+.content .list .item .secondaryInfo {
+ display: inline-flex;
+ gap: 0 1.3rem;
+ margin-left: auto;
+ color: hsla(0, 0%, 69.5%, 1);
+ align-items: center;
+}
+
+[data-theme='dark'] .content .list .item .secondaryInfo {
+ color: hsla(0, 0%, 75%, 0.32);
+}
+
+/* .content .list .item :is(.rank, .rating) {
+ color: hsla(0, 0%, 69.5%, 1);
+}
+
+[data-theme='dark'] .content .list .item :is(.rank, .rating) {
+ color: hsla(0, 0%, 75%, 0.32);
+} */
+
+.list .item .propic {
+ height: 1.5rem;
+ width: 1.5rem;
+ position: relative;
+ border-radius: 50vh;
+ overflow: hidden;
+}
+
+.bar .icon span[aria-saving='true'] {
+ height: 100%;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ background: radial-gradient(farthest-side, #444 94%, #4440) top/4px 4px
+ no-repeat,
+ conic-gradient(#4440 30%, #444);
+ -webkit-mask: radial-gradient(farthest-side, #4440 calc(100% - 4px), #444 0);
+ animation: l13 1s infinite linear;
+ margin: auto;
+ display: inline-block;
+ margin-top: 2.5px;
+}
+
+[data-theme='dark'] .bar .icon span[aria-saving='true'] {
+ background: radial-gradient(farthest-side, #ddd 94%, #ddd0) top/4px 4px
+ no-repeat,
+ conic-gradient(#ddd0 30%, #ddd);
+ -webkit-mask: radial-gradient(farthest-side, #ddd0 calc(100% - 4px), #ddd 0);
+ animation: l13 1s infinite linear;
+}
+
+@keyframes l13 {
+ 100% {
+ transform: rotate(1turn);
+ }
+}
+
+[data-theme='dark'] .content .list {
+ color: hsla(0, 0%, 58%, 1);
+}
+
+[data-theme='dark'] .content .list .item span {
+ color: hsla(var(--accent-secondary-color));
+}
+
+[data-theme='dark'] .bar input::placeholder {
+ color: #999;
+}
+
+[data-theme='dark'] .bar .icon img {
+ -webkit-filter: invert(80%);
+ filter: invert(80%);
+}
diff --git a/components/SearchBar/SearchBar.tsx b/components/SearchBar/SearchBar.tsx
new file mode 100644
index 0000000..8d4366a
--- /dev/null
+++ b/components/SearchBar/SearchBar.tsx
@@ -0,0 +1,284 @@
+'use client';
+
+import { fetchSearchData } from '@/app/actions';
+import searchIcon from '@/public/icons/search.svg';
+import { useClickAway } from '@uidotdev/usehooks';
+import { AnimatePresence, motion, stagger } from 'framer-motion';
+import Image from 'next/image';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
+import { useFormState } from 'react-dom';
+import styles from './SearchBar.module.css';
+
+const initialState = {
+ search: undefined,
+};
+
+const containerMotionStates = {
+ initial: {
+ backgroundColor: 'hsla(0, 0%, 0%, 0)',
+ transition: {
+ delay: 0.15,
+ },
+ },
+ animate: {
+ backgroundColor: 'hsla(0, 0%, 0%, 0.8)',
+ },
+};
+
+const bodyMotionStates = {
+ initial: {
+ opacity: 0,
+ y: -20,
+ transition: {
+ duration: 0.15,
+ },
+ },
+ animate: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ delay: 0.1,
+ duration: 0.2,
+ },
+ },
+};
+
+const bodyContentMotionStates = {
+ initial: {
+ opacity: 0,
+ x: -10,
+ },
+ animate: (index: number) => ({
+ opacity: 1,
+ x: 0,
+ transition: {
+ duration: 0.3,
+ delay: 0.15 * index,
+ ease: 'easeOut',
+ },
+ }),
+ exit: {
+ opacity: 0,
+ x: -10,
+ transition: {
+ duration: 0.3,
+ delay: 0.15,
+ ease: 'easeOut',
+ },
+ },
+};
+
+const mode: { [key: number]: { image: any; alt: string } } = {
+ 0: 'std',
+ 1: 'taiko',
+ 2: 'ctb',
+ 3: 'mania',
+};
+
+export default function SearchBar({ setIsSeachBarOpen }) {
+ const [searchValue, setSearchValue] = useState('');
+ const [state, formAction] = useFormState(fetchSearchData, initialState);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const ref = useClickAway(() => {
+ setIsSeachBarOpen(false);
+ });
+
+ useEffect(() => {
+ if (searchValue.length < 3) {
+ setIsLoading(false);
+ return;
+ }
+
+ setIsLoading(true);
+ let timeout = setTimeout(() => {
+ let formData = new FormData();
+ formData.append('search', searchValue);
+ formAction(formData);
+ }, 1000);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [searchValue]);
+
+ useEffect(() => {
+ if (state.status !== 'success') return;
+
+ setIsLoading(false);
+ }, [state]);
+
+ return (
+
+
+
+ {state?.search?.players.length > 0 && (
+
+ Players
+
+ {state?.search?.players.slice(0, 12).map((player) => {
+ /* const selectedText = searchValue;
+
+ let indexesUsername = [
+ player.text.indexOf(searchValue),
+ player.text.lastIndexOf(searchValue),
+ ];
+
+ const regEscape = (v) =>
+ v.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+
+ let username = player.text.split(
+ new RegExp(regEscape(searchValue), 'ig')
+ ); */
+
+ return (
+
setIsSeachBarOpen(false)}
+ >
+
+
+
+
+ {/* {username.length > 1 && (
+ <>
+
{username[0]}
+
{selectedText}
+
{username[1]}
+ >
+ )}
+ {username.length < 2 && indexesUsername[0] === 0 && (
+ <>
+
+ {/[A-Z]/.test(player.text[0])
+ ? selectedText.text.charAt(0).toUpperCase() +
+ selectedText.text.slice(1)
+ : selectedText}
+
+
{username[0]}
+ >
+ )}
+ {username.length < 2 && indexesUsername[0] !== 0 && (
+ <>
+
{username[0]}
+
{selectedText}
+ >
+ )} */}
+ {player.username}
+
+
+ {player.globalRank && (
+
+ #
+ {Intl.NumberFormat('us-US').format(player.globalRank)}
+
+ )}
+ {player.rating && (
+
+ {player.rating.toFixed(0)} TR
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+ {state?.search?.tournaments.length > 0 && (
+
+ Tournaments
+
+ {state?.search?.tournaments.slice(0, 12).map((tournament) => {
+ return (
+
+
{tournament.name}
+
+ {/*
{}
*/}
+
+ {tournament.teamSize}v{tournament.teamSize}
+
+
+ {mode[tournament.ruleset]}
+
+
+
+ );
+ })}
+
+
+ )}
+ {state?.search?.matches.length > 0 && (
+
+ Matches
+
+ {state?.search?.matches.slice(0, 12).map((match) => {
+ return (
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index 9eceece..5e6bcf4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
+ "@uidotdev/usehooks": "^2.4.1",
"chart.js": "^4.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"clsx": "^2.0.0",
@@ -21,6 +22,7 @@
"react": "^18",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18",
+ "react-hotkeys-hook": "^4.5.0",
"react-range": "^1.8.14",
"react-tooltip": "^5.26.3",
"react-wrap-balancer": "^1.1.0",
@@ -679,6 +681,18 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@uidotdev/usehooks": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
+ "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -3331,6 +3345,15 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hotkeys-hook": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz",
+ "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==",
+ "peerDependencies": {
+ "react": ">=16.8.1",
+ "react-dom": ">=16.8.1"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/package.json b/package.json
index 2455622..557cd2c 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
+ "@uidotdev/usehooks": "^2.4.1",
"chart.js": "^4.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"clsx": "^2.0.0",
@@ -22,6 +23,7 @@
"react": "^18",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18",
+ "react-hotkeys-hook": "^4.5.0",
"react-range": "^1.8.14",
"react-tooltip": "^5.26.3",
"react-wrap-balancer": "^1.1.0",
diff --git a/public/icons/search.svg b/public/icons/search.svg
new file mode 100644
index 0000000..efb649c
--- /dev/null
+++ b/public/icons/search.svg
@@ -0,0 +1,3 @@
+