mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-07 14:28:09 +03:00
Add Session Page
This commit is contained in:
@@ -97,12 +97,13 @@ export default function SessionsPage({
|
||||
<GenericSessionTable
|
||||
indexCol={sessionSet === "best"}
|
||||
dataset={data!}
|
||||
reqUser={reqUser}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
/>
|
||||
</LoadingWrapper>
|
||||
) : (
|
||||
<SearchSessionsTable {...{ game, playtype, baseUrl, search }} />
|
||||
<SearchSessionsTable {...{ game, playtype, reqUser, baseUrl, search }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,8 +114,9 @@ function SearchSessionsTable({
|
||||
search,
|
||||
game,
|
||||
playtype,
|
||||
reqUser,
|
||||
baseUrl,
|
||||
}: { search: string; baseUrl: string } & GamePT) {
|
||||
}: { search: string; baseUrl: string; reqUser: PublicUserDocument } & GamePT) {
|
||||
const { isLoading, error, data } = useQuery<SessionDataset, UnsuccessfulAPIResponse>(
|
||||
`${baseUrl}?search=${search}`,
|
||||
async () => {
|
||||
@@ -135,7 +137,12 @@ function SearchSessionsTable({
|
||||
|
||||
return (
|
||||
<LoadingWrapper {...{ isLoading, error, dataset: data }}>
|
||||
<GenericSessionTable dataset={data!} game={game} playtype={playtype} />
|
||||
<GenericSessionTable
|
||||
reqUser={reqUser}
|
||||
dataset={data!}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
/>
|
||||
</LoadingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import SessionOverview from "components/sessions/SessionOverview";
|
||||
import SessionRaiseBreakdown from "components/sessions/SessionRaiseBreakdown";
|
||||
import ScoreTable from "components/tables/scores/ScoreTable";
|
||||
import ApiError from "components/util/ApiError";
|
||||
import Divider from "components/util/Divider";
|
||||
import FormInput from "components/util/FormInput";
|
||||
import Icon from "components/util/Icon";
|
||||
import Loading from "components/util/Loading";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
import { Badge, Button, Col, Form, Modal, Row } from "react-bootstrap";
|
||||
import { Redirect, useParams } from "react-router-dom";
|
||||
import { GetGameConfig, PublicUserDocument, SessionDocument } from "tachi-common";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
import { GamePT, SetState } from "types/react";
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data";
|
||||
import { DelayedPageReload } from "util/misc";
|
||||
|
||||
type Props = { reqUser: PublicUserDocument } & GamePT;
|
||||
|
||||
export default function SpecificSessionPage({ reqUser, game, playtype }: Props) {
|
||||
const { sessionID } = useParams<{ sessionID: string }>();
|
||||
|
||||
const { data, isLoading, error } = useApiQuery<SessionReturns>(`/sessions/${sessionID}`);
|
||||
|
||||
if (error) {
|
||||
return <ApiError error={error} />;
|
||||
}
|
||||
|
||||
if (isLoading || !data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (
|
||||
data.user.id !== reqUser.id ||
|
||||
game !== data.session.game ||
|
||||
playtype !== data.session.playtype
|
||||
) {
|
||||
return (
|
||||
<Redirect
|
||||
to={`/dashboard/users/${data.user.username}/games/${data.session.game}/${data.session.playtype}/sessions/${sessionID}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SessionPage {...{ data, game, playtype, reqUser }} />;
|
||||
}
|
||||
|
||||
function SessionPage({ data, game, playtype }: Props & { data: SessionReturns }) {
|
||||
const { session, user, charts, scores, songs } = data;
|
||||
|
||||
const { user: loggedInUser } = useContext(UserContext);
|
||||
|
||||
useSetSubheader(
|
||||
[
|
||||
"Users",
|
||||
user.username,
|
||||
"Games",
|
||||
GetGameConfig(game).name,
|
||||
playtype,
|
||||
"Sessions",
|
||||
session.name,
|
||||
],
|
||||
[session.name, game, playtype, user],
|
||||
`${user.username}: ${session.name}`
|
||||
);
|
||||
|
||||
const [view, setView] = useState<"raises" | "overview" | "scores">("overview");
|
||||
|
||||
const songMap = CreateSongMap(songs);
|
||||
const chartMap = CreateChartMap(charts);
|
||||
const scoreMap = CreateScoreIDMap(scores);
|
||||
|
||||
const scoreDataset = useMemo(() => {
|
||||
const d = [];
|
||||
|
||||
for (const scoreInfo of session.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
if (!score) {
|
||||
console.error(`No score for scoreID ${scoreInfo.scoreID}, but one was in session?`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const chart = chartMap.get(score.chartID);
|
||||
const song = songMap.get(score.songID);
|
||||
|
||||
if (!chart || !song) {
|
||||
console.error(`No chart for ${score.chartID} (${score.songID})?`);
|
||||
continue;
|
||||
}
|
||||
|
||||
d.push({
|
||||
...score,
|
||||
__related: {
|
||||
chart,
|
||||
song,
|
||||
index: 0,
|
||||
user,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return d;
|
||||
}, [data]);
|
||||
|
||||
const [highlight, setHighlight] = useState(session.highlight);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
|
||||
return (
|
||||
<Row className="justify-content-center">
|
||||
<Col xs={12} className="text-center">
|
||||
<h1>{session.name}</h1>
|
||||
<h4 className="text-muted mb-4">{session.desc}</h4>
|
||||
<Badge variant="secondary">
|
||||
<span
|
||||
style={{
|
||||
lineHeight: "15px",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
>
|
||||
<Icon type="eye" style={{ verticalAlign: "middle" }} /> Views:{" "}
|
||||
{session.views}
|
||||
</span>
|
||||
</Badge>
|
||||
{session.highlight && (
|
||||
<Badge
|
||||
style={{
|
||||
lineHeight: "15px",
|
||||
}}
|
||||
variant="warning"
|
||||
className="ml-2"
|
||||
>
|
||||
Highlight!
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Divider className="mt-8 mb-4" />
|
||||
{user.id === loggedInUser?.id && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline-info"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
className="mr-4"
|
||||
>
|
||||
Edit Session
|
||||
</Button>
|
||||
{highlight ? (
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={async () => {
|
||||
setHighlight(false);
|
||||
session.highlight = false;
|
||||
|
||||
await APIFetchV1(
|
||||
`/sessions/${session.sessionID}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
highlight: false,
|
||||
}),
|
||||
},
|
||||
true,
|
||||
true
|
||||
);
|
||||
}}
|
||||
>
|
||||
Un-Highlight Session
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline-warning"
|
||||
onClick={async () => {
|
||||
setHighlight(true);
|
||||
session.highlight = true;
|
||||
|
||||
await APIFetchV1(
|
||||
`/sessions/${session.sessionID}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
highlight: true,
|
||||
}),
|
||||
},
|
||||
true,
|
||||
true
|
||||
);
|
||||
}}
|
||||
>
|
||||
Highlight Session
|
||||
</Button>
|
||||
)}
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<div className="btn-group">
|
||||
<SelectButton value={view} setValue={setView} id="raises">
|
||||
<Icon type="receipt" />
|
||||
Raises
|
||||
</SelectButton>
|
||||
<SelectButton value={view} setValue={setView} id="overview">
|
||||
<Icon type="chart-area" />
|
||||
Overview
|
||||
</SelectButton>
|
||||
<SelectButton value={view} setValue={setView} id="scores">
|
||||
<Icon type="table" />
|
||||
All Scores
|
||||
</SelectButton>
|
||||
</div>
|
||||
<Divider />
|
||||
</Col>
|
||||
{view === "raises" ? (
|
||||
<SessionRaiseBreakdown sessionData={data} />
|
||||
) : view === "overview" ? (
|
||||
<SessionOverview scoreDataset={scoreDataset} sessionData={data} />
|
||||
) : (
|
||||
<Col xs={12}>
|
||||
<ScoreTable
|
||||
dataset={scoreDataset}
|
||||
game={session.game}
|
||||
playtype={session.playtype}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
{showEditModal && (
|
||||
<EditModal session={session} show={showEditModal} setShow={setShowEditModal} />
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function EditModal({
|
||||
session,
|
||||
show,
|
||||
setShow,
|
||||
}: {
|
||||
session: SessionDocument;
|
||||
show: boolean;
|
||||
setShow: SetState<boolean>;
|
||||
}) {
|
||||
const [name, setName] = useState(session.name);
|
||||
const [desc, setDesc] = useState(session.desc ?? "No Description...");
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={() => setShow(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Edit Comment</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form
|
||||
onSubmit={async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const r = await APIFetchV1(
|
||||
`/sessions/${session.sessionID}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
desc,
|
||||
}),
|
||||
},
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
if (r.success) {
|
||||
DelayedPageReload();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormInput fieldName="Session Name" value={name} setValue={setName} />
|
||||
<Divider />
|
||||
<FormInput
|
||||
fieldName="Session Description"
|
||||
value={desc ?? ""}
|
||||
setValue={setDesc as SetState<string>}
|
||||
/>
|
||||
<Divider />
|
||||
<Button type="submit">Submit</Button>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import FoldersMainPage from "app/pages/dashboard/users/games/_game/_playtype/fol
|
||||
import LeaderboardsPage from "app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage";
|
||||
import OverviewPage from "app/pages/dashboard/users/games/_game/_playtype/OverviewPage";
|
||||
import SessionsPage from "app/pages/dashboard/users/games/_game/_playtype/SessionsPage";
|
||||
import SpecificSessionPage from "app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage";
|
||||
import UGPTSettingsPage from "app/pages/dashboard/users/games/_game/_playtype/UGPTSettingsPage";
|
||||
import UserGamesPage from "app/pages/dashboard/users/UserGamesPage";
|
||||
import UserIntegrationsPage from "app/pages/dashboard/users/UserIntegrationsPage";
|
||||
@@ -247,6 +248,9 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument
|
||||
<Route exact path="/dashboard/users/:userID/games/:game/:playtype/sessions">
|
||||
<SessionsPage reqUser={reqUser} game={game} playtype={playtype} />
|
||||
</Route>
|
||||
<Route path="/dashboard/users/:userID/games/:game/:playtype/sessions/:sessionID">
|
||||
<SpecificSessionPage reqUser={reqUser} game={game} playtype={playtype} />
|
||||
</Route>
|
||||
<Route exact path="/dashboard/users/:userID/games/:game/:playtype/achievables">
|
||||
<AchievablesPage reqUser={reqUser} game={game} playtype={playtype} />
|
||||
</Route>
|
||||
|
||||
@@ -179,7 +179,7 @@ function SessionTab({ data }: { data: Data }) {
|
||||
<tr>
|
||||
<td>
|
||||
<Link
|
||||
to={`/dashboard/games/${r.session.game}/${r.session.playtype}/sessions/${r.session.sessionID}`}
|
||||
to={`/dashboard/users/${r.session.userID}/games/${r.session.game}/${r.session.playtype}/sessions/${r.session.sessionID}`}
|
||||
className="gentle-link"
|
||||
>
|
||||
{r.session.name}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { ResponsiveLine } from "@nivo/line";
|
||||
import ChartTooltip from "components/charts/ChartTooltip";
|
||||
import Card from "components/layout/page/Card";
|
||||
import ScoreTable from "components/tables/scores/ScoreTable";
|
||||
import DebugContent from "components/util/DebugContent";
|
||||
import Divider from "components/util/Divider";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import { ColourConfig } from "lib/config";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import { Col } from "react-bootstrap";
|
||||
import { ChartDocument, GetGamePTConfig, ScoreDocument, SongDocument } from "tachi-common";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
import { GamePT } from "types/react";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
import { TACHI_CHART_THEME } from "util/constants/chart-theme";
|
||||
import { UppercaseFirst } from "util/misc";
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { FormatDuration } from "util/time";
|
||||
|
||||
export default function SessionOverview({
|
||||
sessionData,
|
||||
scoreDataset,
|
||||
}: {
|
||||
sessionData: SessionReturns;
|
||||
scoreDataset: ScoreDataset;
|
||||
}) {
|
||||
const { scores, session } = sessionData;
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
// const performanceDataset = useMemo(() => {
|
||||
// const d = [];
|
||||
|
||||
// for (const scoreInfo of session.scoreInfo) {
|
||||
// const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
// if (!score) {
|
||||
// console.error(`Score ${scoreInfo.scoreID} doesn't exist?`);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// const chart = chartMap.get(score.chartID);
|
||||
// const song = songMap.get(score.songID);
|
||||
|
||||
// if (!chart || !song) {
|
||||
// console.error(`Chart ${score.chartID} (${score.songID}) has no chart?`);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// d.push({
|
||||
// ...score,
|
||||
// __related: {
|
||||
// song,
|
||||
// chart,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// return d;
|
||||
// }, [sessionData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatThing name="Scores" value={session.scoreInfo.length} />
|
||||
<StatThing
|
||||
name="Duration"
|
||||
value={FormatDuration(session.timeEnded - session.timeStarted)}
|
||||
/>
|
||||
<StatThing md12 name="Highlights" value={scores.filter(e => e.highlight).length} />
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
<Card header="Highlights">
|
||||
<ScoreTable
|
||||
game={session.game}
|
||||
playtype={session.playtype}
|
||||
dataset={scoreDataset.filter(e => e.highlight)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StatThing({
|
||||
value,
|
||||
name,
|
||||
md12,
|
||||
}: {
|
||||
value: string | number;
|
||||
name: string;
|
||||
md12?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Col xs={12} md={md12 ? 12 : 6} lg={4}>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="display-4">{value}</div>
|
||||
<div style={{ fontSize: "1.2rem" }}>{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
type PerformanceData = ScoreDocument & {
|
||||
__related: {
|
||||
chart: ChartDocument;
|
||||
song: SongDocument;
|
||||
};
|
||||
};
|
||||
|
||||
function PerformanceComponent({
|
||||
dataset,
|
||||
timeStart,
|
||||
game,
|
||||
playtype,
|
||||
}: {
|
||||
dataset: PerformanceData[];
|
||||
timeStart: number;
|
||||
} & GamePT) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const data = useMemo(() => {
|
||||
const data = [];
|
||||
|
||||
for (const alg of gptConfig.scoreRatingAlgs) {
|
||||
data.push({
|
||||
id: UppercaseFirst(alg),
|
||||
data: dataset
|
||||
.map(e => ({
|
||||
x: e.timeAchieved!,
|
||||
y: (e.calculatedData[alg] ?? 0) < 0 ? null : e.calculatedData[alg] ?? 0,
|
||||
}))
|
||||
.sort(NumericSOV(e => e.x, true)),
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}, [dataset]);
|
||||
|
||||
return (
|
||||
<Card header="Session Graph" style={{ width: "100%" }}>
|
||||
<div style={{ width: "100%", height: "400px" }}>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
margin={{ top: 30, bottom: 50, left: 40, right: 40 }}
|
||||
xScale={{ type: "time", format: "%Q" }}
|
||||
axisBottom={{
|
||||
format: (x: Date) => FormatDuration(timeStart - x.valueOf()),
|
||||
}}
|
||||
gridXValues={3}
|
||||
motionConfig="stiff"
|
||||
crosshairType="x"
|
||||
yScale={{ type: "linear", min: "auto", max: "auto" }}
|
||||
colors={[ColourConfig.primary]}
|
||||
useMesh={true}
|
||||
theme={TACHI_CHART_THEME}
|
||||
tooltip={d => (
|
||||
<ChartTooltip point={d.point} renderFn={r => <DebugContent data={r} />} />
|
||||
)}
|
||||
// legends={[]}
|
||||
enableArea
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
import DifficultyCell from "components/tables/cells/DifficultyCell";
|
||||
import LampCell from "components/tables/cells/LampCell";
|
||||
import ScoreCell from "components/tables/cells/ScoreCell";
|
||||
import TitleCell from "components/tables/cells/TitleCell";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import Loading from "components/util/Loading";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import deepmerge from "deepmerge";
|
||||
import { nanoid } from "nanoid";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import Divider from "components/util/Divider";
|
||||
import Icon from "components/util/Icon";
|
||||
import {
|
||||
FolderDocument,
|
||||
TableDocument,
|
||||
Lamps,
|
||||
IDStrings,
|
||||
ScoreDocument,
|
||||
SessionScoreInfo,
|
||||
Grades,
|
||||
GetGamePTConfig,
|
||||
GamePTConfig,
|
||||
integer,
|
||||
SongDocument,
|
||||
Game,
|
||||
ChartDocument,
|
||||
} from "tachi-common";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { CreateSongMap, CreateChartMap, CreateScoreIDMap } from "util/data";
|
||||
import { PartialArrayRecordAssign } from "util/misc";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
|
||||
export default function SessionRaiseBreakdown({ sessionData }: { sessionData: SessionReturns }) {
|
||||
const game = sessionData.session.game;
|
||||
const playtype = sessionData.session.playtype;
|
||||
|
||||
// todo infer default table
|
||||
// const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const [chartIDs, setChartIDs] = useState<string[] | null>([]);
|
||||
const [tableID, setTableID] = useState<string | null>(null);
|
||||
const [tableFolders, setTableFolders] = useState<FolderDocument[]>([]);
|
||||
const [folder, setFolder] = useState<FolderDocument | null>(null);
|
||||
const [filter, setFilter] = useState<"folder" | "all" | "highlighted">("all");
|
||||
|
||||
useEffect(() => {
|
||||
if (!tableID) {
|
||||
return;
|
||||
}
|
||||
|
||||
APIFetchV1(`/games/${game}/${playtype}/tables/${tableID}`).then(r => {
|
||||
if (!r.success) {
|
||||
throw new Error(r.description);
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
setTableFolders(r.body.folders);
|
||||
// @ts-expect-error todo
|
||||
setFolder(r.body.folders[r.body.folders.length - 1]);
|
||||
});
|
||||
}, [tableID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== "folder" || !folder) {
|
||||
setChartIDs(null);
|
||||
return;
|
||||
}
|
||||
|
||||
APIFetchV1(`/games/${game}/${playtype}/folders/${folder.folderID}`).then(r => {
|
||||
if (!r.success) {
|
||||
throw new Error(r.description);
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
setChartIDs(r.body.charts.map(e => e.chartID));
|
||||
});
|
||||
}, [folder, tableID, filter]);
|
||||
|
||||
const { isLoading, error, data } = useQuery(`/games/${game}/${playtype}/tables`, async () => {
|
||||
const res = await APIFetchV1<TableDocument[]>(`/games/${game}/${playtype}/tables`);
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.description);
|
||||
}
|
||||
|
||||
return res.body;
|
||||
});
|
||||
const [view, setView] = useState<"lamps" | "both" | "grades">("both");
|
||||
|
||||
useEffect(() => {
|
||||
if (tableID === null && (data?.length ?? 0) > 0) {
|
||||
setTableID(data![0].tableID);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
return <>{(error as Error).message}</>;
|
||||
}
|
||||
|
||||
if (isLoading || !data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<div className="row">
|
||||
<div className="col-6 col-lg-6 offset-lg-3">
|
||||
<div className="d-flex justify-content-center">
|
||||
<div className="btn-group">
|
||||
<SelectButton value={view} setValue={setView} id="lamps">
|
||||
<Icon type="lightbulb" />
|
||||
New Lamps
|
||||
</SelectButton>
|
||||
|
||||
<SelectButton value={view} setValue={setView} id="both">
|
||||
<Icon type="bolt" />
|
||||
Both
|
||||
</SelectButton>
|
||||
<SelectButton value={view} setValue={setView} id="grades">
|
||||
<Icon type="sort-alpha-up" />
|
||||
New Grades
|
||||
</SelectButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-6 col-lg-3">
|
||||
<select
|
||||
className="form-control ml-auto"
|
||||
value={filter}
|
||||
onChange={e => setFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">No Filter</option>
|
||||
<option value="folder">Folder Filters</option>
|
||||
<option value="highlighted">Highlights Only</option>
|
||||
</select>
|
||||
</div>
|
||||
{filter === "folder" && (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={e => setTableID(e.target.value)}
|
||||
>
|
||||
{data.map(e => (
|
||||
<option key={e.tableID} value={e.tableID}>
|
||||
{e.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<select
|
||||
className="form-control"
|
||||
value={folder?.folderID}
|
||||
onChange={e =>
|
||||
setFolder(
|
||||
tableFolders.find(f => f.folderID === e.target.value)!
|
||||
)
|
||||
}
|
||||
>
|
||||
{tableFolders.map(e => (
|
||||
<option key={e.folderID} value={e.folderID}>
|
||||
{e.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
<SessionScoreStatBreakdown {...{ sessionData, chartIDs, filter, view }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionScoreStatBreakdown({
|
||||
sessionData,
|
||||
chartIDs,
|
||||
filter,
|
||||
view,
|
||||
}: {
|
||||
sessionData: SessionReturns;
|
||||
chartIDs: string[] | null;
|
||||
filter: "folder" | "all" | "highlighted";
|
||||
view: "lamps" | "both" | "grades";
|
||||
}) {
|
||||
const songMap = CreateSongMap(sessionData.songs);
|
||||
const chartMap = CreateChartMap(sessionData.charts);
|
||||
const scoreMap = CreateScoreIDMap(sessionData.scores);
|
||||
|
||||
const [newLamps, newGrades] = useMemo(() => {
|
||||
const newLamps: Partial<Record<
|
||||
Lamps[IDStrings],
|
||||
{ score: ScoreDocument; scoreInfo: SessionScoreInfo }[]
|
||||
>> = {};
|
||||
const newGrades: Partial<Record<
|
||||
Grades[IDStrings],
|
||||
{ score: ScoreDocument; scoreInfo: SessionScoreInfo }[]
|
||||
>> = {};
|
||||
|
||||
for (const scoreInfo of sessionData.session.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
if (!score) {
|
||||
console.error(
|
||||
`Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter === "folder" && chartIDs && !chartIDs.includes(score.chartID)) {
|
||||
continue;
|
||||
} else if (filter === "highlighted" && !score.highlight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scoreInfo.isNewScore) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, { score, scoreInfo });
|
||||
} else {
|
||||
if (scoreInfo.lampDelta > 0) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
}
|
||||
|
||||
if (scoreInfo.gradeDelta > 0) {
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, {
|
||||
score,
|
||||
scoreInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [newLamps, newGrades];
|
||||
}, [chartIDs, filter, view]);
|
||||
|
||||
const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype);
|
||||
|
||||
return (
|
||||
<>
|
||||
{view === "both" ? (
|
||||
<>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Lamp", "New Lamps"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Grade", "New Grades"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
</>
|
||||
) : view === "grades" ? (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Grade", "New Grades"]} colSpan={[1, 100]}>
|
||||
<ElementStatTable
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
) : (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Lamps", "New Lamps"]} colSpan={[1, 100]}>
|
||||
<ElementStatTable
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ElementStatTable({
|
||||
type,
|
||||
counts,
|
||||
gptConfig,
|
||||
songMap,
|
||||
chartMap,
|
||||
game,
|
||||
fullSize = false,
|
||||
}: {
|
||||
type: "lamp" | "grade";
|
||||
counts: Record<string, { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>;
|
||||
gptConfig: GamePTConfig;
|
||||
songMap: Map<integer, SongDocument<Game>>;
|
||||
chartMap: Map<string, ChartDocument<IDStrings>>;
|
||||
game: Game;
|
||||
fullSize?: boolean;
|
||||
}) {
|
||||
const tableContents = useMemo(() => {
|
||||
// relements.. haha
|
||||
const relevantElements =
|
||||
type === "lamp"
|
||||
? gptConfig.lamps.slice(gptConfig.lamps.indexOf(gptConfig.clearLamp) - 1).reverse()
|
||||
: gptConfig.grades
|
||||
.slice(gptConfig.grades.indexOf(gptConfig.clearGrade) - 1)
|
||||
.reverse();
|
||||
|
||||
const colours = type === "lamp" ? gptConfig.lampColours : gptConfig.gradeColours;
|
||||
|
||||
const tableContents = [];
|
||||
for (const element of relevantElements) {
|
||||
if (!counts[element] || !counts[element].length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstData = counts[element][0];
|
||||
|
||||
tableContents.push(
|
||||
<tr key={element}>
|
||||
<td
|
||||
style={{
|
||||
// @ts-expect-error this is a hack due to the funky type of colours and element.
|
||||
backgroundColor: ChangeOpacity(colours[element], 0.1),
|
||||
}}
|
||||
rowSpan={counts[element]!.length}
|
||||
>
|
||||
{element}
|
||||
</td>
|
||||
<BreakdownChartContents
|
||||
{...firstData}
|
||||
{...{ chartMap, songMap, fullSize, game, gptConfig, type }}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
|
||||
for (const data of counts[element]!.slice(1)) {
|
||||
tableContents.push(
|
||||
<tr key={nanoid()}>
|
||||
<BreakdownChartContents
|
||||
{...data}
|
||||
{...{ chartMap, songMap, fullSize, game, gptConfig, type }}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tableContents;
|
||||
}, [type, counts, fullSize, game]);
|
||||
|
||||
if (tableContents.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={3}>Nothing...</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{tableContents}</>;
|
||||
}
|
||||
|
||||
function BreakdownChartContents({
|
||||
score,
|
||||
scoreInfo,
|
||||
game,
|
||||
songMap,
|
||||
chartMap,
|
||||
fullSize,
|
||||
gptConfig,
|
||||
type,
|
||||
}: {
|
||||
score: ScoreDocument;
|
||||
scoreInfo: SessionScoreInfo;
|
||||
fullSize: boolean;
|
||||
game: Game;
|
||||
songMap: Map<integer, SongDocument>;
|
||||
chartMap: Map<string, ChartDocument>;
|
||||
gptConfig: GamePTConfig;
|
||||
type: "lamp" | "grade";
|
||||
}) {
|
||||
const chart = chartMap.get(score.chartID)!;
|
||||
const song = songMap.get(score.songID)!;
|
||||
|
||||
if (!chart || !song) {
|
||||
console.error(`No chart for ${score.chartID}/${score.songID}???`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fullSize) {
|
||||
let preScoreCell = <td>No Play</td>;
|
||||
|
||||
if (!scoreInfo.isNewScore) {
|
||||
const oldGradeIndex = score.scoreData.gradeIndex - scoreInfo.gradeDelta;
|
||||
const oldLampIndex = score.scoreData.lampIndex - scoreInfo.lampDelta;
|
||||
|
||||
const mockScore = deepmerge(score, {
|
||||
scoreData: {
|
||||
score: score.scoreData.score - scoreInfo.scoreDelta,
|
||||
percent: score.scoreData.percent - scoreInfo.percentDelta,
|
||||
grade: gptConfig.grades[oldGradeIndex],
|
||||
gradeIndex: oldGradeIndex,
|
||||
lamp: gptConfig.lamps[oldLampIndex],
|
||||
lampIndex: oldLampIndex,
|
||||
},
|
||||
}) as ScoreDocument;
|
||||
|
||||
if (type === "grade") {
|
||||
preScoreCell = <ScoreCell score={mockScore} />;
|
||||
} else {
|
||||
preScoreCell = <LampCell score={mockScore} />;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(game);
|
||||
|
||||
if (score) {
|
||||
return (
|
||||
<>
|
||||
<TitleCell chart={chart} game={game} song={song} />
|
||||
<DifficultyCell chart={chart} game={game} />
|
||||
{preScoreCell}
|
||||
<td>⟶</td>
|
||||
{type === "grade" ? (
|
||||
<ScoreCell {...{ score, game, playtype: score.playtype }} />
|
||||
) : (
|
||||
<LampCell score={score} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleCell noArtist chart={chart} game={game} song={song} />
|
||||
<DifficultyCell alwaysShort chart={chart} game={game} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,13 @@
|
||||
import SessionRaiseBreakdown from "components/sessions/SessionRaiseBreakdown";
|
||||
import DebugContent from "components/util/DebugContent";
|
||||
import Divider from "components/util/Divider";
|
||||
import HasDevModeOn from "components/util/HasDevModeOn";
|
||||
import Icon from "components/util/Icon";
|
||||
import Loading from "components/util/Loading";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import deepmerge from "deepmerge";
|
||||
import { nanoid } from "nanoid";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import {
|
||||
ChartDocument,
|
||||
FolderDocument,
|
||||
Game,
|
||||
GamePTConfig,
|
||||
GetGamePTConfig,
|
||||
Grades,
|
||||
IDStrings,
|
||||
integer,
|
||||
Lamps,
|
||||
ScoreDocument,
|
||||
SessionScoreInfo,
|
||||
SongDocument,
|
||||
TableDocument,
|
||||
} from "tachi-common";
|
||||
import { SpecificSessionReturns } from "types/api-returns";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data";
|
||||
import { PartialArrayRecordAssign } from "util/misc";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import ScoreCell from "../cells/ScoreCell";
|
||||
import TitleCell from "../cells/TitleCell";
|
||||
import MiniTable from "../components/MiniTable";
|
||||
import { SessionDataset } from "../sessions/GenericSessionTable";
|
||||
import DropdownStructure from "./components/DropdownStructure";
|
||||
|
||||
@@ -40,7 +15,7 @@ export default function GenericSessionDropdown({ data }: { data: SessionDataset[
|
||||
const { isLoading, error, data: sessionData } = useQuery(
|
||||
`/sessions/${data.sessionID}`,
|
||||
async () => {
|
||||
const res = await APIFetchV1<SpecificSessionReturns>(`/sessions/${data.sessionID}`);
|
||||
const res = await APIFetchV1<SessionReturns>(`/sessions/${data.sessionID}`);
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.description);
|
||||
@@ -89,448 +64,3 @@ export default function GenericSessionDropdown({ data }: { data: SessionDataset[
|
||||
</DropdownStructure>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionRaiseBreakdown({ sessionData }: { sessionData: SpecificSessionReturns }) {
|
||||
const game = sessionData.session.game;
|
||||
const playtype = sessionData.session.playtype;
|
||||
|
||||
// todo infer default table
|
||||
// const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const [chartIDs, setChartIDs] = useState<string[] | null>([]);
|
||||
const [tableID, setTableID] = useState<string | null>(null);
|
||||
const [tableFolders, setTableFolders] = useState<FolderDocument[]>([]);
|
||||
const [folder, setFolder] = useState<FolderDocument | null>(null);
|
||||
const [filter, setFilter] = useState<"folder" | "all" | "highlighted">("all");
|
||||
|
||||
useEffect(() => {
|
||||
if (!tableID) {
|
||||
return;
|
||||
}
|
||||
|
||||
APIFetchV1(`/games/${game}/${playtype}/tables/${tableID}`).then(r => {
|
||||
if (!r.success) {
|
||||
throw new Error(r.description);
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
setTableFolders(r.body.folders);
|
||||
// @ts-expect-error todo
|
||||
setFolder(r.body.folders[r.body.folders.length - 1]);
|
||||
});
|
||||
}, [tableID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== "folder" || !folder) {
|
||||
setChartIDs(null);
|
||||
return;
|
||||
}
|
||||
|
||||
APIFetchV1(`/games/${game}/${playtype}/folders/${folder.folderID}`).then(r => {
|
||||
if (!r.success) {
|
||||
throw new Error(r.description);
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
setChartIDs(r.body.charts.map(e => e.chartID));
|
||||
});
|
||||
}, [folder, tableID, filter]);
|
||||
|
||||
const { isLoading, error, data } = useQuery(`/games/${game}/${playtype}/tables`, async () => {
|
||||
const res = await APIFetchV1<TableDocument[]>(`/games/${game}/${playtype}/tables`);
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.description);
|
||||
}
|
||||
|
||||
return res.body;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (tableID === null && (data?.length ?? 0) > 0) {
|
||||
setTableID(data![0].tableID);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
return <>{(error as Error).message}</>;
|
||||
}
|
||||
|
||||
if (isLoading || !data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="btn-group">
|
||||
<SelectButton value={filter} setValue={setFilter} id="folder">
|
||||
<Icon type="folder-open" />
|
||||
Folder Filter
|
||||
</SelectButton>
|
||||
<SelectButton value={filter} setValue={setFilter} id="all">
|
||||
<Icon type="plus" />
|
||||
No Filter
|
||||
</SelectButton>
|
||||
<SelectButton value={filter} setValue={setFilter} id="highlighted">
|
||||
<Icon type="star" />
|
||||
Highlights Only
|
||||
</SelectButton>
|
||||
</div>
|
||||
</div>
|
||||
{filter === "folder" && (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={e => setTableID(e.target.value)}
|
||||
>
|
||||
{data.map(e => (
|
||||
<option key={e.tableID} value={e.tableID}>
|
||||
{e.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<select
|
||||
className="form-control"
|
||||
value={folder?.folderID}
|
||||
onChange={e =>
|
||||
setFolder(
|
||||
tableFolders.find(f => f.folderID === e.target.value)!
|
||||
)
|
||||
}
|
||||
>
|
||||
{tableFolders.map(e => (
|
||||
<option key={e.folderID} value={e.folderID}>
|
||||
{e.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
<SessionScoreStatBreakdown {...{ sessionData, chartIDs, filter }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionScoreStatBreakdown({
|
||||
sessionData,
|
||||
chartIDs,
|
||||
filter,
|
||||
}: {
|
||||
sessionData: SpecificSessionReturns;
|
||||
chartIDs: string[] | null;
|
||||
filter: "folder" | "all" | "highlighted";
|
||||
}) {
|
||||
const [view, setView] = useState<"lamps" | "both" | "grades">("both");
|
||||
|
||||
const songMap = CreateSongMap(sessionData.songs);
|
||||
const chartMap = CreateChartMap(sessionData.charts);
|
||||
const scoreMap = CreateScoreIDMap(sessionData.scores);
|
||||
|
||||
const [newLamps, newGrades] = useMemo(() => {
|
||||
const newLamps: Partial<Record<
|
||||
Lamps[IDStrings],
|
||||
{ score: ScoreDocument; scoreInfo: SessionScoreInfo }[]
|
||||
>> = {};
|
||||
const newGrades: Partial<Record<
|
||||
Grades[IDStrings],
|
||||
{ score: ScoreDocument; scoreInfo: SessionScoreInfo }[]
|
||||
>> = {};
|
||||
|
||||
for (const scoreInfo of sessionData.session.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
if (!score) {
|
||||
console.error(
|
||||
`Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter === "folder" && chartIDs && !chartIDs.includes(score.chartID)) {
|
||||
continue;
|
||||
} else if (filter === "highlighted" && !score.highlight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scoreInfo.isNewScore) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, { score, scoreInfo });
|
||||
} else {
|
||||
if (scoreInfo.lampDelta > 0) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
}
|
||||
|
||||
if (scoreInfo.gradeDelta > 0) {
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, {
|
||||
score,
|
||||
scoreInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [newLamps, newGrades];
|
||||
}, [chartIDs, filter, view]);
|
||||
|
||||
const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-12 d-none d-lg-block">
|
||||
<div className="btn-group">
|
||||
<SelectButton value={view} setValue={setView} id="lamps">
|
||||
<Icon type="lightbulb" />
|
||||
New Lamps
|
||||
</SelectButton>
|
||||
|
||||
<SelectButton value={view} setValue={setView} id="both">
|
||||
<Icon type="bolt" />
|
||||
Both
|
||||
</SelectButton>
|
||||
<SelectButton value={view} setValue={setView} id="grades">
|
||||
<Icon type="sort-alpha-up" />
|
||||
New Grades
|
||||
</SelectButton>
|
||||
</div>
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
{view === "both" ? (
|
||||
<>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Lamp", "New Lamps"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Grade", "New Grades"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
</>
|
||||
) : view === "grades" ? (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Grade", "New Grades"]} colSpan={[1, 100]}>
|
||||
<ElementStatTable
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
) : (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Lamps", "New Lamps"]} colSpan={[1, 100]}>
|
||||
<ElementStatTable
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ElementStatTable({
|
||||
type,
|
||||
counts,
|
||||
gptConfig,
|
||||
songMap,
|
||||
chartMap,
|
||||
game,
|
||||
fullSize = false,
|
||||
}: {
|
||||
type: "lamp" | "grade";
|
||||
counts: Record<string, { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>;
|
||||
gptConfig: GamePTConfig;
|
||||
songMap: Map<integer, SongDocument<Game>>;
|
||||
chartMap: Map<string, ChartDocument<IDStrings>>;
|
||||
game: Game;
|
||||
fullSize?: boolean;
|
||||
}) {
|
||||
const tableContents = useMemo(() => {
|
||||
// relements.. haha
|
||||
const relevantElements =
|
||||
type === "lamp"
|
||||
? gptConfig.lamps.slice(gptConfig.lamps.indexOf(gptConfig.clearLamp) - 1).reverse()
|
||||
: gptConfig.grades
|
||||
.slice(gptConfig.grades.indexOf(gptConfig.clearGrade) - 1)
|
||||
.reverse();
|
||||
|
||||
const colours = type === "lamp" ? gptConfig.lampColours : gptConfig.gradeColours;
|
||||
|
||||
const tableContents = [];
|
||||
for (const element of relevantElements) {
|
||||
if (!counts[element] || !counts[element].length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstData = counts[element][0];
|
||||
|
||||
tableContents.push(
|
||||
<tr key={element}>
|
||||
<td
|
||||
style={{
|
||||
// @ts-expect-error this is a hack due to the funky type of colours and element.
|
||||
backgroundColor: ChangeOpacity(colours[element], 0.1),
|
||||
}}
|
||||
rowSpan={counts[element]!.length}
|
||||
>
|
||||
{element}
|
||||
</td>
|
||||
<BreakdownChartContents
|
||||
{...firstData}
|
||||
{...{ chartMap, songMap, fullSize, game, gptConfig, type }}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
|
||||
for (const data of counts[element]!.slice(1)) {
|
||||
tableContents.push(
|
||||
<tr key={nanoid()}>
|
||||
<BreakdownChartContents
|
||||
{...data}
|
||||
{...{ chartMap, songMap, fullSize, game, gptConfig, type }}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tableContents;
|
||||
}, [type, counts, fullSize, game]);
|
||||
|
||||
if (tableContents.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={3}>Nothing...</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{tableContents}</>;
|
||||
}
|
||||
|
||||
function BreakdownChartContents({
|
||||
score,
|
||||
scoreInfo,
|
||||
game,
|
||||
songMap,
|
||||
chartMap,
|
||||
fullSize,
|
||||
gptConfig,
|
||||
type,
|
||||
}: {
|
||||
score: ScoreDocument;
|
||||
scoreInfo: SessionScoreInfo;
|
||||
fullSize: boolean;
|
||||
game: Game;
|
||||
songMap: Map<integer, SongDocument>;
|
||||
chartMap: Map<string, ChartDocument>;
|
||||
gptConfig: GamePTConfig;
|
||||
type: "lamp" | "grade";
|
||||
}) {
|
||||
const chart = chartMap.get(score.chartID)!;
|
||||
const song = songMap.get(score.songID)!;
|
||||
|
||||
if (!chart || !song) {
|
||||
console.error(`No chart for ${score.chartID}/${score.songID}???`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fullSize) {
|
||||
let preScoreCell = <td>No Play</td>;
|
||||
|
||||
if (!scoreInfo.isNewScore) {
|
||||
const oldGradeIndex = score.scoreData.gradeIndex - scoreInfo.gradeDelta;
|
||||
const oldLampIndex = score.scoreData.lampIndex - scoreInfo.lampDelta;
|
||||
|
||||
const mockScore = deepmerge(score, {
|
||||
scoreData: {
|
||||
score: score.scoreData.score - scoreInfo.scoreDelta,
|
||||
percent: score.scoreData.percent - scoreInfo.percentDelta,
|
||||
grade: gptConfig.grades[oldGradeIndex],
|
||||
gradeIndex: oldGradeIndex,
|
||||
lamp: gptConfig.lamps[oldLampIndex],
|
||||
lampIndex: oldLampIndex,
|
||||
},
|
||||
}) as ScoreDocument;
|
||||
|
||||
if (type === "grade") {
|
||||
preScoreCell = <ScoreCell score={mockScore} />;
|
||||
} else {
|
||||
preScoreCell = <LampCell score={mockScore} />;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(game);
|
||||
|
||||
if (score) {
|
||||
return (
|
||||
<>
|
||||
<TitleCell chart={chart} game={game} song={song} />
|
||||
<DifficultyCell chart={chart} game={game} />
|
||||
{preScoreCell}
|
||||
<td>⟶</td>
|
||||
{type === "grade" ? (
|
||||
<ScoreCell {...{ score, game, playtype: score.playtype }} />
|
||||
) : (
|
||||
<LampCell score={score} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleCell noArtist chart={chart} game={game} song={song} />
|
||||
<DifficultyCell alwaysShort chart={chart} game={game} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useSessionRatingAlg } from "components/util/useScoreRatingAlg";
|
||||
import { nanoid } from "nanoid";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
integer,
|
||||
PublicUserDocument,
|
||||
SessionCalculatedDataLookup,
|
||||
SessionDocument,
|
||||
} from "tachi-common";
|
||||
@@ -25,11 +27,13 @@ export type SessionDataset = (SessionDocument & { __related: { index: integer }
|
||||
export default function GenericSessionTable({
|
||||
dataset,
|
||||
indexCol = false,
|
||||
reqUser,
|
||||
game,
|
||||
playtype,
|
||||
}: {
|
||||
dataset: SessionDataset;
|
||||
indexCol?: boolean;
|
||||
reqUser: PublicUserDocument;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
}) {
|
||||
@@ -84,7 +88,15 @@ export default function GenericSessionTable({
|
||||
timestamp: x => x.timeStarted,
|
||||
[alg]: x => x.calculatedData[alg] ?? 0,
|
||||
}}
|
||||
rowFunction={s => <Row data={s} key={s.sessionID} rating={alg} indexCol={indexCol} />}
|
||||
rowFunction={s => (
|
||||
<Row
|
||||
reqUser={reqUser}
|
||||
data={s}
|
||||
key={s.sessionID}
|
||||
rating={alg}
|
||||
indexCol={indexCol}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -92,12 +104,14 @@ export default function GenericSessionTable({
|
||||
function Row({
|
||||
data,
|
||||
rating,
|
||||
reqUser,
|
||||
indexCol = false,
|
||||
}: // reqUser,
|
||||
{
|
||||
data: SessionDataset[0];
|
||||
// reqUser: PublicUserDocument;
|
||||
rating: SessionCalculatedDataLookup[IDStrings];
|
||||
reqUser: PublicUserDocument;
|
||||
indexCol?: boolean;
|
||||
}) {
|
||||
const [highlight, setHighlight] = useState(data.highlight);
|
||||
@@ -106,13 +120,18 @@ function Row({
|
||||
const sessionState = { highlight, desc, setHighlight, setDesc };
|
||||
|
||||
return (
|
||||
<DropdownRow
|
||||
<tr
|
||||
className={highlight ? "highlighted-row" : ""}
|
||||
dropdown={<GenericSessionDropdown data={data} />}
|
||||
// dropdown={<GenericSessionDropdown data={data} />}
|
||||
>
|
||||
{indexCol && <IndexCell index={data.__related.index} />}
|
||||
<td style={{ minWidth: "140px" }}>
|
||||
{data.name}
|
||||
<Link
|
||||
to={`/dashboard/users/${reqUser.username}/games/${data.game}/${data.playtype}/sessions/${data.sessionID}`}
|
||||
className="gentle-link"
|
||||
>
|
||||
{data.name}
|
||||
</Link>
|
||||
<br />
|
||||
<small className="text-muted">{desc}</small>
|
||||
</td>
|
||||
@@ -130,6 +149,6 @@ function Row({
|
||||
<br />
|
||||
<small className="text-muted">{FormatTime(data.timeStarted)}</small>
|
||||
</td>
|
||||
</DropdownRow>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { CSSProperties } from "react";
|
||||
|
||||
export default function Icon({
|
||||
type,
|
||||
@@ -6,12 +6,14 @@ export default function Icon({
|
||||
brand,
|
||||
onClick,
|
||||
colour,
|
||||
style,
|
||||
}: {
|
||||
type: string;
|
||||
noPad?: boolean;
|
||||
brand?: boolean;
|
||||
onClick?: () => void;
|
||||
colour?: "info" | "primary" | "danger" | "warning";
|
||||
style?: CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<i
|
||||
@@ -19,6 +21,7 @@ export default function Icon({
|
||||
className={`fa${brand ? "b" : "s"} fa-${type} ${noPad ? "p-0" : ""} ${
|
||||
colour ? `text-${colour}` : ""
|
||||
}`}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,14 +82,6 @@ export interface UGPTChartPBComposition<I extends IDStrings = IDStrings> {
|
||||
pb: PBScoreDocument<I>;
|
||||
}
|
||||
|
||||
export interface SpecificSessionReturns<I extends IDStrings = IDStrings> {
|
||||
session: SessionDocument<I>;
|
||||
scores: ScoreDocument<I>[];
|
||||
songs: SongDocument<IDStringToGame[I]>[];
|
||||
charts: ChartDocument<I>[];
|
||||
user: PublicUserDocument;
|
||||
}
|
||||
|
||||
export type UGSWithRankingData<I extends IDStrings = IDStrings> = UserGameStats<I> & {
|
||||
__rankingData: Record<UGSRatingsLookup[I], { outOf: number; ranking: number }>;
|
||||
};
|
||||
|
||||
@@ -90,6 +90,7 @@ export const loggedInSplashes = [
|
||||
"Discovered Colours",
|
||||
"Meeting People Is Easy",
|
||||
"See ya later, innovator",
|
||||
"bou bou bounce",
|
||||
];
|
||||
|
||||
// Stuff that's bad for the kids, you know?
|
||||
|
||||
Reference in New Issue
Block a user