More big updates (just incase hdd dies or something)

This commit is contained in:
zkldi
2021-07-29 19:15:24 +01:00
parent 3a80c4ac93
commit bc6d4e4d60
16 changed files with 404 additions and 40 deletions
+13 -13
View File
@@ -143,7 +143,7 @@ dependencies:
resolve: 1.12.0
resolve-url-loader: 3.1.0
socicon: 3.0.5
tachi-common: github.com/zkldi/tachi-common/21c3bb4cc4407601a902e049fb1e1b774b793e40_typescript@4.3.5
tachi-common: github.com/zkldi/tachi-common/353c7dcee5ca01dc25324cbb0e8404e5bf80ce1d_typescript@4.3.5
devDependencies:
'@types/humanize-duration': 3.25.1
@@ -3102,8 +3102,8 @@ packages:
'@babel/runtime': 7.14.6
dev: false
/@types/bson/4.0.4:
resolution: {integrity: sha512-awqorHvQS0DqxkHQ/FxcPX9E+H7Du51Qw/2F+5TBMSaE3G0hm+8D3eXJ6MAzFw75nE8V7xF0QvzUSdxIjJb/GA==}
/@types/bson/4.0.5:
resolution: {integrity: sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==}
dependencies:
'@types/node': 16.0.0
dev: false
@@ -3154,7 +3154,7 @@ packages:
/@types/mongodb/3.6.20:
resolution: {integrity: sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==}
dependencies:
'@types/bson': 4.0.4
'@types/bson': 4.0.5
'@types/node': 16.0.0
dev: false
@@ -8462,7 +8462,7 @@ packages:
bl: 2.2.1
bson: 1.1.6
denque: 1.5.0
optional-require: 1.1.0
optional-require: 1.1.1
safe-buffer: 5.2.1
optionalDependencies:
saslprep: 1.0.3
@@ -8926,8 +8926,8 @@ packages:
webpack: 4.41.0
dev: false
/optional-require/1.1.0:
resolution: {integrity: sha512-5/7ee3eTFg1P+F9usTubuNCLfWRK6DjV0EFHLlbp7MmV5UlWqpWIVSnH6xo4u+fc5WHXaJuvJi6iuYnfDyj6oQ==}
/optional-require/1.1.1:
resolution: {integrity: sha512-EnUe33GTAltyZlIsQ2l93KzBC9zi8BsxLvKP3wxALOsz/YIakVojyuZsv5PFFk8y8e6r+SbaPIsNmyPoSK0OHw==}
engines: {node: '>=4'}
dependencies:
require-at: 1.0.6
@@ -10435,8 +10435,8 @@ packages:
react-dom: 16.12.0_react@17.0.2
dev: false
/react/16.12.0:
resolution: {integrity: sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==}
/react/16.14.0:
resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==}
engines: {node: '>=0.10.0'}
dependencies:
loose-envify: 1.4.0
@@ -11732,7 +11732,7 @@ packages:
mkdirp: 1.0.4
nyc: 15.1.0
opener: 1.5.2
react: 16.12.0
react: 16.14.0
rimraf: 3.0.2
signal-exit: 3.0.3
source-map-support: 0.5.19
@@ -12799,9 +12799,9 @@ packages:
yargs-parser: 18.1.3
dev: false
github.com/zkldi/tachi-common/21c3bb4cc4407601a902e049fb1e1b774b793e40_typescript@4.3.5:
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/21c3bb4cc4407601a902e049fb1e1b774b793e40}
id: github.com/zkldi/tachi-common/21c3bb4cc4407601a902e049fb1e1b774b793e40
github.com/zkldi/tachi-common/353c7dcee5ca01dc25324cbb0e8404e5bf80ce1d_typescript@4.3.5:
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/353c7dcee5ca01dc25324cbb0e8404e5bf80ce1d}
id: github.com/zkldi/tachi-common/353c7dcee5ca01dc25324cbb0e8404e5bf80ce1d
name: tachi-common
version: 0.1.0
dependencies:
@@ -6,7 +6,7 @@ import { UserContext } from "context/UserContext";
import { UserGameStatsContext } from "context/UserGameStatsContext";
import { nanoid } from "nanoid";
import React, { useContext, useMemo, useState } from "react";
import { Button, OverlayTrigger, Tooltip } from "react-bootstrap";
import { Badge, Button, OverlayTrigger, Tooltip } from "react-bootstrap";
import { Link } from "react-router-dom";
import {
FormatGame,
@@ -20,8 +20,10 @@ import {
FolderDocument,
GetGamePTConfig,
UserGameStats,
SessionDocument,
ScoreDocument,
} from "tachi-common";
import { UGPTHistory, UGPTPreferenceStatsReturn } from "types/api-returns";
import { SessionReturns, UGPTHistory, UGPTPreferenceStatsReturn } from "types/api-returns";
import { GamePT } from "types/react";
import { APIFetchV1 } from "util/api";
import AsyncLoader from "components/util/AsyncLoader";
@@ -30,9 +32,16 @@ import TimelineChart from "components/charts/TimelineChart";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import { DateTime } from "luxon";
import { FormatDate, MillisToSince } from "util/time";
import { FormatDate, FormatDuration, FormatTime, MillisToSince } from "util/time";
import SelectButton from "components/util/SelectButton";
import { UppercaseFirst } from "util/misc";
import { ONE_HOUR, ONE_MINUTE } from "util/constants/time";
import MiniTable from "components/tables/components/MiniTable";
import { GetPBs, CreateChartMap, CreateSongMap } from "util/data";
import IIDXScoreTable from "components/tables/scores/IIDXScoreTable";
import { NumericSOV } from "util/sorts";
import { ScoreDataset } from "types/tables";
export default function OverviewPage({
reqUser,
@@ -51,10 +60,202 @@ export default function OverviewPage({
<>
<StatShowcase reqUser={reqUser} game={game} playtype={playtype} />
<RankingInfo reqUser={reqUser} game={game} playtype={playtype} />
<LastSession reqUser={reqUser} game={game} playtype={playtype} />
</>
);
}
function LastSession({ reqUser, game, playtype }: { reqUser: PublicUserDocument } & GamePT) {
return (
<AsyncLoader
promiseFn={async () => {
const res = await APIFetchV1<SessionDocument>(
`/users/${reqUser.id}/games/${game}/${playtype}/sessions/last`
);
if (res.statusCode === 404) {
return null;
}
if (!res.success) {
throw new Error(res.description);
}
const sessionDataRes = await APIFetchV1<SessionReturns>(
`/sessions/${res.body!.sessionID}`
);
if (!sessionDataRes.success) {
throw new Error(res.description);
}
return { session: res.body, sessionData: sessionDataRes.body };
}}
>
{data =>
data && (
<Card className="mt-4" header="Most Recent Session">
<div className="row d-flex justify-content-center">
{
<>
<div className="col-12 text-center">
<h4 className="display-4">{data.session.name}</h4>
<span className="text-muted">{data.session.desc}</span>
<Divider className="mt-4 mb-8" />
</div>
<div className="col-12 col-lg-3 text-center">
<MiniTable
className="mt-4"
headers={["Information"]}
colSpan={2}
>
<tr>
<td>Started</td>
<td>{FormatTime(data.session.timeStarted)}</td>
</tr>
<tr>
{/* If session ended less than 2 hours ago, it can still be appended to. */}
{/* Kinda - time is a bit nonlinear wrt. importing scores, as scores don't have to have */}
{/* happened at the same time they were imported. */}
{/* that doesn't matter though - this is just a UI thing */}
<td>
{Date.now() - data.session.timeEnded <
ONE_HOUR * 2
? "Last Score"
: "Ended At"}
</td>
<td>
{FormatTime(data.session.timeEnded)}
{/* if the last score was less than 10 mins ago, this session is probably still being populated */}
{Date.now() - data.session.timeEnded <
ONE_MINUTE * 10 && (
<>
<br />
<Badge
variant="primary"
className="mt-2"
>
Maybe Ongoing?
</Badge>
</>
)}
</td>
</tr>
<tr>
<td>Duration</td>
<td>
{FormatDuration(
data.session.timeEnded -
data.session.timeStarted
)}
</td>
</tr>
<tr>
<td>Scores</td>
<td>{data.session.scoreInfo.length}</td>
</tr>
<tr>
<td>PBs</td>
<td>{GetPBs(data.session.scoreInfo).length}</td>
</tr>
</MiniTable>
</div>
<div className="col-12 col-lg-9">
<RecentSessionScoreInfo
{...data}
game={game}
playtype={playtype}
/>
</div>
</>
}
</div>
</Card>
)
}
</AsyncLoader>
);
}
function RecentSessionScoreInfo({
session,
sessionData,
game,
playtype,
}: {
session: SessionDocument;
sessionData: SessionReturns;
} & GamePT) {
const highlightedScores = sessionData.scores.find(e => e.highlight);
const [mode, setMode] = useState<"highlight" | "best" | "recent">(
highlightedScores ? "highlight" : "best"
);
const gptConfig = GetGamePTConfig(game, playtype);
const songMap = CreateSongMap(sessionData.songs);
const chartMap = CreateChartMap(sessionData.charts);
const dataset = useMemo(() => {
let scoreSet = sessionData.scores;
if (mode === "highlight") {
scoreSet = sessionData.scores.filter(e => e.highlight);
} else if (mode === "best") {
scoreSet.sort((a, b) =>
NumericSOV<ScoreDocument>(
d => d.calculatedData[gptConfig.defaultScoreRatingAlg] ?? 0
)(b, a)
);
} else if (mode === "recent") {
// sneaky hack to sort in reverse
scoreSet.sort((a, b) => NumericSOV<ScoreDocument>(d => d.timeAchieved ?? 0)(b, a));
}
const scoreDataset: ScoreDataset = [];
for (const score of scoreSet) {
const chart = chartMap.get(score.chartID)!;
scoreDataset.push({
...score,
__related: {
chart,
song: songMap.get(chart.songID)!,
index: 0,
},
});
}
return scoreDataset;
}, [mode]);
return (
<div className="row">
<div className="col-12 d-flex justify-content-center">
<div className="btn-group">
<SelectButton id="highlight" value={mode} setValue={setMode}>
<Icon type="star" />
Highlights
</SelectButton>
<SelectButton id="best" value={mode} setValue={setMode}>
<Icon type="sort-amount-up" />
Best
</SelectButton>
<SelectButton id="recent" value={mode} setValue={setMode}>
<Icon type="history" />
Recent
</SelectButton>
</div>
</div>
<div className="col-12 mt-4">
<IIDXScoreTable dataset={dataset as any} pageLen={5} />
</div>
</div>
);
}
function RankingInfo({ reqUser, game, playtype }: { reqUser: PublicUserDocument } & GamePT) {
return (
<AsyncLoader
@@ -109,13 +310,13 @@ function UserHistory({
return (
<Card className="mt-4" header={`${reqUser.username}'s History`}>
<div className="row d-flex justify-content-center align-items-center mb-4">
<div className="d-none d-md-block col-md-3 text-center">
{/* <div className="d-none d-md-block col-md-3 text-center">
<div className="mb-4">Something</div>
<div>
<span className="display-4">Todo</span>
</div>
</div>
<div className="col-12 col-md-6 d-flex justify-content-center">
</div> */}
<div className="col-12 col-md-6 offset-md-3 d-flex justify-content-center">
<div className="btn-group">
<SelectButton id="ranking" value={mode} setValue={setMode}>
<Icon type="trophy" />
+14 -2
View File
@@ -33,12 +33,24 @@ export default function TimelineChart({
style={{ height: mobileHeight, width: mobileWidth }}
>
<div className="d-flex h-100 justify-content-center align-items-center">
Not Enough Data... Yet.
<div className="text-center">
Not Enough Data... Yet.
<br />
<small className="text-muted">
(You need atleast 7 days worth of data)
</small>
</div>
</div>
</div>
<div className="d-none d-md-block" style={{ height, width }}>
<div className="d-flex h-100 justify-content-center align-items-center">
Not Enough Data... Yet.
<div className="text-center">
Not Enough Data... Yet.
<br />
<small className="text-muted">
(You need atleast 7 days worth of data)
</small>
</div>
</div>
</div>
</>
+50
View File
@@ -0,0 +1,50 @@
import QuickTooltip from "components/layout/misc/QuickTooltip";
import React from "react";
import { Badge } from "react-bootstrap";
import { GetGamePTConfig, IDStrings, integer } from "tachi-common";
import { GameClassSets } from "tachi-common/js/game-classes";
import { GamePT } from "types/react";
import { UppercaseFirst } from "util/misc";
export default function ClassBadge<I extends IDStrings = IDStrings>({
game,
playtype,
classSet,
classValue,
showSetOnHover = true,
}: {
classSet: GameClassSets[I];
classValue: integer;
showSetOnHover?: boolean;
} & GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const data = gptConfig.classHumanisedFormat[classSet][classValue];
if (!data) {
throw new Error(`Unknown class value ${classSet}:${classValue}`);
}
let badgeComponent;
if (data.variant) {
badgeComponent = <Badge variant={data.variant}>{data.display}</Badge>;
} else if (data.css) {
badgeComponent = <Badge style={data.css}>{data.display}</Badge>;
} else {
badgeComponent = <Badge variant="secondary">{data.display}</Badge>;
}
if (data.mouseover && showSetOnHover) {
return (
<QuickTooltip text={`${UppercaseFirst(classSet)}: ${data.mouseover}`}>
{badgeComponent}
</QuickTooltip>
);
} else if (data.mouseover) {
return <QuickTooltip text={data.mouseover}>{badgeComponent}</QuickTooltip>;
} else if (showSetOnHover) {
<QuickTooltip text={UppercaseFirst(classSet)}>{badgeComponent}</QuickTooltip>;
}
return badgeComponent;
}
@@ -17,7 +17,11 @@ export function Footer() {
</div>
<div className="nav nav-dark order-1 order-md-2 justify-content-center">
<Link to="/dashboard/credits" className="nav-link px-3">
<Link
to="/dashboard/credits"
className="nav-link px-3"
onClick={() => window.scrollTo(0, 0)}
>
Credits
</Link>
<a
@@ -21,5 +21,5 @@ export default function AllGames() {
}
}
return <MenuDropdown name="All Games">{links}</MenuDropdown>;
return <MenuDropdown name="Games">{links}</MenuDropdown>;
}
@@ -66,7 +66,7 @@ export function HeaderMenu() {
{/* <MenuLink name="Dashboard" to="/dashboard" /> */}
{user && ugs && ugs.length !== 0 && (
<MenuDropdown name="My Games">
<MenuDropdown name="Your Profiles">
{ugs.map(e => (
<MenuLink
key={`${e.game}:${e.playtype}`}
@@ -10,7 +10,9 @@ export default function MiniTable({
colSpan = 1,
}: { className?: string; headers: string[]; colSpan?: integer } & JustChildren) {
return (
<table className={`table table-hover table-striped ${className}`}>
<table
className={`table table-hover table-striped table-vertical-center text-center ${className}`}
>
<thead>
<tr>
{headers.map(e => (
@@ -7,7 +7,7 @@ import { ChangeOpacity } from "util/color-opacity";
import { NumericSOV, StrSOV } from "util/sorts";
import SelectableRating from "../components/SelectableRating";
import { ScoreDataset } from "types/tables";
import { GetGamePTConfig, ScoreCalculatedDataLookup } from "tachi-common";
import { GetGamePTConfig, integer, ScoreCalculatedDataLookup } from "tachi-common";
import TachiTable, { Header } from "../components/TachiTable";
import DifficultyCell from "../cells/DifficultyCell";
import ScoreCell from "../cells/ScoreCell";
@@ -15,7 +15,13 @@ import DeltaCell from "../cells/DeltaCell";
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
import { nanoid } from "nanoid";
export default function IIDXScoreTable({ dataset }: { dataset: ScoreDataset<"iidx:SP"> }) {
export default function IIDXScoreTable({
dataset,
pageLen,
}: {
dataset: ScoreDataset<"iidx:SP">;
pageLen?: integer;
}) {
const gptConfig = GetGamePTConfig<"iidx:SP">("iidx", "SP");
const [rating, setRating] = useState<ScoreCalculatedDataLookup["iidx:SP"]>(
@@ -25,6 +31,7 @@ export default function IIDXScoreTable({ dataset }: { dataset: ScoreDataset<"iid
return (
<TachiTable
dataset={dataset}
pageLen={pageLen}
headers={[
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
["Song", "Song", StrSOV(x => x.__related.song.title)],
@@ -85,7 +92,27 @@ export default function IIDXScoreTable({ dataset }: { dataset: ScoreDataset<"iid
>
<strong>{sc.scoreData.lamp}</strong>
<br />
<small>[BP: {sc.scoreData.hitMeta.bp ?? "No Data"}]</small>
<small>
[BP: {sc.scoreData.hitMeta.bp ?? "No Data"}
{sc.scoreData.hitMeta.comboBreak
? `, CBRK: ${sc.scoreData.hitMeta.comboBreak}`
: ""}
]
</small>
{sc.scoreData.lamp === "FAILED" && (
<>
<br />
<small className="text-muted">
{sc.scoreData.hitMeta.gsm
? `EC: ${sc.scoreData.hitMeta.gsm.EASY}, NC: ${sc.scoreData.hitMeta.gsm.NORMAL}`
: sc.scoreMeta.gauge === "EASY"
? `EC: ${sc.scoreData.hitMeta.gauge}`
: sc.scoreMeta.gauge === "NORMAL"
? `NC: ${sc.scoreMeta.gauge}`
: ""}
</small>
</>
)}
</td>
{rating === "BPI" ? (
<BPICell bpi={sc.calculatedData.BPI} />
@@ -9,6 +9,7 @@ import {
SessionScoreInfo,
} from "tachi-common";
import { Playtype } from "types/tachi";
import { GetPBs } from "util/data";
import { NumericSOV, StrSOV } from "util/sorts";
import { FormatDuration, FormatTime, MillisToSince } from "util/time";
import IndexCell from "../cells/IndexCell";
@@ -87,7 +88,3 @@ export default function GenericSessionTable({
/>
);
}
function GetPBs(scoreInfo: SessionScoreInfo[]) {
return scoreInfo.map(e => e.isNewScore === true || e.lampDelta > 0 || e.scoreDelta > 0);
}
+23 -7
View File
@@ -1,13 +1,15 @@
import MiniTable from "components/tables/components/MiniTable";
import Divider from "components/util/Divider";
import React from "react";
import { Game, GetGameConfig, PublicUserDocument } from "tachi-common";
import { Game, GetGameConfig, GetGamePTConfig, PublicUserDocument } from "tachi-common";
import { UGPTStatsReturn } from "types/api-returns";
import { Playtype } from "types/tachi";
import { MillisToSince } from "util/time";
import ProfilePicture from "./ProfilePicture";
import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import ClassBadge from "components/game/ClassBadge";
import { UppercaseFirst } from "util/misc";
export default function UGPTHeader({
reqUser,
@@ -21,6 +23,7 @@ export default function UGPTHeader({
stats: UGPTStatsReturn;
}) {
const gameConfig = GetGameConfig(game);
const gptConfig = GetGamePTConfig(game, playtype);
return (
<div className="row">
@@ -77,18 +80,31 @@ export default function UGPTHeader({
colSpan={2}
>
<>
{gptConfig.supportedClasses.map(k => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{stats.gameStats.classes[k] ? (
<ClassBadge
showSetOnHover={false}
key={`${k}:${stats.gameStats.classes[k]}`}
game={game}
playtype={playtype}
classSet={k}
classValue={stats.gameStats.classes[k]!}
/>
) : (
"No Data"
)}
</td>
</tr>
))}
{Object.entries(stats.gameStats.ratings).map(([k, v]) => (
<tr key={k}>
<td>{k}</td>
<td>{v.toFixed(2)}</td>
</tr>
))}
{Object.entries(stats.gameStats.classes).map(([k, v]) => (
<tr key={k}>
<td>{k}</td>
<td>{v}</td>
</tr>
))}
</>
</MiniTable>
</div>
+1 -1
View File
@@ -9,7 +9,7 @@ export default function AsyncLoader<T = unknown>({
}: {
promiseFn?: () => Promise<T>;
promise?: Promise<T>;
children: (data: T) => JSX.Element;
children: (data: T) => JSX.Element | null | string | (JSX.Element | string)[];
}) {
const Component = children;
return (
+12
View File
@@ -10,6 +10,10 @@ import {
ShowcaseStatChart,
ShowcaseStatFolder,
UserGameStatsSnapshot,
SessionDocument,
IDStringToGame,
ChartDocument,
SongDocument,
} from "tachi-common";
export interface UGPTStatsReturn<I extends IDStrings = IDStrings> {
@@ -55,3 +59,11 @@ export type UGPTPreferenceStatsReturn =
};
export type UGPTHistory = Omit<UserGameStatsSnapshot, "userID" | "game" | "playtype">[];
export interface SessionReturns<I extends IDStrings = IDStrings> {
session: SessionDocument;
scores: ScoreDocument[];
songs: SongDocument<IDStringToGame[I]>[];
charts: ChartDocument<I>[];
user: PublicUserDocument;
}
+6
View File
@@ -45,6 +45,12 @@ export async function APIFetchV1<T = unknown>(
const rj = await res.json();
if (!rj.success) {
console.error(rj);
} else {
console.log(rj.description);
}
if (!rj.success && displayFailure) {
toast.error(rj.description, { duration: 10_000 });
}
+5
View File
@@ -0,0 +1,5 @@
export const ONE_SECOND = 1000;
export const ONE_MINUTE = ONE_SECOND * 60;
export const ONE_HOUR = ONE_MINUTE * 60;
export const ONE_DAY = ONE_HOUR * 24;
export const ONE_WEEK = ONE_DAY * 7;
+32
View File
@@ -0,0 +1,32 @@
import {
ChartDocument,
Game,
IDStrings,
integer,
SessionScoreInfo,
SongDocument,
} from "tachi-common";
export function GetPBs(scoreInfo: SessionScoreInfo[]) {
return scoreInfo.map(e => e.isNewScore === true || e.lampDelta > 0 || e.scoreDelta > 0);
}
export function CreateSongMap<G extends Game = Game>(songs: SongDocument<G>[]) {
const songMap = new Map<integer, SongDocument<G>>();
for (const song of songs) {
songMap.set(song.id, song);
}
return songMap;
}
export function CreateChartMap<I extends IDStrings = IDStrings>(charts: ChartDocument<I>[]) {
const chartMap = new Map<string, ChartDocument<I>>();
for (const chart of charts) {
chartMap.set(chart.chartID, chart);
}
return chartMap;
}