diff --git a/client/src/app/pages/RegisterPage.tsx b/client/src/app/pages/RegisterPage.tsx index ec4e3ce34..1326acc6d 100644 --- a/client/src/app/pages/RegisterPage.tsx +++ b/client/src/app/pages/RegisterPage.tsx @@ -143,7 +143,7 @@ export default function RegisterPage() { -
(Opens in a new tab.)
+
(This link opens in a new tab.)
diff --git a/client/src/app/pages/dashboard/DashboardPage.tsx b/client/src/app/pages/dashboard/DashboardPage.tsx index 745fd45bc..097c0a3e0 100644 --- a/client/src/app/pages/dashboard/DashboardPage.tsx +++ b/client/src/app/pages/dashboard/DashboardPage.tsx @@ -1,15 +1,23 @@ import useSetSubheader from "components/layout/header/useSetSubheader"; +import SessionCard from "components/sessions/SessionCard"; +import ApiError from "components/util/ApiError"; +import AsyncLoader from "components/util/AsyncLoader"; import Divider from "components/util/Divider"; import LinkButton from "components/util/LinkButton"; -import SplashText from "components/util/SplashText"; +import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; import { UserContext } from "context/UserContext"; import { UserGameStatsContext } from "context/UserGameStatsContext"; import { UserSettingsContext } from "context/UserSettingsContext"; import { TachiConfig } from "lib/config"; import React, { useContext } from "react"; +import { Alert } from "react-bootstrap"; import { Link } from "react-router-dom"; -import { PublicUserDocument } from "tachi-common"; -import { ToCDNURL } from "util/api"; +import { GetGameConfig, PublicUserDocument } from "tachi-common"; +import { UGSWithRankingData, UserRecentSummary } from "types/api-returns"; +import { APIFetchV1 } from "util/api"; +import { NumericSOV } from "util/sorts"; +import { GameStatContainer } from "./users/UserGamesPage"; export function DashboardPage() { const { settings } = useContext(UserSettingsContext); @@ -21,17 +29,113 @@ export function DashboardPage() { if (!user) { return ; - } else if (user && !ugs) { - return ; } - return ; + return ( + <> + {ugs?.length ? ( + + ) : ( + + )} + + ); } function DashboardLoggedIn({ user }: { user: PublicUserDocument }) { return (
Welcome Back, {user.username}. + + +

Here's all your profiles.

+ + + +
+ ); +} + +function RecentInfo({ user }: { user: PublicUserDocument }) { + const { data, isLoading, error } = useApiQuery( + `/users/${user.id}/recent-summary` + ); + + if (error) { + return ; + } + + if (isLoading || !data) { + return ; + } + + if (!data.recentSessions.length) { + return <>; + } + + return ( + <> + +
+

Today's Summary

+ You've gotten {data.recentPlaycount} new score + {data.recentPlaycount !== 1 ? "s" : ""} today! +
+
+ +

New Sessions

+ You've had {data.recentSessions.length} session + {data.recentSessions.length !== 1 ? "s" : ""} today! + + {data.recentSessions.sort(NumericSOV(x => x.timeEnded, true)).map(e => ( + + ))} + + + ); +} + +function UserGameStatsInfo({ user }: { user: PublicUserDocument }) { + return ( +
+ { + const res = await APIFetchV1( + `/users/${user.id}/game-stats` + ); + + if (!res.success) { + throw new Error(res.description); + } + + return res.body.sort((a, b) => { + if (a.game === b.game) { + const gameConfig = GetGameConfig(a.game); + + return ( + gameConfig.validPlaytypes.indexOf(a.playtype) - + gameConfig.validPlaytypes.indexOf(b.playtype) + ); + } + + const i1 = TachiConfig.supportedGames.indexOf(a.game); + const i2 = TachiConfig.supportedGames.indexOf(b.game); + + return i1 - i2; + }); + }} + > + {ugs => + ugs.map(e => ( + + )) + } +
); } @@ -76,7 +180,7 @@ function DashboardNotLoggedIn() { function FeatureContainer({ tagline, description }: { tagline: string; description: string }) { return ( -
+

{tagline}

{description} @@ -86,5 +190,21 @@ function FeatureContainer({ tagline, description }: { tagline: string; descripti } function DashboardLoggedInNoScores({ user }: { user: PublicUserDocument }) { - return foo; + return ( +
+ + Welcome to {TachiConfig.name}, {user.username}! + +

It looks like you have no scores. Let's get you set up!

+ +

+ Once you've got some scores imported, we'll analyse your scores. +
+ You'll get a profile for that game, and a position on the leaderboards! +

+ + Import some scores! + +
+ ); } diff --git a/client/src/app/pages/dashboard/users/UserGamesPage.tsx b/client/src/app/pages/dashboard/users/UserGamesPage.tsx index 04b96f2f1..6bd220cde 100644 --- a/client/src/app/pages/dashboard/users/UserGamesPage.tsx +++ b/client/src/app/pages/dashboard/users/UserGamesPage.tsx @@ -77,49 +77,35 @@ function GamesInfo({ ugs, reqUser }: { ugs: UserGameStats[]; reqUser: PublicUser return <>; } - // uses modulo to determine whether we're on the - // last row or not - // to do sizing like [12] or [6][6] instead of [3][3][3] - let mdSize = "6"; - let lgSize = "4"; - if (ugs.length - i <= ugs.length % 3 && ugs.length > 3) { - lgSize = (12 / (ugs.length % 3)).toString(); - } - - if (ugs.length - i <= ugs.length % 2 && ugs.length > 2) { - mdSize = (12 / (ugs.length % 2)).toString(); - } - - return ( -
- -
- ); + return ; })} ); } -function GameStatContainer({ ugs, reqUser }: { ugs: UGSWithRankingData } & Props) { +export function GameStatContainer({ ugs, reqUser }: { ugs: UGSWithRankingData } & Props) { return ( - - - View Game Profile - -
- } - header={FormatGame(ugs.game, ugs.playtype)} - > - - - +
+ + + View Game Profile + +
+ } + header={FormatGame(ugs.game, ugs.playtype)} + > + + + +
); } diff --git a/client/src/app/routes/DashboardRoutes.tsx b/client/src/app/routes/DashboardRoutes.tsx index e01523798..d5f93a2df 100644 --- a/client/src/app/routes/DashboardRoutes.tsx +++ b/client/src/app/routes/DashboardRoutes.tsx @@ -1,7 +1,10 @@ import { ErrorPage } from "app/pages/ErrorPage"; import { Layout } from "components/layout/Layout"; -import React from "react"; +import EmailVerify from "components/layout/misc/EmailVerify"; +import { UserContext } from "context/UserContext"; +import React, { useContext, useEffect, useState } from "react"; import { Redirect, Route, Switch } from "react-router-dom"; +import { APIFetchV1 } from "util/api"; import { DashboardPage } from "../pages/dashboard/DashboardPage"; import CreditsPage from "../pages/dashboard/misc/CreditsPage"; import GameRoutes from "./GameRoutes"; @@ -9,6 +12,32 @@ import ImportRoutes from "./ImportRoutes"; import UserRoutes from "./UserRoutes"; export default function DashboardRoutes() { + const { user } = useContext(UserContext); + + const [hasVerifiedEmail, setHasVerifiedEmail] = useState(null); + + useEffect(() => { + if (!user) { + return setHasVerifiedEmail(null); + } + + (async () => { + const hasVerified = await APIFetchV1(`/users/${user.id}/is-email-verified`); + + if (hasVerified.success) { + setHasVerifiedEmail(hasVerified.body); + } + })(); + }, [user]); + + if (hasVerifiedEmail === false) { + return ( + + + + ); + } + return ( diff --git a/client/src/components/layout/misc/EmailVerify.tsx b/client/src/components/layout/misc/EmailVerify.tsx new file mode 100644 index 000000000..322c3205b --- /dev/null +++ b/client/src/components/layout/misc/EmailVerify.tsx @@ -0,0 +1,79 @@ +import Divider from "components/util/Divider"; +import Muted from "components/util/Muted"; +import { UserContext } from "context/UserContext"; +import React, { useContext, useEffect } from "react"; +import { Button } from "react-bootstrap"; +import toast from "react-hot-toast"; +import { SetState } from "types/react"; +import { APIFetchV1 } from "util/api"; +import { ONE_MINUTE } from "util/constants/time"; +import useSetSubheader from "../header/useSetSubheader"; + +export default function EmailVerify({ + setHasVerifiedEmail, +}: { + setHasVerifiedEmail: SetState; +}) { + useSetSubheader("Email Verification..."); + + const { user } = useContext(UserContext); + + async function CheckEmail() { + const r = await APIFetchV1(`/users/${user!.id}/is-email-verified`); + + if (r.success && r.body) { + setHasVerifiedEmail(true); + } + + return r.success ? r.body : false; + } + + useEffect(() => { + const interval = setInterval(CheckEmail, ONE_MINUTE); + + return () => clearInterval(interval); + }, []); + + return ( +
+ One last thing! You need to verify your email before you can use the site. +
+ An email has been sent to the address you signed up with. + + + + { + await APIFetchV1("/auth/resend-verify-email", { + method: "POST", + }); + + toast.success("Sent a new email to your address."); + }} + > + I haven't got it. Re-send the email! + +
+ + + Can't get the email? Signed up with a fake email like an idiot? You'll have to + contact us at the discord below. + + +
+ ); +} diff --git a/client/src/components/sessions/SessionCard.tsx b/client/src/components/sessions/SessionCard.tsx new file mode 100644 index 000000000..326d5ba9d --- /dev/null +++ b/client/src/components/sessions/SessionCard.tsx @@ -0,0 +1,152 @@ +import Card from "components/layout/page/Card"; +import ScoreTable from "components/tables/scores/ScoreTable"; +import ApiError from "components/util/ApiError"; +import Divider from "components/util/Divider"; +import Icon from "components/util/Icon"; +import LinkButton from "components/util/LinkButton"; +import Loading from "components/util/Loading"; +import Muted from "components/util/Muted"; +import useApiQuery from "components/util/query/useApiQuery"; +import { UserContext } from "context/UserContext"; +import React, { useContext, useMemo, useState } from "react"; +import { Button } from "react-bootstrap"; +import toast from "react-hot-toast"; +import { FormatGame, SessionDocument } from "tachi-common"; +import { SessionReturns } from "types/api-returns"; +import { ScoreDataset } from "types/tables"; +import { APIFetchV1 } from "util/api"; +import { CreateChartMap, CreateSongMap } from "util/data"; +import { NumericSOV } from "util/sorts"; +import { FormatDuration } from "util/time"; + +export default function SessionCard({ sessionID }: { sessionID: string }) { + const { user } = useContext(UserContext); + + const { data, isLoading, error } = useApiQuery(`/sessions/${sessionID}`); + + const [highlight, setHighlight] = useState(false); + + useMemo(() => { + setHighlight(data?.session.highlight ?? false); + }, [data]); + + if (error) { + return ; + } + + if (isLoading || !data) { + return ; + } + + const { session, charts, scores, songs, user: sessionUser } = data; + + const songMap = CreateSongMap(songs); + const chartMap = CreateChartMap(charts); + + const scoreDataset: ScoreDataset = []; + + for (const score of scores) { + scoreDataset.push({ + ...score, + __related: { + chart: chartMap.get(score.chartID)!, + song: songMap.get(score.songID)!, + index: 0, + user: sessionUser, + }, + }); + } + + scoreDataset.sort(NumericSOV(x => x.timeAchieved ?? 0, true)); + + return ( + + + + + + {sessionUser.id === user?.id && ( +
+ + + View Session + +
+ )} +
+ ); +} + +function SessionOverview({ session, username }: { session: SessionDocument; username: string }) { + return ( +
+ + +
+
+ + View Session + +
+
+ + +
+ ); +} + +function StatIcon({ name, value }: { name: string; value: React.ReactChild }) { + return ( +
+

+ {name} +

+

{value}

+
+ ); +} diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts index 6853fd7cd..213a04000 100644 --- a/client/src/types/api-returns.ts +++ b/client/src/types/api-returns.ts @@ -164,3 +164,9 @@ export interface UserLeaderboardReturns { users: PublicUserDocument[]; gameStats: UserGameStats[]; } + +export interface UserRecentSummary { + recentPlaycount: integer; + recentSessions: SessionDocument[]; + recentFolders: FolderDocument[]; +} diff --git a/client/src/util/splashes.ts b/client/src/util/splashes.ts index 07890fcc5..1b53c6950 100644 --- a/client/src/util/splashes.ts +++ b/client/src/util/splashes.ts @@ -80,7 +80,7 @@ export const loggedInSplashes = [ "bwhæhæhææhæhhææhæh", "Maximal opinions and Minimal evidence", "Who is the scrimbler bimbler?", - "You wish you know who the hibble gibble gobbler was.", + "You wish you knew who the hibble gibble gobbler was.", `The current day is ${FormatDate(Date.now())}, probably.`, "I have always thought of myself as an ambassador for the dance game community.", "When lava pours out near the sea surface, tremendous volcanic explosions sometimes occur.", diff --git a/client/src/util/time.ts b/client/src/util/time.ts index 56b50e17c..5f31b74f9 100644 --- a/client/src/util/time.ts +++ b/client/src/util/time.ts @@ -14,7 +14,10 @@ export function FormatDate(ms: number) { } export function FormatDuration(ms: number) { - return humaniseDuration(ms); + return humaniseDuration(ms, { + units: ["d", "h", "m"], + maxDecimalPoints: 0, + }); } export function FormatTimeSmall(ms: number) {