#{player.globalRank}
@@ -58,12 +59,25 @@ export default function Leaderboard({
+
{/*
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/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 (
+ setTheme(theme === 'light' ? 'dark' : 'light')}>
+
+ {theme === 'light' && }
+ {theme === 'dark' && }
+ {!theme && <>>}
+ {/* */}
+
+
+ );
+}
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}
+
+
+
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 (
+
+
+
+ {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/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 (