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)} + > + {'search'} +
+ + {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 ( + + +
+ setSearchValue(e.target.value)} + onFocus={(e) => e.preventDefault(true)} + autoFocus={true} + /> +
+ {isLoading ? ( + + ) : ( + {'search + )} +
+
+ {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)} + > +
+ {`${player.username}`} +
+
+ {/* {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 ( +
+
{match.name}
+
+ ); + })} +
+
+ )} +
+
+ ); +} 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 @@ + + +