feat: things (#1098)

* feat: clean up this disgusting filter directive thing

* fix: THIS FIX WAS THIS EASY FOR THIS LONG???

* fix: version is obsolete

* feat: much prettier...

* feat: big import warnings

* feat: sour times
This commit is contained in:
zkldi
2024-06-05 01:49:51 +01:00
committed by GitHub
parent e6df663872
commit 2ebe9f9544
20 changed files with 427 additions and 297 deletions
+1 -12
View File
@@ -2,23 +2,12 @@ const MAJOR = 2;
const MINOR = 2;
const PATCH = 0;
const TOKYO_SHOEGAZER_MOONDIVER = [
"Into the Deep Sky",
"Your Relief",
"In Full Bloom",
"Night Dance",
"Bedtime Story",
"Moondive",
"Stay White",
];
export const VERSION_INFO = {
major: MAJOR,
minor: MINOR,
patch: PATCH,
name: TOKYO_SHOEGAZER_MOONDIVER[MINOR],
};
export const VERSION_STR = `${MAJOR}.${MINOR}.${PATCH}`;
export const VERSION_PRETTY = `v${VERSION_STR} (${VERSION_INFO.name})`;
export const VERSION_PRETTY = `v${VERSION_STR}`;
@@ -23,6 +23,8 @@ import {
UserDocument,
} from "tachi-common";
import Col from "react-bootstrap/Col";
import { Alert } from "react-bootstrap";
import Icon from "components/util/Icon";
export default function ImportPage({ user }: { user: UserDocument }) {
useSetSubheader(["Import Scores"]);
@@ -369,6 +371,11 @@ function ImportInfoDisplayer({ game }: { game: Game }) {
<div className="text-center mb-4">
<h1>{gameConfig.name}</h1>
</div>
<Row xs={{ cols: 1 }}>
<Col xs={12}>
<InputAlert game={game} />
</Col>
</Row>
<Row xs={{ cols: 1 }} lg={{ cols: 2 }}>
{Content}
</Row>
@@ -376,6 +383,84 @@ function ImportInfoDisplayer({ game }: { game: Game }) {
);
}
function InputAlert({ game }: { game: Game }) {
function doit(game: Game): JSX.Element | null {
switch (game) {
case "jubeat":
case "maimai":
case "iidx":
case "museca":
case "chunithm":
case "gitadora":
case "maimaidx":
case "popn":
case "wacca":
case "ongeki":
return (
<>
<strong>Scores must be achieved on an arcade-size controller!</strong>
<br />
Playing on other input devices (like a keyboard) will get you in trouble.
</>
);
case "sdvx":
return (
<>
<strong>Scores must be achieved on an arcade-size controller!</strong>
<br />
Playing on other input devices (like a keyboard) will get you in trouble.
<br />
<strong>
A Pocket-Voltex DOES NOT count as an <i>ARCADE SIZE</i> controller!
</strong>
</>
);
case "usc":
return (
<>
<strong>Please use the right leaderboard for your controller!</strong>
<br />
<br />
<strong>Arcade-sized controllers</strong> should go on the controller
leaderboards.
<br />
<strong>Keyboards and anything else</strong> go on the Keyboard/Other
leaderboards.
<br />
<br />
<strong>
A Pocket-Voltex DOES NOT count as an <i>ARCADE SIZE</i> controller!
</strong>
</>
);
case "itg":
case "arcaea":
case "bms":
case "pms":
return null;
}
}
const ct = doit(game);
if (!ct) {
return <></>;
}
return (
<Alert variant="warning">
<div className="d-flex" style={{ alignItems: "center", gap: "1rem", fontSize: "2rem" }}>
<div style={{ fontSize: "4rem" }}>
<Icon type="exclamation-triangle" />
</div>
<div className="text-body">{ct}</div>
</div>
</Alert>
);
}
function ImportTypeInfoCard({
importType,
}: {
@@ -33,10 +33,17 @@ export default function USCIRPage() {
Fill out IR Base URL with <code>{ToServerURL("/ir/usc/Keyboard")}</code>.
</li>
<Alert variant="danger" className="mt-4">
You <b>MUST</b> select the right IR! These leaderboards are separated because it
is unfair to compare the two input devices.
You <b>MUST</b> select the right IR! These leaderboards are separated because
it's unfair to compare controller and keyboard scores.
<br />
<b>DELIBERATELY SELECTING THE WRONG IR WILL RESULT IN A PERMANENT BAN!</b>
Ignoring this will get you in trouble, please don't!
<br />
<br />
<strong>Arcade-sized controllers</strong> should go on the controller
leaderboards.
<br />
<strong>Keyboards and anything else</strong> go on the Keyboard/Other
leaderboards.
</Alert>
<Divider />
<li>
@@ -46,12 +53,6 @@ export default function USCIRPage() {
</li>
<li>That's it! Your scores will now automatically upload to the server.</li>
</ol>
<Divider />
<Muted>
Note: If you submit a score on a chart that {TachiConfig.NAME} doesn't recognise,
you'll need to wait until atleast 2 other players submit scores for that chart
before it'll show up. This is to combat accidental IR spam.
</Muted>
</div>
);
}
@@ -3,7 +3,6 @@ import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data";
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import SessionOverview from "components/sessions/SessionOverview";
import ScoreTable from "components/tables/scores/ScoreTable";
import ApiError from "components/util/ApiError";
import DebugContent from "components/util/DebugContent";
import Divider from "components/util/Divider";
@@ -250,27 +249,8 @@ function SessionPage({ data, game, playtype }: UGPT & { data: SessionReturns })
scoreDataset={scoreDataset}
sessionData={sessionData}
setSessionData={setSessionData}
reqUser={data.user}
/>
<Col xs={12}>
<Divider />
<ScoreTable
dataset={scoreDataset}
game={session.game}
playtype={session.playtype}
onScoreUpdate={(sc) => {
const newScores = [
...sessionData.scores.filter((e) => e.scoreID !== sc.scoreID),
sc,
];
setSessionData({
...sessionData,
scores: newScores,
});
}}
/>
</Col>
{settings?.preferences.developerMode && (
<Col xs={12}>
<Divider />
@@ -7,7 +7,6 @@ import DropdownIndicatorCell from "components/tables/cells/DropdownIndicatorCell
import DropdownRow from "components/tables/components/DropdownRow";
import MiniTable from "components/tables/components/MiniTable";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Loading from "components/util/Loading";
import Muted from "components/util/Muted";
import useApiQuery from "components/util/query/useApiQuery";
@@ -48,7 +47,6 @@ export default function SessionFolderRaiseBreakdown({
const gptConfig = GetGamePTConfig(game, playtype);
const [selectedTable, setSelectedTable] = useState<"LOADING" | null | TableDocument>("LOADING");
const [shouldLimit, setShouldLimit] = useState(true);
const { data, error } = useApiQuery<Array<SessionFolderRaises>>(
`/sessions/${sessionData.session.sessionID}/folder-raises`
@@ -139,16 +137,14 @@ export default function SessionFolderRaiseBreakdown({
}
return (
<Col xs={12}>
<Divider />
<>
<h1 className="w-100 text-center">Folder Raises</h1>
{filteredTables.length > 1 && (
{filteredTables.length > 1 && selectedTable !== null && (
<div className="w-100">
<Select
name="Table"
value={selectedTable?.tableID ?? null}
value={selectedTable.tableID}
setValue={(t) => setSelectedTable(tableData.find((e) => e.tableID === t)!)}
>
{filteredTables.map((e) => (
@@ -161,7 +157,7 @@ export default function SessionFolderRaiseBreakdown({
)}
<Row>
{folders.slice(0, shouldLimit ? 6 : Infinity).map((folder, i) => (
{folders.map((folder, i) => (
<Col key={i} xs={12} lg={6} xl={4}>
<Card
className="my-4"
@@ -195,18 +191,7 @@ export default function SessionFolderRaiseBreakdown({
</Col>
))}
</Row>
{shouldLimit && folders.length > 6 && (
<Row>
<div
className="w-100 text-center text-hover-primary"
onClick={() => setShouldLimit(false)}
>
{folders.length - 6} {folders.length > 6 + 1 ? "folders" : "folder"} hidden.
View more?
</div>
</Row>
)}
</Col>
</>
);
}
@@ -3,25 +3,44 @@ import { FormatDuration } from "util/time";
import Card from "components/layout/page/Card";
import ScoreTable from "components/tables/scores/ScoreTable";
import Divider from "components/util/Divider";
import React from "react";
import { Col, Row } from "react-bootstrap";
import { AnySessionRatingAlg, GetGamePTConfig, ScoreDocument, SessionDocument } from "tachi-common";
import React, { useEffect, useState } from "react";
import { Badge, Col, Row } from "react-bootstrap";
import {
AnySessionRatingAlg,
GetGamePTConfig,
GetGPTString,
PBScoreDocument,
ScoreDocument,
SessionDocument,
UserDocument,
} from "tachi-common";
import { SessionReturns } from "types/api-returns";
import { ScoreDataset } from "types/tables";
import { SetState } from "types/react";
import SessionRaiseBreakdown from "./SessionRaiseBreakdown";
import SelectLinkButton from "components/util/SelectLinkButton";
import Icon from "components/util/Icon";
import useUGPTBase from "components/util/useUGPTBase";
import { Route, Switch } from "react-router-dom";
import useApiQuery from "components/util/query/useApiQuery";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import SessionFolderRaiseBreakdown from "./SessionFolderRaiseBreakdown";
import SessionRaiseBreakdown from "./SessionRaiseBreakdown";
export default function SessionOverview({
sessionData,
setSessionData,
scoreDataset,
reqUser,
}: {
sessionData: SessionReturns;
setSessionData: SetState<SessionReturns>;
scoreDataset: ScoreDataset;
reqUser: UserDocument;
}) {
const { scores, session } = sessionData;
const gptConfig = GetGamePTConfig(session.game, session.playtype);
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(session.game, session.playtype)];
const MAX_SCORES = gptImpl.sessionImportantScoreCount;
const setScores = (scores: ScoreDocument[]) => {
setSessionData({
@@ -30,6 +49,41 @@ export default function SessionOverview({
});
};
const [importantScores, setImportantScores] = useState<null | ScoreDataset>(null);
const { data } = useApiQuery<Array<{ pbs: Array<PBScoreDocument> }>>(
Object.keys(gptConfig.profileRatingAlgs).map(
(e) =>
`/users/${reqUser.id}/games/${session.game}/${session.playtype}/pbs/best?alg=${e}`
)
);
useEffect(() => {
if (!data) {
setImportantScores(null);
return;
}
const important: Array<string> = [];
for (const d of data) {
for (const pb of d.pbs.slice(0, MAX_SCORES)) {
const didFind = pb.composedFrom.find((e) => session.scoreIDs.includes(e.scoreID));
if (didFind) {
important.push(didFind.scoreID);
}
}
}
setImportantScores(scoreDataset.filter((e) => important.includes(e.scoreID)));
}, [data]);
const base = useUGPTBase({ game: session.game, playtype: session.playtype, reqUser });
const baseUrl = `${base}/sessions/${session.sessionID}`;
const highlightedScores = scoreDataset.filter((e) => e.highlight);
return (
<>
<Row xs={{ cols: 1 }} lg={{ cols: 3 }} className="p-0 row-gap-4">
@@ -40,28 +94,107 @@ export default function SessionOverview({
/>
<StatThing name="Highlights" value={scores.filter((e) => e.highlight).length} />
</Row>
<SessionFolderRaiseBreakdown sessionData={sessionData} />
<Col xs={12}>
<div className="p-4">
<RatingsOverview session={session} />
</div>
<Row xs={12}>
<Divider />
<Card header="Raise Breakdown">
<Row>
<SessionRaiseBreakdown sessionData={sessionData} setScores={setScores} />
</Row>
</Card>
</Col>
<Col className="text-center">
<div className="btn-group d-flex justify-content-center mb-4">
<SelectLinkButton to={`${baseUrl}`}>
<Icon type="chart-line" /> Raises
</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/folders`}>
<Icon type="sort-numeric-up-alt" /> Folder Stats
</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/important`}>
<Icon type="star" /> Important Scores{" "}
{importantScores && importantScores.length > 0 && (
<Badge style={{ marginLeft: "5px" }} bg="primary">
{importantScores.length} new top {MAX_SCORES}
{importantScores.length === 1 ? "" : "s"}!
</Badge>
)}
</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/scores`}>
<Icon type="database" /> All Scores
</SelectLinkButton>
</div>
</Col>
</Row>
<Col xs={12}>
<Divider />
<Card header="Highlights">
<ScoreTable
game={session.game}
playtype={session.playtype}
dataset={scoreDataset.filter((e) => e.highlight)}
/>
</Card>
</Col>
<RatingsOverview session={session} />
<Col xs={12}>
<Switch>
<Route exact path="/u/:userID/games/:game/:playtype/sessions/:sessionID">
<Card header="Raise Breakdown">
<Row>
<SessionRaiseBreakdown
sessionData={sessionData}
setScores={setScores}
/>
</Row>
</Card>
</Route>
<Route
exact
path="/u/:userID/games/:game/:playtype/sessions/:sessionID/folders"
>
<SessionFolderRaiseBreakdown sessionData={sessionData} />
</Route>
<Route
exact
path="/u/:userID/games/:game/:playtype/sessions/:sessionID/important"
>
{importantScores && importantScores.length > 0 && (
<Card header={`Scores in your top ${MAX_SCORES}!`}>
<ScoreTable
game={session.game}
playtype={session.playtype}
dataset={importantScores}
/>
</Card>
)}
{importantScores &&
importantScores?.length > 0 &&
highlightedScores.length > 0 && <Divider />}
<Card header="Scores you've highlighted">
<ScoreTable
game={session.game}
playtype={session.playtype}
dataset={highlightedScores}
/>
</Card>
</Route>
<Route exact path="/u/:userID/games/:game/:playtype/sessions/:sessionID/scores">
<ScoreTable
dataset={scoreDataset}
game={session.game}
playtype={session.playtype}
onScoreUpdate={(sc) => {
const newScores = [
...sessionData.scores.filter((e) => e.scoreID !== sc.scoreID),
sc,
];
setSessionData({
...sessionData,
scores: newScores,
});
}}
/>
</Route>
</Switch>
</Col>
</>
);
}
@@ -71,27 +204,34 @@ export default function SessionOverview({
function RatingsOverview({ session }: { session: SessionDocument }) {
const gptConfig = GetGamePTConfig(session.game, session.playtype);
return (
<Col xs={12}>
<Divider />
<Card header="Ratings">
<div className="d-flex text-center" style={{ justifyContent: "space-evenly" }}>
{Object.keys(gptConfig.sessionRatingAlgs).map((e) => (
<div key={e}>
<div className="display-4">{UppercaseFirst(e)}</div>
<div style={{ fontSize: "1.2rem" }}>
{FormatSessionRating(
session.game,
session.playtype,
e as AnySessionRatingAlg,
session.calculatedData[e as AnySessionRatingAlg]
)}
</div>
</div>
))}
function Thing({ value, name }: { value: string | number; name: string }) {
return (
<div className="d-flex" style={{ flexGrow: 1 }}>
<div className="card" style={{ flexGrow: 1 }}>
<div className="card-body">
<div className="display-4">{value}</div>
<div style={{ fontSize: "1.2rem" }}>{name}</div>
</div>
</div>
</Card>
</Col>
</div>
);
}
return (
<div className="d-flex text-center" style={{ justifyContent: "space-evenly", gap: "1rem" }}>
{Object.keys(gptConfig.sessionRatingAlgs).map((e) => (
<Thing
key={e}
name={`Average ${UppercaseFirst(e)}`}
value={FormatSessionRating(
session.game,
session.playtype,
e as AnySessionRatingAlg,
session.calculatedData[e as AnySessionRatingAlg]
)}
/>
))}
</div>
);
}
@@ -36,7 +36,7 @@ export default function DropdownRow({
{children}
</tr>
<tr className={nested ? "nested-expandable-pseudo-row" : "expandable-pseudo-row"}>
<td colSpan={100} style={{ padding: 0, borderTop: "none" }}>
<td colSpan={100} style={{ padding: 0, borderTop: "none", width: "100%" }}>
<Collapse in={showDropdown}>
{renderDropdown ? (
<div className="tr-dropdown-container">{dropdown}</div>
@@ -1,43 +1,56 @@
import { GetValueGetter, SearchFunctions } from "util/ztable/search";
import { RFA } from "util/misc";
import Divider from "components/util/Divider";
import ExternalLink from "components/util/ExternalLink";
import HoverText from "components/util/HoverText";
import SmallText from "components/util/SmallText";
import StickyPopover from "components/util/StickyPopover";
import { nanoid } from "nanoid";
import React, { useState } from "react";
import { Modal, OverlayTrigger, Tooltip } from "react-bootstrap";
import { Modal } from "react-bootstrap";
function AdditionalTableProps({
function ExampleQueries({
keyName: key,
value,
}: {
keyName: string;
value: string | number | [string, number] | boolean | null;
}) {
let typeName;
if (Array.isArray(value)) {
typeName = "Hybrid";
} else if (typeof value === "string") {
typeName = "Text";
if (typeof value === "string") {
return (
<>
<code>
{key}={value}
</code>
.{" "}
<small className="text-body-secondary" style={{ fontSize: "1rem" }}>
(You can use <code>==</code> for exact matching)
</small>
</>
);
} else if (value === null) {
typeName = "Cannot Infer :(";
return <code>{key}=asdf</code>;
} else if (typeof value === "boolean") {
typeName = "Presence";
} else {
typeName = "Number";
return (
<>
<code>{key}=yes</code>, <code>{key}=no</code>
</>
);
}
let v;
if (Array.isArray(value)) {
v = value[0];
} else {
v = Number.isInteger(value) ? value : value.toFixed(2);
}
const randomMathOp = RFA([">", "<", "<=", ">="]);
return (
<>
<td>{typeName}</td>
<td>
{typeof value === "boolean"
? "Yes/No/True/False"
: Array.isArray(value)
? `${value[0]} (${value[1]})`
: value}
</td>
</>
<code>
{key}
{randomMathOp}
{v}
</code>
);
}
@@ -46,7 +59,7 @@ export default function FilterDirectivesIndicator<D>({
doc,
}: {
searchFunctions: SearchFunctions<D>;
doc?: D;
doc: D;
}) {
const [modalShow, setModalShow] = useState(false);
@@ -72,99 +85,41 @@ export default function FilterDirectivesIndicator<D>({
</StickyPopover>
<Modal show={modalShow} onHide={() => setModalShow(false)} size="xl">
<Modal.Header closeButton>
<Modal.Title>Available Directives</Modal.Title>
<Modal.Title>Table Filters</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
This is a slightly advanced feature for{" "}
<HoverText hover="nerds">power users</HoverText>. You can read about it{" "}
<a href="https://docs.tachi.ac/wiki/filter-directives/">here</a>
.<br />
The quick explanation is that you can use directives like{" "}
<code>Title=conflict Score&gt;=100</code> to perform advanced filters!
You can use filters on this table to show just what you care about. The list
of filters you can use here is shown below!
</p>
<Divider className="mb-4" />
{doc ? (
<table className="table table-striped table-hover text-center">
<thead>
<tr>
<th>Key</th>
<OverlayTrigger
overlay={
<Tooltip className="tooltip tooltip-wide" id={nanoid()}>
<table className="table table-sm table-striped table-hover">
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>Text</strong>
</td>
<td>
Works like text. <code>&gt;</code>{" "}
and similar operators compare
alphabetically.
</td>
</tr>
<tr>
<td>
<strong>Number</strong>
</td>
<td>Works like a number.</td>
</tr>
<tr>
<td>
<strong>Hybrid</strong>
</td>
<td>
Although displayed as a string, and
works like a string,{" "}
<code>&gt;</code> and similar
operators compare numerically on a
hidden number. This is so things
like <code>grade:&gt;AAA</code>
get all the grades better than AAA,
instead of alphabeticalising.
</td>
</tr>
</tbody>
</table>
</Tooltip>
}
placement="right"
>
<th>Data Type</th>
</OverlayTrigger>
<th>Example Data</th>
</tr>
</thead>
<tbody>
{Object.keys(searchFunctions).map((key) => (
<tr key={key}>
<td>
<strong>
{key[0].toUpperCase()}
{key.substring(1)}
</strong>
</td>
<AdditionalTableProps
value={GetValueGetter(searchFunctions[key])(doc)}
/>
</tr>
))}
</tbody>
</table>
) : (
<span className="text-center">
Looks like this table has no data. We can't show anything about the
directives.
</span>
)}
<ul style={{ fontSize: "1.5rem" }}>
{Object.keys(searchFunctions).map((key) => (
<li>
<ExampleQueries
keyName={key}
value={GetValueGetter(searchFunctions[key])(doc)}
/>
</li>
))}
</ul>
<Divider className="mb-4" />
Option List
<ul>
<li>
<code>=</code> (Equal to)
</li>
<li>
<code>==</code> (Text is EXACTLY equal to)
</li>
<li>
<code>&gt;</code>, <code>&lt;</code> (Greater than, Less than)
</li>
<li>
<code>&gt;=</code>, <code>&lt;=</code> (Greater than or equal to, Less
than or equal to)
</li>
</ul>
<span>
<small className="text-body-secondary">
You can <SmallText small="tap" large="click" /> anywhere off this dialog
@@ -151,10 +151,12 @@ export default function TachiTable<D>({
placeholder={`Filter ${entryName}`}
value={search}
/>
<FilterDirectivesIndicator
searchFunctions={searchFunctions}
doc={dataset[0]}
/>
{dataset[0] && (
<FilterDirectivesIndicator
searchFunctions={searchFunctions}
doc={dataset[0]}
/>
)}
</InputGroup>
)}
</div>
@@ -168,16 +170,18 @@ export default function TachiTable<D>({
</div>
<div className="row row-gap-4">
<div className="col-lg-4 d-flex justify-content-center justify-content-lg-start">
<Select
name={`Show this many ${entryName}:`}
value={ztable.pageLen.toString()}
setValue={(e) => ztable.setPageLen(Number(e))}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</Select>
{dataset.length > 10 && (
<Select
name={`Show this many ${entryName}:`}
value={ztable.pageLen.toString()}
setValue={(e) => ztable.setPageLen(Number(e))}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</Select>
)}
</div>
<div className="d-none d-lg-flex col-lg-4 justify-content-center align-items-center">
{settings?.preferences.developerMode && (
@@ -199,23 +203,25 @@ export default function TachiTable<D>({
)}
</div>
<div className="col-lg-4 ms-auto d-flex justify-content-center justify-content-lg-end">
<div className="btn-group">
<Button
variant="secondary"
disabled={pageState === "start" || pageState === "start-end"}
onClick={decrementPage}
>
<SmallText small="<" large="Previous" />
</Button>
<PageSelector currentPage={page} maxPage={maxPage} setPage={setPage} />
<Button
variant="secondary"
disabled={pageState === "end" || pageState === "start-end"}
onClick={incrementPage}
>
<SmallText small=">" large="Next" />
</Button>
</div>
{dataset.length > 10 && (
<div className="btn-group">
<Button
variant="secondary"
disabled={pageState === "start" || pageState === "start-end"}
onClick={decrementPage}
>
<SmallText small="<" large="Previous" />
</Button>
<PageSelector currentPage={page} maxPage={maxPage} setPage={setPage} />
<Button
variant="secondary"
disabled={pageState === "end" || pageState === "start-end"}
onClick={incrementPage}
>
<SmallText small=">" large="Next" />
</Button>
</div>
)}
</div>
</div>
</div>
@@ -19,17 +19,26 @@ export default function SelectLinkButton({
} & LinkButtonProps) {
const match = DoesMatchRoute(window.location.href, to, !matchIfStartsWith);
const variant = match ? onVariant : offVariant;
const classNames = `${match ? "" : "text-body text-light-hover text-light-focus"} ${className}`;
const classNames = `${
match ? "" : "text-body text-light-hover text-light-focus"
} ${className} text-wrap`;
return (
<LinkButton
to={to}
size="lg"
isActive={() => match}
variant={variant}
{...props}
className={classNames}
>
{children}
<div className="d-none d-lg-block">{children}</div>
<div
className="d-lg-none"
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
>
{children}
</div>
</LinkButton>
);
}
+9 -1
View File
@@ -1,6 +1,5 @@
import { NumericSOV } from "util/sorts";
import { ChangeOpacity } from "util/color-opacity";
import { FormatMillions } from "util/misc";
import { COLOUR_SET, GPTString, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common";
import CHUNITHMJudgementCell from "components/tables/cells/CHUNITHMJudgementCell";
import ITGJudgementCell from "components/tables/cells/ITGJudgementCell";
@@ -42,6 +41,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
"iidx:SP": IIDX_SP_IMPL,
"iidx:DP": IIDX_DP_IMPL,
"chunithm:Single": {
sessionImportantScoreCount: 30,
enumIcons: defaultEnumIcons,
classColours: {
colour: {
@@ -129,6 +129,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"jubeat:Single": {
sessionImportantScoreCount: 30,
enumIcons: defaultEnumIcons,
enumColours: {
lamp: {
@@ -187,6 +188,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
ratingCell: ({ sc }) => <JubilityCell score={sc} />,
},
"maimai:Single": {
sessionImportantScoreCount: 30,
enumIcons: defaultEnumIcons,
enumColours: {
lamp: {
@@ -287,6 +289,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"maimaidx:Single": {
sessionImportantScoreCount: 30,
enumIcons: defaultEnumIcons,
enumColours: {
lamp: {
@@ -429,6 +432,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"museca:Single": {
sessionImportantScoreCount: 20,
enumIcons: defaultEnumIcons,
enumColours: {
grade: {
@@ -476,6 +480,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"popn:9B": {
sessionImportantScoreCount: 20,
enumIcons: {
grade: "sort-alpha-up",
lamp: "lightbulb",
@@ -551,6 +556,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"wacca:Single": {
sessionImportantScoreCount: 50,
enumIcons: defaultEnumIcons,
enumColours: {
grade: {
@@ -635,6 +641,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"itg:Stamina": {
sessionImportantScoreCount: 10,
enumIcons: defaultEnumIcons,
enumColours: {
grade: {
@@ -722,6 +729,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
),
},
"ongeki:Single": {
sessionImportantScoreCount: 45,
enumIcons: {
grade: "sort-alpha-up",
noteLamp: "lightbulb",
+1
View File
@@ -89,6 +89,7 @@ const ArcaeaRatingCell: GPTClientImplementation<GPTStrings["arcaea"]>["ratingCel
}) => <RatingCell score={sc} rating={rating} />;
export const ARCAEA_TOUCH_IMPL: GPTClientImplementation<"arcaea:Touch"> = {
sessionImportantScoreCount: 30,
ratingSystems: [],
enumIcons: {
grade: "sort-alpha-up",
+1
View File
@@ -12,6 +12,7 @@ import { GetEnumColour } from "lib/game-implementations";
import { CreateRatingSys, bgc } from "./_util";
const BASE_IMPL: GPTClientImplementation<GPTStrings["bms" | "pms"]> = {
sessionImportantScoreCount: 20,
difficultyColours: {
CHART: COLOUR_SET.gray, // lol
},
+3 -1
View File
@@ -7,7 +7,7 @@ import ScoreCell from "components/tables/cells/ScoreCell";
import { GetEnumColour } from "lib/game-implementations";
import React from "react";
import RatingCell from "components/tables/cells/RatingCell";
import { bg, bgc } from "./_util";
import { bgc } from "./_util";
const GITADORA_ENUM_COLOURS: GPTClientImplementation<
"gitadora:Dora" | "gitadora:Gita"
@@ -82,6 +82,7 @@ const GITADORARatingCell: GPTClientImplementation<GPTStrings["gitadora"]>["ratin
}) => <RatingCell score={sc} rating={rating} />;
export const GITADORA_GITA_IMPL: GPTClientImplementation<"gitadora:Gita"> = {
sessionImportantScoreCount: 50,
enumIcons: {
grade: "sort-alpha-up",
lamp: "lightbulb",
@@ -105,6 +106,7 @@ export const GITADORA_GITA_IMPL: GPTClientImplementation<"gitadora:Gita"> = {
};
export const GITADORA_DORA_IMPL: GPTClientImplementation<"gitadora:Dora"> = {
sessionImportantScoreCount: 50,
enumIcons: {
grade: "sort-alpha-up",
lamp: "lightbulb",
+2
View File
@@ -128,6 +128,7 @@ const IIDXRatingCell: GPTClientImplementation<GPTStrings["iidx"]>["ratingCell"]
);
export const IIDX_SP_IMPL: GPTClientImplementation<"iidx:SP"> = {
sessionImportantScoreCount: 20,
difficultyColours: IIDX_DIFF_COLOURS,
enumColours: IIDX_ENUM_COLOURS,
enumIcons: {
@@ -167,6 +168,7 @@ export const IIDX_SP_IMPL: GPTClientImplementation<"iidx:SP"> = {
};
export const IIDX_DP_IMPL: GPTClientImplementation<"iidx:DP"> = {
sessionImportantScoreCount: 20,
difficultyColours: IIDX_DIFF_COLOURS,
enumColours: IIDX_ENUM_COLOURS,
enumIcons: {
+3 -1
View File
@@ -9,7 +9,7 @@ import SDVXLampCell from "components/tables/cells/SDVXLampCell";
import { GetEnumColour } from "lib/game-implementations";
import React from "react";
import VF6Cell from "components/tables/cells/VF6Cell";
import { CreateRatingSys, bg, bgc } from "./_util";
import { CreateRatingSys, bgc } from "./_util";
type SDVXLikes = GPTStrings["usc" | "sdvx"];
@@ -75,6 +75,7 @@ const SDVXRatingCell: GPTClientImplementation<SDVXLikes>["ratingCell"] = ({ sc,
);
export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = {
sessionImportantScoreCount: 50,
enumColours: SDVXLIKE_ENUM_COLOURS,
enumIcons: {
grade: "sort-alpha-up",
@@ -168,6 +169,7 @@ export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = {
ratingCell: SDVXRatingCell,
};
export const USC_IMPL: GPTClientImplementation<GPTStrings["usc"]> = {
sessionImportantScoreCount: 50,
enumColours: SDVXLIKE_ENUM_COLOURS,
enumIcons: {
grade: "sort-alpha-up",
+7
View File
@@ -8,6 +8,7 @@ import {
Difficulties,
ExtractedClasses,
GPTString,
integer,
PBScoreDocument,
ScoreDocument,
ScoreRatingAlgorithms,
@@ -124,4 +125,10 @@ export interface GPTClientImplementation<GPT extends GPTString = GPTString> {
chart: ChartDocument<GPT>;
rating: ScoreRatingAlgorithms[GPT];
}) => JSX.Element;
/**
* How many scores should we consider for the topX in the session important scores
* display?
*/
sessionImportantScoreCount: integer;
}
-1
View File
@@ -1,4 +1,3 @@
version: "3.9"
services:
mongo:
container_name: mongo
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tachi",
"version": "2.5.1",
"version": "2.5.2",
"private": true,
"description": "The root of the Tachi monorepo. Contains common utils like linters.",
"scripts": {
+1 -43
View File
@@ -8,47 +8,6 @@ import { execSync } from "child_process";
import fs from "fs";
import path from "path";
// As is with all front-facing zkldi projects, the version names for tachi-server
// are from an album I like. In this case, the al(bum is Portishead - Dummy.
// The fact that this album is very similar to Mezzanine
// - the album used for Kamaitachi v1 - is deliberate. :) - zkldi
const PORTISHEAD_DUMMY = [
// v2.0.0
"Mysterons",
// v2.1.0
"Sour Times",
// v2.2.0
"Strangers",
// v2.3.0
"It Could Be Sweet",
// v2.4.0
"Wandering Star",
// v2.5.0
"It's a Fire",
// v2.6.0
"Numb",
// v2.7.0
"Roads",
// v2.8.0
"Pedestal",
// v2.9.0
"Biscuit",
// v2.10.0
"Glory Box",
// v2.11+ onwards are not expected to occur, but if they do, we'll figure something out.
];
const logger = CreateLogCtx(__filename);
// we want to read the monorepo root package.json.
@@ -85,9 +44,8 @@ export const VERSION_INFO = {
minor: semverInfo?.minor ?? 0,
patch: semverInfo?.patch ?? 0,
commit,
name: PORTISHEAD_DUMMY[semverInfo?.minor ?? 0] ?? "No Version Name",
};
export const VERSION_STR = semverInfo?.raw ?? "0.0.0-unknown";
export const VERSION_PRETTY = `v${VERSION_STR} (${VERSION_INFO.name}) [${commit}]`;
export const VERSION_PRETTY = `v${VERSION_STR} [${commit}]`;