mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-29 02:18:09 +03:00
More big updates (just incase hdd dies or something)
This commit is contained in:
Generated
+13
-13
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user