diff --git a/app/actions.ts b/app/actions.ts index 99e77a1..cb0c21f 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -25,13 +25,13 @@ export async function getSession(onlyData: boolean = false) { return { isLogged: session?.isLogged, id: session?.id, - userId: session?.userId, + playerId: session?.playerId, osuId: session?.osuId, osuCountry: session?.osuCountry, osuPlayMode: session?.osuPlayMode, osuPlayModeSelected: session?.osuPlayModeSelected, username: session?.username, - roles: session?.roles, + scopes: session?.scopes, accessToken: session?.accessToken, }; @@ -40,7 +40,7 @@ export async function getSession(onlyData: boolean = false) { export async function login(cookie: { accessToken: string; - refreshToken: string; + refreshToken?: string; accessExpiration?: number; }) { const session = await getSession(); @@ -56,12 +56,15 @@ export async function login(cookie: { session.accessToken = cookie.accessToken; - await cookies().set('OTR-Refresh-Token', cookie.refreshToken, { - httpOnly: true, - path: '/', - sameSite: 'strict', - secure: process.env.NODE_ENV === 'production', - }); + if (cookie?.refreshToken) { + await cookies().set('OTR-Refresh-Token', cookie.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: process.env.NODE_ENV === 'production', + maxAge: 1209600, + }); + } const loggedUser = await getLoggedUser(cookie.accessToken); @@ -72,21 +75,27 @@ export async function login(cookie: { } session.id = loggedUser.id; - session.userId = loggedUser.userId; + session.playerId = loggedUser.playerId; session.osuId = loggedUser.osuId; session.osuCountry = loggedUser.osuCountry; session.osuPlayMode = loggedUser.osuPlayMode; - session.osuPlayModeSelected = loggedUser.osuPlayMode; - session.username = loggedUser.username; - session.roles = loggedUser.roles; + session.osuPlayModeSelected = loggedUser.osuPlayMode; // maybe to delete + session.username = loggedUser.osuUsername; + session.scopes = loggedUser.scopes; session.isLogged = true; + await cookies().set('OTR-user-selected-osu-mode', loggedUser.osuPlayMode, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: process.env.NODE_ENV === 'production', + maxAge: 1209600, + }); + await session.save(); /* await changeOsuModeCookie(res.osuPlayMode); */ - return NextResponse.redirect( - new URL('/', process.env.REACT_APP_ORIGIN_URL) - ); + return NextResponse.redirect(new URL('/', process.env.REACT_APP_ORIGIN_URL)); } export async function getLoggedUser(accessToken: string) { @@ -169,7 +178,7 @@ export async function saveTournamentMatches( const session = await getSession(true); /* IF USER IS UNAUTHORIZED REDIRECT TO HOMEPAGE */ - if (!session.userId) return redirect('/'); + if (!session.id) return redirect('/'); try { /* REGEX TO REMOVE ALL SPACES AND ENTERS */ @@ -199,15 +208,15 @@ export async function saveTournamentMatches( rankRangeLowerBound: parseInt(formData.get('rankRestriction')), teamSize: parseInt(formData.get('teamSize')), mode: parseInt(formData.get('gameMode')), - submitterId: session?.userId ?? 0, + submitterId: session?.id ?? 0, ids: matchIDs, }); let isSubmissionVerified = formData.get('verifierCheckBox') == 'on' ?? false; - await fetch( - `${process.env.REACT_APP_API_URL}/matches/batch?verified=${isSubmissionVerified}`, + let tournamentSubmit = await fetch( + `${process.env.REACT_APP_API_URL}/tournaments?verify=${isSubmissionVerified}`, { method: 'POST', headers: { @@ -218,32 +227,26 @@ export async function saveTournamentMatches( credentials: 'include', body: JSON.stringify(data), } - ) - .then((response) => { - if (response.status !== 200) { - throw new Error({ - issues: [ - { - path: ['serverError'], - message: response.body, - }, - ], - }); - } + ); - return { - status: 'success', - }; - }) - .then((data) => { - console.log(data); - }) - .catch((error) => { - console.log(JSON.parse(error.message)); - }); + if (!tournamentSubmit?.ok) { + const errorMessage = await tournamentSubmit.text(); + + return { + error: { + status: tournamentSubmit.status, + text: tournamentSubmit.statusText, + message: errorMessage, + }, + }; + } return { - status: 'success', + success: { + status: tournamentSubmit.status, + text: tournamentSubmit.statusText, + message: 'Tournament submitted successfully', + }, }; } catch (error) { let errors = {}; @@ -292,7 +295,7 @@ export async function applyLeaderboardFilters(params: {}) { let string = ''; params[key].forEach((value, index) => { - string += `${value}${index === 0 ? `&${key}=` : ''}`; + string += `${value}${index < params[key].length - 1 ? `&${key}=` : ''}`; }); return (urlStringObject[key] = string); @@ -312,8 +315,7 @@ export async function fetchLeaderboard(params: {}) { /* MISSING MODE, PLAYERID */ - const { type, page, rank, rating, matches, winrate, inclTier, exclTier } = - params; + const { type, page, rank, rating, matches, winrate, tiers } = params; const tierFilters = { bronze: 'bronze', @@ -367,16 +369,10 @@ export async function fetchLeaderboard(params: {}) { .sort(compareNumbers)) : undefined; - inclTier - ? Array.isArray(inclTier) - ? (paramsToProcess.inclTier = inclTier) - : (paramsToProcess.inclTier = Array(inclTier)) - : undefined; - - exclTier - ? Array.isArray(exclTier) - ? (paramsToProcess.exclTier = exclTier) - : (paramsToProcess.exclTier = Array(exclTier)) + tiers + ? Array.isArray(tiers) + ? (paramsToProcess.tiers = tiers) + : (paramsToProcess.tiers = Array(tiers)) : undefined; const queryCheck = await LeaderboardsQuerySchema.safeParse({ @@ -451,19 +447,12 @@ export async function fetchLeaderboard(params: {}) { } /* Check included tiers filter */ - if (queryCheck.data.inclTier) { - queryCheck.data.inclTier.forEach((tier) => { + if (queryCheck.data.tiers) { + queryCheck.data.tiers.forEach((tier) => { backendObject[tierFilters[tier]] = true; }); } - /* Check included tiers filter */ - if (queryCheck.data.exclTier) { - queryCheck.data.exclTier.forEach((tier) => { - backendObject[tierFilters[tier]] = false; - }); - } - let backendString = new URLSearchParams(backendObject).toString(); let data = await fetch( @@ -528,7 +517,7 @@ export async function fetchUserPage(player: string | number) { let res = await fetch( `${process.env.REACT_APP_API_URL}/stats/${player}${ - session?.userId ? `?comparerId=${session?.userId}` : '' + session?.playerId ? `?comparerId=${session?.playerId}` : '' }`, { headers: { @@ -563,7 +552,9 @@ export async function paginationParamsToURL(params: {}) { let string = `${index !== 0 ? '&' : ''}${key}=`; params[key].forEach((value, index) => { - string += `${value}${index === 0 ? `&${key}=` : ''}`; + string += `${value}${ + index < params[key].length - 1 ? `&${key}=` : '' + }`; }); return (url += `${string}`); @@ -577,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/auth/route.ts b/app/auth/route.ts index 55d3fab..d82dfc3 100644 --- a/app/auth/route.ts +++ b/app/auth/route.ts @@ -7,8 +7,9 @@ export async function GET(request: Request) { const refreshToken = searchParams.get('refreshToken'); const accessToken = searchParams.get('accessToken'); + // Refresh session if refreshToken is set if (refreshToken && accessToken) { - return await login({ accessToken, refreshToken }); + return await login({ accessToken }); } if (code) { diff --git a/app/dashboard/error.module.css b/app/dashboard/error.module.css new file mode 100644 index 0000000..77c2c46 --- /dev/null +++ b/app/dashboard/error.module.css @@ -0,0 +1,47 @@ +.errorDiv { + position: relative; + min-height: 60dvh; + height: 70dvh; + max-height: 70dvh; + margin: var(--main-padding); + margin-top: 0; +} + +.errorDiv .content { + height: 100%; + width: 100%; + position: absolute; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + padding: var(--internal-gap); + gap: var(--internal-gap); + z-index: 2; +} + +.errorDiv img { + z-index: 1; +} + +.errorDiv h1 { + font-size: 4rem; +} + +.errorDiv span { + font-size: 3.5rem; + letter-spacing: -2%; + text-align: center; + max-width: 40vw; +} + +.errorDiv button { + margin-top: 1.3rem; + padding: 1rem 3rem; + width: fit-content; + font-weight: 500; + cursor: pointer; + border-radius: 0.4rem; + font-size: 1rem; + transition: background-color 0.2s ease-out; +} diff --git a/app/dashboard/error.tsx b/app/dashboard/error.tsx new file mode 100644 index 0000000..93162b9 --- /dev/null +++ b/app/dashboard/error.tsx @@ -0,0 +1,57 @@ +'use client'; + +import backgroundError from '@/public/images/error-background.svg'; +import Image from 'next/image'; +import { useEffect } from 'react'; +import Balancer from 'react-wrap-balancer'; +import styles from './error.module.css'; + +const errors = { + '4': { + title: 'No data', + message: "You don't have any data for the selected ruleset", + reloadBtn: true, + }, + '404': { + title: '404', + message: "We don't have that page", + reloadBtn: false, + }, +}; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error(error); + }, [error]); + + let errorContent = !isNaN(error.message) + ? errors[error.message] + : errors['404']; + + return ( +
+ Error background +
+

{errorContent.title}

+ {errorContent.message} + {errorContent.reloadBtn && ( + + )} +
+
+ ); +} diff --git a/app/dashboard/page.module.css b/app/dashboard/page.module.css index d961a3d..668a0bf 100644 --- a/app/dashboard/page.module.css +++ b/app/dashboard/page.module.css @@ -17,7 +17,7 @@ .graphContainer { display: flex; flex-flow: column; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: var(--main-padding); gap: 1.2rem; diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 702ff82..1b90040 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -29,45 +29,20 @@ export default async function page({ }) { const data = await fetchDashboard(); + if (!data.generalStats || !data.playerInfo) { + throw Error('4'); + return; + } + return (
- {/*
- - Your TR (tournament rating) is calculated based on your match cost - relative to other players in your matches, see{' '} - - here - {' '} - for a more detailed explanation. If you notice that someone's - rating is significantly higher than others of similar skill level, - that means they are consistently outperforming in tournaments and - should participate against higher-rated opponents in more challenging - settings in order to have a more accurate rating. - - - Missing tournament data? Don't worry! We are aggressively adding - tournament data, so check back later. -
- Help us populate your data first by submitting your matches{' '} - - here! - -
- -
*/}
- {Math.round(data.generalStats.rating)} + {Math.round(data.generalStats?.rating)} - {data.matchStats.ratingGained.toFixed(0) !== 0 && - data.matchStats.ratingGained.toFixed(0)} + {data.matchStats.ratingGained?.toFixed(0) !== 0 && + data.matchStats.ratingGained?.toFixed(0)}
Highest rating
- {data.matchStats.highestRating.toFixed(0)} + {data.matchStats.highestRating?.toFixed(0)}
@@ -115,13 +90,13 @@ export default async function page({
Average opponent rating - {data?.matchStats.averageOpponentRating.toFixed(0)} + {data?.matchStats.averageOpponentRating?.toFixed(0)}
Average teammate rating - {data?.matchStats.averageTeammateRating.toFixed(0)} + {data?.matchStats.averageTeammateRating?.toFixed(0)}
@@ -143,19 +118,19 @@ export default async function page({
Average misses - {data?.matchStats.matchAverageMissesAggregate.toFixed(0)} + {data?.matchStats.matchAverageMissesAggregate?.toFixed(0)}
Average accuracy - {data?.matchStats.matchAverageAccuracyAggregate.toFixed(2)}% + {data?.matchStats.matchAverageAccuracyAggregate?.toFixed(2)}%
Average maps played - {data?.matchStats.averageGamesPlayedAggregate.toFixed(0)} + {data?.matchStats.averageGamesPlayedAggregate?.toFixed(0)}
diff --git a/app/globals.css b/app/globals.css index c0a4747..9e1c292 100644 --- a/app/globals.css +++ b/app/globals.css @@ -13,7 +13,11 @@ --main-borderRadius: 0.75rem; --background-hsl: 0, 0%, 100%; + --background-content-hsl: 220, 14%, 96%; + --background-content-childs-hsl: 220, 13%, 91%; --foreground-hsl: 0, 0%, 6%; + --foreground-inactive-hsl: 0, 0%, 6%; + --foreground-active-hsl: 0, 0%, 6%; --color-grey: 0, 0%, 6%; @@ -121,6 +125,143 @@ /* 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'] { + --max-width: 1100px; + --border-radius: 12px; + --font-families: var(--font-Inter), ui-monospace, Menlo, Monaco, + 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', + 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', + 'Courier New', monospace; + + /* --main-padding: 2.5rem; + --internal-gap: 0.8rem; */ + --main-padding: 2vw; /* fluid design */ + --internal-gap: 1vw; /* fluid design */ + --main-borderRadius: 0.75rem; + + --color-grey: 0, 0%, 6%; + + /* COLORS */ + + --gray-950: 210, 20%, 98%; + --gray-900: 220, 14%, 96%; + --gray-800: 220, 13%, 91%; + --gray-700: 216, 12%, 84%; + --gray-600: 218, 11%, 65%; + --gray-500: 220, 9%, 46%; + --gray-400: 215, 14%, 34%; + --gray-300: 217, 19%, 27%; + --gray-200: 215, 28%, 17%; + --gray-100: 221, 39%, 11%; + --gray-50: 224, 71%, 4%; + + --blue-50: 214, 100%, 97%; + --blue-100: 214, 95%, 93%; + --blue-200: 213, 97%, 87%; + --blue-300: 212, 96%, 78%; + --blue-400: 213, 94%, 68%; + --blue-500: 217, 91%, 60%; + --blue-600: 221, 83%, 53%; + --blue-700: 224, 76%, 48%; + --blue-800: 226, 71%, 40%; + --blue-900: 224, 64%, 33%; + --blue-950: 226, 57%, 21%; + + --yellow-50: 55, 92%, 95%; + --yellow-100: 55, 97%, 88%; + --yellow-200: 53, 98%, 77%; + --yellow-300: 50, 98%, 64%; + --yellow-400: 48, 96%, 53%; + --yellow-500: 45, 93%, 47%; + --yellow-600: 41, 96%, 40%; + --yellow-700: 35, 92%, 33%; + --yellow-800: 32, 81%, 29%; + --yellow-900: 28, 73%, 26%; + --yellow-950: 26, 83%, 14%; + + --red-50: 0, 86%, 97%; + --red-100: 0, 93%, 94%; + --red-200: 0, 96%, 89%; + --red-300: 0, 94%, 82%; + --red-400: 0, 91%, 71%; + --red-500: 0, 84%, 60%; + --red-600: 0, 72%, 51%; + --red-700: 0, 74%, 42%; + --red-800: 0, 70%, 35%; + --red-900: 0, 63%, 31%; + --red-950: 0, 75%, 15%; + + --green-50: 138, 76%, 97%; + --green-100: 141, 84%, 93%; + --green-200: 141, 79%, 85%; + --green-300: 142, 77%, 73%; + --green-400: 142, 69%, 58%; + --green-500: 142, 71%, 45%; + --green-600: 142, 76%, 36%; + --green-700: 142, 72%, 29%; + --green-800: 143, 64%, 24%; + --green-900: 144, 61%, 20%; + --green-950: 145, 80%, 10%; + + --background-hsl: 240, 10%, 6%; + --background-content-hsl: 240, 6%, 10%; + --background-content-childs-hsl: 240, 5%, 14%; + --foreground-hsl: 0, 0%, 85%; + --foreground-inactive-hsl: 0, 0%, 70%; + --foreground-active-hsl: 0, 0%, 90%; + + --mods-NM-bg: 226, 99%, 65%; + --mods-NM-bg-row: 226, 99%, 65%, 0.15; + --mods-HD-bg: 43, 98%, 60%; + --mods-HD-bg-row: 43, 98%, 60%, 0.15; + --mods-HR-bg: 0, 100%, 69%; + --mods-HR-bg-row: 0, 100%, 69%, 0.15; + --mods-HDHR-bg: 13, 98%, 60%; + --mods-HDHR-bg-row: 13, 98%, 60%, 0.15; + --mods-HDDT-bg: 302, 97%, 86%; + --mods-HDDT-bg-row: 302, 97%, 86%, 0.15; + --mods-EZ-bg: 120, 100%, 50%; + --mods-EZ-bg-row: 120, 100%, 50%, 0.15; + --mods-FL-bg: 0, 0%, 8%; + --mods-FL-bg-row: 0, 0%, 8%, 0.15; + --mods-HT-bg: 6, 34%, 52%; + --mods-HT-bg-row: 6, 34%, 52%, 0.15; + --mods-DT-bg: 267, 97%, 73%; + --mods-DT-bg-row: 267, 97%, 73%, 0.15; + --mods-FM-bg: 101, 38%, 63%; + --mods-FM-bg-row: 101, 38%, 63%, 0.15; + --mods-TB-bg: 191, 26%, 57%; + --mods-TB-bg-row: 191, 26%, 57%, 0.15; + + --accent-color: 216, 100%, 65%; + --accent-secondary-color: var(--blue-400); + + /* CHARTS */ + --tooltip-color: 0, 0%, 100%; + --tooltip-gain: var(--green-400); + --tooltip-loss: var(--red-400); + + /* BADGES */ + --badge-provisional-background: var(--yellow-300); + --badge-provisional-foreground: var(--yellow-900); + + /* USER RATING TIER */ + --tier-bar-background: 240, 5%, 21%; + --tier-bar-accent: var(--accent-color); + + /* 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%; } * { @@ -138,8 +279,13 @@ body { scroll-behavior: smooth !important; } +[data-theme='dark'] ::selection, +[data-theme='dark'] ::-moz-selection { + background-color: hsla(var(--accent-color), 0.24); +} + body { - background-color: hsl(var(--background-rgb)); + background-color: hsl(var(--background-hsl)); color: hsl(var(--foreground-hsl)); font-family: var(--font-families); min-height: 100vh; diff --git a/app/layout.tsx b/app/layout.tsx index 6732abc..1b3ca67 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import ErrorProvider from '@/util/ErrorContext'; import UserProvider from '@/util/UserLoggedContext'; import type { Metadata } from 'next'; import { Viewport } from 'next'; +import { ThemeProvider } from 'next-themes'; import { Inter } from 'next/font/google'; import './globals.css'; @@ -31,16 +32,18 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + - - - - {children} -
- - - + + + + + {children} +
+ + + + ); diff --git a/app/leaderboards/page.tsx b/app/leaderboards/page.tsx index ea21fa0..1b355bd 100644 --- a/app/leaderboards/page.tsx +++ b/app/leaderboards/page.tsx @@ -13,27 +13,27 @@ export const metadata: Metadata = { title: 'Leaderboards', }; -/* const catchErrors = async (params: {}) => { - const { type, rank, rating, matches, winrate, inclTier, exclTier } = params; +const catchErrors = async (params: {}, leaderboard: any) => { + /* const { type, rank, rating, matches, winrate, inclTier, exclTier } = params; */ - let leaderboardTypes = ['global', 'country', 'friends']; + if (leaderboard === undefined) { + return redirect('/leaderboards'); + } - if (!type || (type?.length > 0 && leaderboardTypes.includes(type))) return; - - return redirect('/leaderboards'); -}; */ + return; +}; export default async function page({ searchParams, }: { searchParams: URLSearchParams; }) { - /* await catchErrors(searchParams); */ const leaderboardData = await fetchLeaderboard(searchParams); + await catchErrors(searchParams, leaderboardData); return (
- {leaderboardData.playerChart && ( + {leaderboardData?.playerChart && ( )}
diff --git a/app/page.module.css b/app/page.module.css index 8eeabe2..58f633d 100644 --- a/app/page.module.css +++ b/app/page.module.css @@ -9,7 +9,7 @@ .row { width: 100%; height: 100%; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); display: flex; flex-flow: row; @@ -37,7 +37,7 @@ display: flex; flex-flow: column; justify-content: space-between; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: var(--main-padding); text-align: left; @@ -49,18 +49,6 @@ justify-content: center; } -.box .logo { - width: 100%; - height: 100%; - position: relative; -} - -.box img { - object-fit: contain; - aspect-ratio: 221/101; - padding: 2rem; -} - .row .info { display: flex; flex-flow: column; diff --git a/app/page.tsx b/app/page.tsx index bb6878d..8b2116e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,14 +1,16 @@ import LoginButton from '@/components/Button/LoginButton'; +import Logo from '@/components/Homepage/Logo/Logo'; import decoration1 from '@/public/decorations/decoration-1.svg'; import decoration2 from '@/public/decorations/decoration-2.svg'; import decoration3 from '@/public/decorations/decoration-3.svg'; import decoration4 from '@/public/decorations/decoration-4.svg'; +import fullLogoDark from '@/public/logos/full-logo-dark.svg'; import fullLogo from '@/public/logos/full-logo.svg'; import Image from 'next/image'; import Balancer from 'react-wrap-balancer'; import styles from './page.module.css'; -export default async function Home() { +export default function Home() { return (
@@ -19,9 +21,7 @@ export default async function Home() {
-
- {'o!TR -
+
diff --git a/app/submit/page.tsx b/app/submit/page.tsx index c7d988b..4604b00 100644 --- a/app/submit/page.tsx +++ b/app/submit/page.tsx @@ -10,12 +10,12 @@ export const metadata: Metadata = { }; export default async function page() { - const { roles } = await getSession(true); + const { scopes } = await getSession(true); return (
- +
); } diff --git a/app/users/[id]/page.module.css b/app/users/[id]/page.module.css index d961a3d..668a0bf 100644 --- a/app/users/[id]/page.module.css +++ b/app/users/[id]/page.module.css @@ -17,7 +17,7 @@ .graphContainer { display: flex; flex-flow: column; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: var(--main-padding); gap: 1.2rem; diff --git a/components/Badges/Provisional/provisionalBadge.module.css b/components/Badges/Provisional/provisionalBadge.module.css index 4fe4d13..20b2ef7 100644 --- a/components/Badges/Provisional/provisionalBadge.module.css +++ b/components/Badges/Provisional/provisionalBadge.module.css @@ -39,6 +39,10 @@ font-weight: 600; } +[data-theme='dark'] .badge:after { + background-color: hsla(var(--badge-provisional-background), 0.9); +} + .badge:hover::after { visibility: visible; opacity: 1; diff --git a/components/Button/FilterChangeButton/FilterChangeButton.module.css b/components/Button/FilterChangeButton/FilterChangeButton.module.css index 37e80a5..1f44884 100644 --- a/components/Button/FilterChangeButton/FilterChangeButton.module.css +++ b/components/Button/FilterChangeButton/FilterChangeButton.module.css @@ -8,7 +8,7 @@ } .button.selected { - background-color: hsla(var(--blue-300)); + background-color: hsla(var(--accent-secondary-color)); color: #ffffff; font-weight: 600; } diff --git a/components/Card/Card.module.css b/components/Card/Card.module.css index c3b7163..875d530 100644 --- a/components/Card/Card.module.css +++ b/components/Card/Card.module.css @@ -1,6 +1,6 @@ .card { width: 100%; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: 1.5rem 2rem; display: flex; diff --git a/components/Charts/AreaChart/AreaChart.module.css b/components/Charts/AreaChart/AreaChart.module.css index 8e46bb9..c5aefcb 100644 --- a/components/Charts/AreaChart/AreaChart.module.css +++ b/components/Charts/AreaChart/AreaChart.module.css @@ -1,7 +1,6 @@ .graphContainer { width: 100%; height: 100%; - /* background-color: white; */ border-radius: var(--main-borderRadius); } @@ -18,6 +17,10 @@ min-width: 10em; } +[data-theme='dark'] .tooltip { + background: hsla(var(--background-content-childs-hsl), 0.85); +} + .tooltip .header { width: 100%; display: flex; diff --git a/components/Charts/AreaChart/AreaChart.tsx b/components/Charts/AreaChart/AreaChart.tsx index fe44691..0757657 100644 --- a/components/Charts/AreaChart/AreaChart.tsx +++ b/components/Charts/AreaChart/AreaChart.tsx @@ -13,6 +13,7 @@ import { Tooltip, } from 'chart.js'; import clsx from 'clsx'; +import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; import { Line } from 'react-chartjs-2'; import styles from './AreaChart.module.css'; @@ -69,6 +70,8 @@ export default function AreaChart({ const [font, setFont] = useState(''); + const { theme } = useTheme(); + /* get variables of colors from CSS */ useEffect(() => { setColors([ @@ -148,7 +151,10 @@ export default function AreaChart({ label: '', data: dataForGraph, borderWidth: 3, - borderColor: `hsla(${colors[0]}, 0.6)`, + borderColor: + theme === 'light' + ? `hsla(${colors[0]}, 0.6)` + : `hsla(${colors[0]}, 0.82)`, backgroundColor: 'transparent', font: font, }, @@ -267,7 +273,8 @@ export default function AreaChart({ point: { radius: 0 /* 0 makes points hidden */, hitRadius: 100, - pointBackgroundColor: `hsla(${colors[0]}, 0.6)`, + pointBackgroundColor: + theme === 'light' ? `hsla(${colors[0]}, 0.6)` : `hsla(${colors[0]})`, }, }, maintainAspectRatio: false, @@ -288,17 +295,35 @@ export default function AreaChart({ size: 16, family: font, }, + color: theme === 'dark' ? '#999' : '#707070', autoSkip: true, maxTicksLimit: 7, major: { enabled: true }, }, + grid: { + color: + theme === 'dark' ? 'rgba(250,250,250,0.028)' : 'rgba(0,0,0,0.08)', + }, + border: { + color: + theme === 'dark' ? 'rgba(250,250,250,0.040)' : 'rgba(0,0,0,0.08)', + }, }, y: { + border: { + color: + theme === 'dark' ? 'rgba(250,250,250,0.040)' : 'rgba(0,0,0,0.08)', + }, + grid: { + color: + theme === 'dark' ? 'rgba(250,250,250,0.028)' : 'rgba(0,0,0,0.08)', + }, ticks: { font: { size: 16, family: font, }, + color: theme === 'dark' ? '#999' : '#707070', autoSkip: true, maxTicksLimit: 6, precision: 0, diff --git a/components/Charts/InlineChart/InlineChart.module.css b/components/Charts/InlineChart/InlineChart.module.css index 60f4116..1c0894a 100644 --- a/components/Charts/InlineChart/InlineChart.module.css +++ b/components/Charts/InlineChart/InlineChart.module.css @@ -44,3 +44,7 @@ position: absolute; bottom: -1.6rem; } + +[data-theme='dark'] .segment .percentile { + color: hsl(0, 0%, 6%); +} diff --git a/components/Charts/RadarChart/RadarChart.tsx b/components/Charts/RadarChart/RadarChart.tsx index 4f2c762..1d457e9 100644 --- a/components/Charts/RadarChart/RadarChart.tsx +++ b/components/Charts/RadarChart/RadarChart.tsx @@ -9,6 +9,7 @@ import { RadialLinearScale, Tooltip, } from 'chart.js'; +import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; import { Radar } from 'react-chartjs-2'; import styles from './RadarChart.module.css'; @@ -37,6 +38,8 @@ export default function RadarChart({ winrateModData?: any; averageModScore?: any; }) { + const { theme } = useTheme(); + const [colors, setColors] = useState([]); const [font, setFont] = useState(''); @@ -170,8 +173,14 @@ export default function RadarChart({ family: font, weight: 600, }, + color: theme === 'dark' ? 'rgba(250,250,250,0.8)' : '#656565', + }, + grid: { + color: + theme === 'dark' ? 'rgba(250,250,250,0.028)' : 'rgba(0,0,0,0.08)', }, - backgroundColor: 'rgb(250,250,250)', + backgroundColor: + theme === 'light' ? 'rgb(250,250,250)' : 'rgba(0,0,0,0.05)', beginAtZero: false, angleLines: { borderDash: (context: any) => { @@ -180,6 +189,8 @@ export default function RadarChart({ const spaceInPx = space / ticksLength; return [0, 0, 0, spaceInPx, 2500]; }, + color: + theme === 'dark' ? 'rgba(250,250,250,0.028)' : 'rgba(0,0,0,0.08)', }, min: winrateModData ? -25 : averageModScore ? -200000 : -25, max: winrateModData ? 100 : averageModScore ? 1000000 : 100, @@ -189,6 +200,7 @@ export default function RadarChart({ family: font, weight: 300, }, + color: theme === 'dark' ? 'rgba(250,250,250,0.7)' : '#707070', stepSize: winrateModData ? 25 : averageModScore ? 200000 : 25, callback: (value: any, tick: any, values: any) => { return value !== 0 diff --git a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css index 9cec94b..f9bafe2 100644 --- a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css +++ b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css @@ -1,6 +1,6 @@ .collapsible { width: 100%; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); padding: var(--main-padding); border-radius: var(--main-borderRadius); display: flex; @@ -67,6 +67,10 @@ border-radius: 0.4rem; } +[data-theme='dark'] .buttons .save { + color: #333; +} + .buttons .save:hover { background-color: hsla(var(--blue-500), 0.8); color: #fff; diff --git a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx index 7fc1e65..1675cf6 100644 --- a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx +++ b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx @@ -19,7 +19,7 @@ export default function FiltersCollapsible({ isCollapsibleOpen: boolean; data: {}; }) { - const { type, rank, rating, matches, winrate, inclTier, exclTier } = params; + const { type, rank, rating, matches, winrate, tiers } = params; const [paramsToPush, setParamsToPush] = useState({}); @@ -34,21 +34,13 @@ export default function FiltersCollapsible({ useEffect(() => { setParamsToPush({ ...params, - inclTier: - inclTier == null + tiers: + tiers == null ? [] - : typeof inclTier === 'string' - ? [inclTier] - : typeof inclTier === 'object' - ? inclTier - : [], - exclTier: - exclTier == null - ? [] - : typeof exclTier === 'string' - ? [exclTier] - : typeof exclTier === 'object' - ? exclTier + : typeof tiers === 'string' + ? [tiers] + : typeof tiers === 'object' + ? tiers : [], rank: rank != null ? rank : [], rating: rating != null ? rating : [], @@ -156,10 +148,7 @@ export default function FiltersCollapsible({

Tier

- +
diff --git a/components/Dashboard/FilterChangeButton/FilterChangeButton.module.css b/components/Dashboard/FilterChangeButton/FilterChangeButton.module.css index 1f44884..d9a1cb6 100644 --- a/components/Dashboard/FilterChangeButton/FilterChangeButton.module.css +++ b/components/Dashboard/FilterChangeButton/FilterChangeButton.module.css @@ -2,7 +2,7 @@ border-radius: var(--main-borderRadius); padding: 0.8vw 2vw; font-size: 1.3rem; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); transition: all 0.2s ease-out; text-transform: capitalize; } @@ -14,7 +14,7 @@ } .button:not(.selected):hover { - background-color: hsla(var(--gray-200)); + background-color: hsla(var(--background-content-childs-hsl)); color: hsla(var(--gray-900)); cursor: pointer; } diff --git a/components/Dashboard/GridCard/GridCard.module.css b/components/Dashboard/GridCard/GridCard.module.css index db9302f..e75ff0b 100644 --- a/components/Dashboard/GridCard/GridCard.module.css +++ b/components/Dashboard/GridCard/GridCard.module.css @@ -1,7 +1,7 @@ .card { display: flex; flex-flow: column; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); padding: var(--main-padding); border-radius: var(--main-borderRadius); gap: 1.3rem; diff --git a/components/Dashboard/Matches/UserTotalMatches/UserTotalMatches.module.css b/components/Dashboard/Matches/UserTotalMatches/UserTotalMatches.module.css index 6f23274..38d34a2 100644 --- a/components/Dashboard/Matches/UserTotalMatches/UserTotalMatches.module.css +++ b/components/Dashboard/Matches/UserTotalMatches/UserTotalMatches.module.css @@ -3,7 +3,7 @@ display: flex; flex-flow: column; padding: var(--main-padding); - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); gap: 1rem; } diff --git a/components/Dashboard/UserMainCard/UserMainCard.module.css b/components/Dashboard/UserMainCard/UserMainCard.module.css index 6967408..e47c797 100644 --- a/components/Dashboard/UserMainCard/UserMainCard.module.css +++ b/components/Dashboard/UserMainCard/UserMainCard.module.css @@ -13,7 +13,7 @@ align-items: center; grid-area: tierImage; gap: 1.2rem; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: 1.4rem 1.8rem; } @@ -44,7 +44,7 @@ flex-flow: column; align-items: flex-start; gap: 0.5rem; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: 1.4rem 1.8rem; } diff --git a/components/ErrorToast/ErrorToast.module.css b/components/ErrorToast/ErrorToast.module.css index 946fba2..a954adf 100644 --- a/components/ErrorToast/ErrorToast.module.css +++ b/components/ErrorToast/ErrorToast.module.css @@ -1,6 +1,8 @@ .toast { - width: 26em; + width: 28em; height: auto; + max-width: 40em; + max-height: 14em; background-color: hsla(var(--toast-error-border)); color: #222; display: flex; @@ -9,7 +11,7 @@ position: fixed; bottom: 0; left: 50%; - margin-left: -13em; + margin-left: -14em; border-radius: 0.3rem; box-sizing: border-box; -moz-box-sizing: border-box; @@ -18,6 +20,7 @@ font-family: var(--font-families); transform: translateY(-1em); opacity: 1; + z-index: 4; } .toast .header { @@ -44,7 +47,10 @@ } .toast .body { + height: 100%; padding: 1em; background-color: hsla(var(--toast-error-bg), 0.8); border-radius: 0.2rem; + overflow: hidden; + text-overflow: ellipsis; } diff --git a/components/Footer/Footer.module.css b/components/Footer/Footer.module.css index 9d62c23..ef557a0 100644 --- a/components/Footer/Footer.module.css +++ b/components/Footer/Footer.module.css @@ -1,7 +1,7 @@ .footer { border-radius: var(--main-borderRadius); padding: 1rem 1.2rem; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); margin: var(--main-padding); display: flex; flex-flow: row; diff --git a/components/Form/Form.module.css b/components/Form/Form.module.css index a82e0cc..edfc051 100644 --- a/components/Form/Form.module.css +++ b/components/Form/Form.module.css @@ -7,7 +7,7 @@ .form :is(input, select, textarea, button) { width: 100%; - background-color: transparent; + background-color: hsla(var(--background-content-hsl)); border: 1px solid hsla(var(--gray-500)); border-radius: 5px; font-family: var(--font-families); diff --git a/components/Homepage/Logo/Logo.module.css b/components/Homepage/Logo/Logo.module.css new file mode 100644 index 0000000..76e7538 --- /dev/null +++ b/components/Homepage/Logo/Logo.module.css @@ -0,0 +1,11 @@ +.logo { + width: 100%; + height: 100%; + position: relative; +} + +.logo img { + object-fit: contain; + aspect-ratio: 221/101; + padding: 2rem; +} diff --git a/components/Homepage/Logo/Logo.tsx b/components/Homepage/Logo/Logo.tsx new file mode 100644 index 0000000..07dcc92 --- /dev/null +++ b/components/Homepage/Logo/Logo.tsx @@ -0,0 +1,34 @@ +'use client'; +import fullLogoDark from '@/public/logos/full-logo-dark.svg'; +import fullLogo from '@/public/logos/full-logo.svg'; +import { useTheme } from 'next-themes'; +import Image from 'next/image'; +import { useEffect, useState } from 'react'; +import styles from './Logo.module.css'; + +export default function Logo() { + const { theme } = useTheme(); + const [activeTheme, setTheme] = useState('light'); + + useEffect(() => { + if (theme) { + setTheme(theme); + } + }, [theme]); + + return ( +
+ {'o!TR +
+ ); +} diff --git a/components/Leaderboard/Leaderboard.module.css b/components/Leaderboard/Leaderboard.module.css index 52004d9..12e9f99 100644 --- a/components/Leaderboard/Leaderboard.module.css +++ b/components/Leaderboard/Leaderboard.module.css @@ -6,7 +6,7 @@ .leaderboardContainer { width: 100%; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: var(--main-padding); font-size: 0.95rem; @@ -46,13 +46,18 @@ .table tbody td { padding: 1.2rem 2rem; - background-color: hsla(var(--gray-200)); + background-color: hsla(var(--background-content-childs-hsl)); } .table tbody tr.me td { background-color: hsla(var(--blue-200), 0.6); } +[data-theme='dark'] .table tbody tr.me td { + background-color: hsla(var(--blue-500), 0.22); + color: hsla(var(--foreground-active-hsl)); +} + .table th:nth-child(1) { width: 15%; } diff --git a/components/Leaderboard/Leaderboard.tsx b/components/Leaderboard/Leaderboard.tsx index f163456..710e659 100644 --- a/components/Leaderboard/Leaderboard.tsx +++ b/components/Leaderboard/Leaderboard.tsx @@ -4,6 +4,7 @@ import { faAngleDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Image from 'next/image'; import Link from 'next/link'; +import { Tooltip } from 'react-tooltip'; import FormattedNumber from '../FormattedNumber/FormattedNumber'; import Pagination from '../Pagination/Pagination'; import styles from './Leaderboard.module.css'; @@ -40,7 +41,7 @@ export default function Leaderboard({ {data.leaderboard.map((player, index) => { return ( #{player.globalRank} @@ -58,12 +59,25 @@ export default function Leaderboard({
+ {player.tier} {/* 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/NavBar/ThemeSwitcher/ThemeSwitcher.module.css b/components/NavBar/ThemeSwitcher/ThemeSwitcher.module.css new file mode 100644 index 0000000..3481a09 --- /dev/null +++ b/components/NavBar/ThemeSwitcher/ThemeSwitcher.module.css @@ -0,0 +1,14 @@ +.themeSwitcher { + position: relative; + height: 1.85rem; + width: 1.9rem; +} + +.themeSwitcher img { + object-fit: contain; +} + +[data-theme='dark'] .themeSwitcher img { + -webkit-filter: invert(75%); + filter: invert(75%); +} diff --git a/components/NavBar/ThemeSwitcher/ThemeSwitcher.tsx b/components/NavBar/ThemeSwitcher/ThemeSwitcher.tsx new file mode 100644 index 0000000..92c75c6 --- /dev/null +++ b/components/NavBar/ThemeSwitcher/ThemeSwitcher.tsx @@ -0,0 +1,30 @@ +'use client'; +import moonSVG from '@/public/icons/moon.svg'; +import sunSVG from '@/public/icons/sun.svg'; +import { useTheme } from 'next-themes'; +import Image from 'next/image'; +import { useHotkeys } from 'react-hotkeys-hook'; +import styles from './ThemeSwitcher.module.css'; + +export default function ThemeSwitcher() { + const { theme, setTheme } = useTheme(); + useHotkeys('ctrl+l', (e) => { + e.preventDefault(); + setTheme(theme === 'light' ? 'dark' : 'light'); + }); + + return ( + + ); +} diff --git a/components/Profile/UserMainCard/UserMainCard.module.css b/components/Profile/UserMainCard/UserMainCard.module.css index f49671a..5fd79f7 100644 --- a/components/Profile/UserMainCard/UserMainCard.module.css +++ b/components/Profile/UserMainCard/UserMainCard.module.css @@ -5,7 +5,7 @@ grid-template-areas: 'header' 'rankings'; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); padding: 1.4rem 1.8rem; gap: 1.2rem; @@ -65,3 +65,12 @@ .item .value { font-weight: 600; } + +.item .image { + position: relative; + display: flex; + flex-flow: row; + height: 1.13em; + aspect-ratio: 1; + margin: auto; +} diff --git a/components/Profile/UserMainCard/UserMainCard.tsx b/components/Profile/UserMainCard/UserMainCard.tsx index 65a7569..c7d8654 100644 --- a/components/Profile/UserMainCard/UserMainCard.tsx +++ b/components/Profile/UserMainCard/UserMainCard.tsx @@ -1,5 +1,6 @@ 'use client'; import Image from 'next/image'; +import { Tooltip } from 'react-tooltip'; import styles from './UserMainCard.module.css'; export default function UserMainCardProfile({ @@ -56,8 +57,26 @@ export default function UserMainCardProfile({
Tier
-
- {generalStats?.rankProgress.currentTier} +
+ + {generalStats?.rankProgress.currentTier}
diff --git a/components/Range/RangeSlider.module.css b/components/Range/RangeSlider.module.css index f1044c5..2379f57 100644 --- a/components/Range/RangeSlider.module.css +++ b/components/Range/RangeSlider.module.css @@ -82,9 +82,13 @@ width: var(--range-height); aspect-ratio: 1; border-radius: 50vh; - background-color: hsl(var(--gray-100)); + background-color: hsl(var(--background-content-hsl)); border: 1px solid hsl(var(--gray-600)); display: flex; justify-content: center; align-items: center; } + +[data-theme='dark'] .rangeSelector { + background-color: hsla(var(--background-content-childs-hsl)); +} 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/components/SubmitMatches/MatchForm/MatchForm.module.css b/components/SubmitMatches/MatchForm/MatchForm.module.css index 1ebc0d2..ef3d6ae 100644 --- a/components/SubmitMatches/MatchForm/MatchForm.module.css +++ b/components/SubmitMatches/MatchForm/MatchForm.module.css @@ -5,7 +5,7 @@ width: 100%; height: fit-content; grid-area: form; - background-color: hsla(var(--gray-100)); + background-color: hsla(var(--background-content-hsl)); border-radius: var(--main-borderRadius); } diff --git a/components/SubmitMatches/MatchForm/MatchForm.tsx b/components/SubmitMatches/MatchForm/MatchForm.tsx index fad2cdf..71c81dc 100644 --- a/components/SubmitMatches/MatchForm/MatchForm.tsx +++ b/components/SubmitMatches/MatchForm/MatchForm.tsx @@ -4,6 +4,7 @@ import { saveTournamentMatches } from '@/app/actions'; import Form from '@/components/Form/Form'; import InfoIcon from '@/components/Form/InfoIcon/InfoIcon'; import Toast from '@/components/Toast/Toast'; +import { useSetError } from '@/util/hooks'; import clsx from 'clsx'; import { useEffect, useState } from 'react'; import { useFormState, useFormStatus } from 'react-dom'; @@ -23,13 +24,19 @@ function SubmitButton() { ); } -export default function MatchForm({ userRoles }: { userRoles: Array }) { +export default function MatchForm({ + userScopes, +}: { + userScopes: Array; +}) { const [state, formAction] = useFormState(saveTournamentMatches, initialState); const [rulesAccepted, setRulesAccepted] = useState(false); const [verifierAccepted, setVerifierAccepted] = useState(false); const [showToast, setShowToast] = useState(false); + const setError = useSetError(); + useEffect(() => { // Shows toast for both success or error, but need better implementation for errors /* if (state?.status) { @@ -39,7 +46,11 @@ export default function MatchForm({ userRoles }: { userRoles: Array }) { }, 6000); } */ - if (state?.status === 'success') { + if (state?.error) { + setError(state?.error); + } + + if (state?.success) { document.getElementById('tournament-form')?.reset(); setShowToast(true); setTimeout(() => { @@ -249,7 +260,7 @@ export default function MatchForm({ userRoles }: { userRoles: Array }) { matches can lead to a restriction
- {(userRoles.includes('verifier')) && ( + {userScopes.includes('verifier') && (
}) {
{showToast && ( )} diff --git a/components/TierSelector/TierSelector.tsx b/components/TierSelector/TierSelector.tsx index 6d73ba4..b1d62e9 100644 --- a/components/TierSelector/TierSelector.tsx +++ b/components/TierSelector/TierSelector.tsx @@ -1,11 +1,11 @@ 'use client'; -import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import clsx from 'clsx'; import { useEffect, useState } from 'react'; import styles from './TierSelector.module.css'; -const possibleRanks = [ +const possibleTiers = [ { id: 'bronze', name: 'Bronze', @@ -53,106 +53,60 @@ export default function TierSelector({ value, setParamsToPush, }: { - value: {}; + value: []; setParamsToPush: any; }) { - const [ranks, setRanks] = useState([]); - const [excludedRanks, setExcludedRanks] = useState([]); + const [tiers, setTiers] = useState([]); - const selectRank = async (rank: string) => { - if (!includesMatch(ranks, rank) && !includesMatch(excludedRanks, rank)) { - setRanks((prev: any) => [...prev, rank]); - return setParamsToPush((prev: any) => ({ - ...prev, - inclTier: [...prev.inclTier, rank], - })); - } - if (includesMatch(ranks, rank)) { - setExcludedRanks((prev: any) => [...prev, rank]); - setParamsToPush((prev: any) => ({ - ...prev, - exclTier: [...prev.exclTier, rank], - })); - setRanks((prev: any) => prev.filter((item: any) => item !== rank)); + const selectTier = async (tier: string) => { + if (!includesMatch(tiers, tier)) { + setTiers((prev: any) => [...prev, tier]); return setParamsToPush((prev: any) => { return { ...prev, - inclTier: [ - ...prev.inclTier.slice( - 0, - prev.inclTier.findIndex((name) => name === rank) - ), - ...prev.inclTier.slice( - prev.inclTier.findIndex((name) => name === rank) + 1 - ), - ], + tiers: [...prev.tiers, tier], }; }); } - if (includesMatch(excludedRanks, rank)) { - setExcludedRanks((prev: any) => - prev.filter((item: any) => item !== rank) - ); + if (includesMatch(tiers, tier)) { + setTiers((prev: any) => prev.filter((item: any) => item !== tier)); return setParamsToPush((prev: any) => { return { ...prev, - exclTier: [ - ...prev.exclTier.slice( - 0, - prev.exclTier.findIndex((name) => name === rank) - ), - ...prev.exclTier.slice( - prev.exclTier.findIndex((name) => name === rank) + 1 - ), - ], + tiers: prev.tiers.filter((item) => item !== tier), }; }); } }; useEffect(() => { - setRanks( - typeof value?.inclTier === 'string' - ? [value?.inclTier] - : value?.inclTier ?? [] - ); - setExcludedRanks( - typeof value?.exclTier === 'string' - ? [value?.exclTier] - : value?.exclTier ?? [] - ); - }, [value.inclTier, value.exclTier]); + setTiers(typeof value === 'string' ? [value] : value ?? []); + }, [value]); return (
- {possibleRanks.map((rank) => { + {possibleTiers.map((tier) => { return (
{ - await selectRank(rank.id); + await selectTier(tier.id); }} >
- {includesMatch(ranks, rank.id) ? ( + {includesMatch(tiers, tier.id) ? ( - ) : includesMatch(excludedRanks, rank.id) ? ( - ) : ( '' )}
- {rank.name} + {tier.name}
); })} diff --git a/lib/types.ts b/lib/types.ts index cc3958e..6dfb26d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -22,8 +22,7 @@ export const LeaderboardsQuerySchema = z.object({ rating: z.array(z.number().positive().gte(100)).max(2).optional(), matches: z.array(z.number().positive()).max(2).optional(), winrate: z.array(z.number().gte(0.01).lte(1)).max(2).optional(), - inclTier: z.array(z.enum(leaderboardsTierNames)).optional(), - exclTier: z.array(z.enum(leaderboardsTierNames)).optional(), + tiers: z.array(z.enum(leaderboardsTierNames)).optional(), pageSize: z.number().default(25), }); @@ -61,13 +60,13 @@ export const MatchesSubmitFormSchema = z.object({ export interface SessionUser { id?: number; - userId?: number; + playerId?: number; osuId?: number; osuCountry?: string; osuPlayMode?: number; osuPlayModeSelected?: number; username?: string; - roles?: [string]; + scopes?: [string]; accessToken?: string; refreshToken?: string; isLogged: boolean; diff --git a/package-lock.json b/package-lock.json index 6a2b4cb..5e6bcf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,20 @@ "@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", "framer-motion": "^10.16.4", "iron-session": "^8.0.1", "next": "^14.1.2", + "next-themes": "^0.3.0", "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", "zod": "^3.22.4" }, @@ -125,6 +129,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", @@ -655,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", @@ -1029,6 +1067,11 @@ "date-fns": ">=2.0.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2917,6 +2960,15 @@ } } }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3293,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", @@ -3307,6 +3368,19 @@ "react-dom": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0" } }, + "node_modules/react-tooltip": { + "version": "5.26.3", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.26.3.tgz", + "integrity": "sha512-MpYAws8CEHUd/RC4GaDCdoceph/T4KHM5vS5Dbk8FOmLMvvIht2ymP2htWdrke7K6lqPO8rz8+bnwWUIXeDlzg==", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/react-wrap-balancer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-wrap-balancer/-/react-wrap-balancer-1.1.0.tgz", diff --git a/package.json b/package.json index bf2da85..557cd2c 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,20 @@ "@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", "framer-motion": "^10.16.4", "iron-session": "^8.0.1", "next": "^14.1.2", + "next-themes": "^0.3.0", "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", "zod": "^3.22.4" }, diff --git a/public/icons/ranks/Elite Grandmaster-Old.svg b/public/icons/ranks/Elite Grandmaster-Old.svg new file mode 100644 index 0000000..d6e650c --- /dev/null +++ b/public/icons/ranks/Elite Grandmaster-Old.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/ranks/Elite Grandmaster.svg b/public/icons/ranks/Elite Grandmaster.svg index d6e650c..50384fd 100644 --- a/public/icons/ranks/Elite Grandmaster.svg +++ b/public/icons/ranks/Elite Grandmaster.svg @@ -1,7 +1,9 @@ - - - - - - + + + + + + + + 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 @@ + + + diff --git a/public/icons/sun.svg b/public/icons/sun.svg new file mode 100644 index 0000000..5aa46ad --- /dev/null +++ b/public/icons/sun.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/error-background.svg b/public/images/error-background.svg new file mode 100644 index 0000000..526be2f --- /dev/null +++ b/public/images/error-background.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logos/full-logo-dark.svg b/public/logos/full-logo-dark.svg new file mode 100644 index 0000000..6057e97 --- /dev/null +++ b/public/logos/full-logo-dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/util/ErrorContext.tsx b/util/ErrorContext.tsx index a381652..9e3a82e 100644 --- a/util/ErrorContext.tsx +++ b/util/ErrorContext.tsx @@ -21,11 +21,7 @@ export default function ErrorProvider({ children }: Props): JSX.Element { useEffect(() => { if (!error || show) return; - if ( - error?.status === 400 || - error?.message === 'No access token cookie found.' - ) - return; + if (error?.message === 'No access token cookie found.') return; if (error?.status === 401 && error?.message == '') return; @@ -33,7 +29,7 @@ export default function ErrorProvider({ children }: Props): JSX.Element { setTimeout(() => { setError(undefined); setShow(false); - }, 6000); + }, 7000); }, [error, show]); return (