fix: backport all features from v2 that came in after merge

This commit is contained in:
zk
2026-05-17 12:53:26 +00:00
parent 426bb250d9
commit fe76dad32d
34 changed files with 602 additions and 5219 deletions
@@ -325,6 +325,13 @@ function ImportInfoDisplayer({ game }: { game: GameGroup }) {
);
} else if (game === "maimaidx") {
Content.unshift(
<ImportInfoCard
desc="Automatically import scores, whenever you get them."
href="rizu"
key="Rizu"
moreInfo="This is the recommended way to import maimai DX scores; data is submitted at the end of each play."
name="Rizu"
/>,
<ImportInfoCard
desc="Use your data from maimai DX NET."
href="kt-maimaidx-site-importer"
@@ -0,0 +1,47 @@
import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function RizuPage() {
useSetSubheader(["Import Scores", "Rizu"]);
return (
<div>
<h1 className="text-center mb-4">What is Rizu?</h1>
<div>
Rizu automatically sends maimai DX scores to a server. {TachiConfig.NAME} is
compatible with what Rizu sends, so you can use it to submit scores!
</div>
<Divider />
<h1 className="text-center my-4">Setup Instructions</h1>
<ol className="instructions-list">
<li>
Download the latest version of <code>Rizu-MelonLoader-vVERSION.zip</code>{" "}
<ExternalLink href="https://gitea.tendokyu.moe/beerpsi/Rizu/releases/latest">
here
</ExternalLink>
.
</li>
<li>
Download your <code>Rizu.cfg</code> config file{" "}
<ExternalLink href="/client-file-flow/CXRizu">here</ExternalLink>
. <br />
<Alert className="mt-2" variant="warning">
This file contains an API Key, which is meant to be kept secret!
</Alert>
</li>
<li>
Follow the installation instructions on{" "}
<ExternalLink href="https://gitea.tendokyu.moe/beerpsi/Rizu#melonloader">
Tendokyu
</ExternalLink>
.
</li>
<li>Your scores are now automatically uploaded to {TachiConfig.NAME}!</li>
</ol>
</div>
);
}
@@ -0,0 +1,205 @@
import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import { UserContext } from "#context/UserContext";
import { TachiConfig } from "#lib/config";
import { Contributors } from "#util/constants/contributors";
import { RFA } from "#util/misc";
import React, { useContext, useState } from "react";
import { Link } from "react-router-dom";
export default function CreditsPage() {
useSetSubheader("Credits");
const [alt, setAlt] = useState(false);
const [whatTheHell, setWhatTheHell] = useState(0);
const { user } = useContext(UserContext);
function a(normal: string | JSX.Element, ...altStr: (string | JSX.Element)[]) {
if (!alt) {
return normal;
} else if (whatTheHell < 0) {
return normal;
}
return RFA(altStr);
}
return (
<div style={{ fontSize: "1.15rem" }}>
<p>
{a(
<>
{TachiConfig.NAME} has been the work of many people, and many more
contributors. None of this would be possible without these people.
</>,
"Funding for this program was made possible by by by by by-",
`${TachiConfig.NAME} actually just appeared here one day, and we're not sure how.`,
`Actually, we just ran npm install ${TachiConfig.NAME}.`,
"To be honest, we had a surplus of monkeys and typewriters.",
)}
</p>
<Divider />
<p>
If you want to support {TachiConfig.NAME} development, see{" "}
<Link to="/support">Support</Link>.
</p>
<Divider />
<div className="mt-4">
<h1 className="user-select-none">
De
<span
className="credits-easter-egg"
onClick={() => {
if (alt) {
// Force a different RFA
// by incrementing a random
// variable
setWhatTheHell(whatTheHell + 1);
} else {
setAlt(true);
}
}}
>
v
</span>{" "}
Team
</h1>
<ul>
<li>
{a(
"Lead Dev",
"Rules Of Hooks Violator",
"ABBA Enthusiast",
"Nutcase",
"IIDX 'player'",
)}
: <strong>zk</strong>
</li>
<li>
{a(
"Infrastructure & Dev/Ops",
"Professional YAML Editor",
"Professional nginx.conf Editor",
"'What do you mean you rm -rf'd that folder?'",
"WintOr",
"Frog Lover",
)}
: <strong>winter</strong>
<br />
<small>Also the reason I even started learning how to program.</small>
</li>
</ul>
</div>
<div className="mt-6">
<h1>Significant Contributors</h1>
<p>
These people have contributed a significant amount of their time to{" "}
{TachiConfig.NAME}.
</p>
<ul>
<li>
{a(
"Tachi Discord Bot Work + Homepage",
"oooOoooo you want to use redux oooo",
"Java Skirt Boy",
"Urbit Enjoyer",
)}
: <strong>pfych</strong>
</li>
<li>
{a("Mikado", "SDVX Pocky")}: <strong>adamaq01</strong>
</li>
<li>
{a("Fervidex", "IIDX Black Magic")}: <strong>aixxe</strong>
</li>
<li>
{a("Barbatos", "SDVX Black Magic")}: <strong>Arm1stice</strong>
</li>
<li>
{a(
"Graphic Design",
"Adobe Suite Purchaser",
"Squiggly Line Drawer",
"Straight Line Drawer",
"Ratboy Genius",
"Large Headded Individual",
"The Ace Of Craigs",
)}
: <strong>Craig</strong>
</li>
<li>
{a(
"Database Management, Various features, generally just a hell of a lot of work",
"This guy put emojis in my codebase",
"Emacs Evangelist",
)}
: <strong>cg505</strong>
</li>
<li>
{a("KsHook", "Konaste Black Magic")}: <strong>Emma</strong>
</li>
<li>
{a("APIs, Databasing", "All Black Magic")}: <strong>Felix</strong>
</li>
<li>
{a("APIs, Massive Dev Support", "Have you tried Elixir?")}:{" "}
<strong>haste</strong>
</li>
<li>
{a("Chunitachi", "CHUNITHM Black Magic")}: <strong>tomatosoup</strong>
</li>
<li>
{a(
"Dev, Maths Consultation",
"Functional Programming Evangelist",
"Born On The Cob",
"Crunchy Nut Enjoyer",
"Has An Afro",
)}
: <strong>Percyqaz</strong>
</li>
<li>
{a("Server", "Ethereum Mining Solutions")}: <strong>viddy</strong>
<br />
<small>Yes, the entire server, networking costs, everything.</small>
</li>
</ul>
</div>
<div className="mt-6">
<h1>Contributors</h1>
<p>These people have contributed to {TachiConfig.NAME}.</p>
<ul>
{Contributors.slice(0)
.sort((a, b) => a.localeCompare(b))
.map((e) => (
<li key={e}>{e}</li>
))}
</ul>
</div>
<div className="mt-6">
<h1>And...</h1>
<p>
Everyone who was part of the beta.
{user?.badges.some((x) => x === "beta" || x === "alpha")
? " (That's you!)"
: ""}
</p>
<p>
Everyone in the <code>#dev</code> chat for the constant advice and comedy gold.
</p>
</div>
<span className="text-body-secondary" style={{ fontSize: "0.2rem" }}>
Click the V in Dev Team.
</span>
</div>
);
}
@@ -1,18 +1,8 @@
import { TachiConfig } from "#lib/config";
import React, { useEffect, useState } from "react";
import React from "react";
import { type UserDocument } from "tachi-common";
export default function SupportBanner({ user }: { user: UserDocument }) {
// const { data, error } = useApiQuery<{ scores: integer; sessions: integer }>(
// `/users/${user.id}/stats`,
// );
const [show, _setShow] = useState(localStorage.getItem("SHOW_SUPPORT_TACHI") === "true");
useEffect(() => {
localStorage.setItem("SHOW_SUPPORT_TACHI", `${show}`);
}, [show]);
// thank you
if (user.isSupporter) {
return (
@@ -23,69 +13,4 @@ export default function SupportBanner({ user }: { user: UserDocument }) {
}
return <></>;
// if (!data) {
// return <></>;
// }
// if (error) {
// return <></>;
// }
// if (data.scores < 1_000) {
// return <></>;
// }
// // too soon to bother
// if (Date.now() - user.joinDate < ONE_DAY * 7) {
// return <></>;
// }
// if (!show) {
// return (
// <div
// className="d-flex w-100 justify-content-center flex-column align-items-center"
// onClick={() => setShow(true)}
// >
// <div>Support us? ❤️</div>
// <div>
// <Icon type="chevron-down" />
// </div>
// </div>
// );
// }
// return (
// <Card
// className="mb-8"
// footer={
// <div className="d-flex w-100 justify-content-center" onClick={() => setShow(false)}>
// <Icon type="chevron-up" />
// </div>
// }
// header={
// <span style={{ fontSize: "1.55rem" }}>
// You've set {data.scores} scores and had {data.sessions} sessions, wew!
// </span>
// }
// >
// <span style={{ fontSize: "1.15rem" }}>
// {TachiConfig.NAME} is a passion project, and is primarily developed by one person.
// <br />
// Since you're enjoying the site so much, maybe you'd want to support me? :3
// <br />
// <br />
// If you want to support development, you can donate to my{" "}
// <ExternalLink href="https://ko-fi.com/zkrising">Ko-Fi</ExternalLink>, if you
// indicate your account name in the donation, you'll get a shiny name on the site!
// <br />
// I'm working on some new projects in secret. Maybe I'll post some updates on there :P
// <br />
// <br />
// Alternatively, you can star or contribute to the fully-open-source{" "}
// <ExternalLink href="https://github.com/zkldi/Tachi">GitHub Repo</ExternalLink>. This
// makes me look cool to employers!
// </span>
// </Card>
// );
}
@@ -5,7 +5,7 @@ import React from "react";
import { Link } from "react-router-dom";
export default function SupportMePage() {
useSetSubheader("Support / Ko-fi");
useSetSubheader("Support");
return (
<div style={{ fontSize: "1.15rem" }}>
@@ -14,11 +14,7 @@ export default function SupportMePage() {
<Link to="/credits">people like you</Link>.
</p>
<p>
If you want to support {TachiConfig.NAME} development, you can donate to my{" "}
<ExternalLink href="https://ko-fi.com/zkrising">Ko-fi</ExternalLink>.
</p>
<p>
Alternatively, you can star the{" "}
You can star the{" "}
<ExternalLink href="https://github.com/zkldi/Tachi">GitHub Repo</ExternalLink>. This
makes me look cool to employers!
</p>
@@ -1,3 +1,4 @@
import CreditsPage from "#app/pages/dashboard/misc/CreditsPage";
import MyProposalsPage from "#app/pages/dashboard/proposals/MyProposalsPage";
import ProposalsPage from "#app/pages/dashboard/proposals/ProposalsPage";
import SearchPage from "#app/pages/dashboard/search/SearchPage";
@@ -95,6 +96,10 @@ export default function DashboardRoutes() {
<PrivacyPolicyPage />
</Route>
<Route exact path="/credits">
<CreditsPage />
</Route>
{/* <Route exact path="/support">
<SupportMePage />
</Route> */}
@@ -19,6 +19,7 @@ import MikadoPage from "#app/pages/dashboard/import/MikadoPage";
import OngekiArtemisExportPage from "#app/pages/dashboard/import/OngekiArtemisExportPage";
import OngekiInoharaPage from "#app/pages/dashboard/import/OngekiInoharaPage";
import OngekiSiteImportPage from "#app/pages/dashboard/import/OngekiSiteImportPage";
import RizuPage from "#app/pages/dashboard/import/RizuPage";
import SaekawaPage from "#app/pages/dashboard/import/SaekawaPage";
import SDVXEamCSVPage from "#app/pages/dashboard/import/SDVXEamCSVPage";
import SilentHookPage from "#app/pages/dashboard/import/SilentHookPage";
@@ -255,6 +256,10 @@ export default function ImportRoutes() {
<WACCAMyPageScraperPage />
</Route>
<Route exact path="/import/rizu">
<RizuPage />
</Route>
<Route exact path="/import/kt-maimaidx-site-importer">
<MaimaiDXSiteImportPage />
</Route>
@@ -45,8 +45,10 @@ export default function ClassBadge<GPT extends V3Game = V3Game>({
</Badge>
);
} else {
const styleWithMaybeShine = classStyle as { shine?: boolean } & React.CSSProperties;
const { shine, ...badgeStyle } = styleWithMaybeShine;
badgeComponent = (
<Badge bg={""} className="mx-2" style={classStyle}>
<Badge bg={""} className={`mx-2${shine ? " shine" : ""}`} style={badgeStyle}>
{data.display}
</Badge>
);
@@ -185,7 +185,9 @@ function ChartInfoMiddle({
const diff = FormatDifficultySearch(chart);
const gameName =
game === "ongeki" ? "オンゲキ" : game === "maimaidx" ? "maimai" : gameGroupConfig.name;
const formattedTitle = song.title.replace(/-/gu, " ");
const formattedTitle = song.title
.replace(/([!?,]){4,}/gu, (m) => m[0].repeat(3))
.replace(/-/gu, " ");
let search = `${gameName} ${formattedTitle}`;
@@ -16,9 +16,14 @@ export default function IIDXStyleSongChartInfoFormat({
game: V3Game;
song: SongDocument<"bms" | "chunithm" | "iidx" | "maimaidx" | "ongeki" | "pms" | "popn">;
}) {
const genre =
game === "ongeki"
? ((song as SongDocument<"ongeki">).data.flavorGenre ?? song.data.genre)
: song.data.genre;
return (
<>
<h4>{song.data.genre}</h4>
<h4>{genre}</h4>
<h4 style={{ fontSize: "2.5rem", fontWeight: "bold" }}>{song.title}</h4>
<h4>{song.artist}</h4>
{chart && <h5>({LevelText(chart)})</h5>}
@@ -63,6 +63,14 @@ export function Footer() {
>
GDPR
</Nav.Link>
<Nav.Link
as={Link}
className={linkClassNames}
onClick={() => window.scrollTo(0, 0)}
to="/credits"
>
Credits
</Nav.Link>
{import.meta.env.VITE_DISCORD && (
<Nav.Link
as={ExternalLink}
@@ -149,7 +149,7 @@ function GraphComponent({
},
]}
difficulty={chart.difficulty}
duration={song.data.duration}
duration={song.data.duration ?? 240}
game="ongeki"
height="360px"
maximumAbsoluteValue={
@@ -25,7 +25,7 @@ import { NumericSOV } from "#util/sorts";
import React from "react";
import { COLOUR_SET, type PBScoreDocument, type ScoreDocument, type V3Game } from "tachi-common";
import { bgc, RAINBOW_EX_GRADIENT, RAINBOW_GRADIENT, RAINBOW_SHINY_GRADIENT } from "./games/_util";
import { bgc, RAINBOW_EX_GRADIENT, RAINBOW_GRADIENT } from "./games/_util";
import { ARCAEA_TOUCH_IMPL } from "./games/arcaea";
import { BMS_7K_IMPL, BMS_14K_IMPL, PMS_IMPL } from "./games/bms-pms";
import { DDR_DP_IMPL, DDR_SP_IMPL } from "./games/ddr";
@@ -785,9 +785,22 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
SILVER: bgc("gray", "var(--bs-light)"),
GOLD: bgc("var(--bs-warning)", "var(--bs-dark)"),
PLATINUM: bgc("silver", "var(--bs-dark)"),
RAINBOW: RAINBOW_GRADIENT,
RAINBOW_SHINY: RAINBOW_SHINY_GRADIENT,
RAINBOW_EX: RAINBOW_EX_GRADIENT,
RAINBOW: {
background: "linear-gradient(-45deg, #ff73d6 20%, #00ffff 50%, #ebfb2a 80%)",
color: "var(--bs-dark)",
},
RAINBOW_SHINY: {
background: "linear-gradient(-45deg, #ff73d6 20%, #00ffff 50%, #ebfb2a 80%)",
color: "var(--bs-dark)",
shine: true,
},
RAINBOW_EX: {
background:
"linear-gradient(-45deg, #df7e85 20%, #efef51 40%, #4bee83 60%, #70bdd7 80%)",
color: "var(--bs-dark)",
shine: true,
},
RAINBOW_EX_TRUE: { ...RAINBOW_EX_GRADIENT, shine: true },
},
},
enumColours: {
@@ -845,7 +858,14 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
[
"Platinum Score",
"P-Score",
NumericSOV((x) => x.scoreData.platinumStars * 100000 + x.scoreData.platinumScore),
// __related is attached when rendering table rows with chart context
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- NumericSOV row shape
NumericSOV((x: any) => {
if (x.__related?.chart?.data !== undefined) {
return x.scoreData.platinumScore / x.__related.chart.data.maxPlatScore;
}
return x.scoreData.platinumStars * 100000 + x.scoreData.platinumScore;
}),
],
["Judgements", "Notes", NumericSOV((x) => x.scoreData.judgements.cbreak ?? 0)],
[
-6
View File
@@ -36,12 +36,6 @@ export const RAINBOW_GRADIENT = {
color: "var(--bs-dark)",
} as const;
export const RAINBOW_SHINY_GRADIENT = {
background:
"linear-gradient(-45deg, #ff8697, #ff9dbf, #9e81d0, #87caff, #7dae80, #ffffa7, #ffd88a, #ffab8d, #ff8697)",
color: "var(--bs-dark)",
} as const;
export const RAINBOW_EX_GRADIENT = {
background: "linear-gradient(-45deg, #0fa091, #0f98d5, #67087f, #d9007e, #f56e06)",
color: "var(--bs-light)",
+1 -1
View File
@@ -65,13 +65,13 @@ export type GPTClassColours<GPT extends V3Game> = {
// css properties => css
// null -> no styling
[V in ClassConfigs[GPT][C]["values"][number]["id"]]:
| ({ shine?: boolean } & CSSProperties)
| "danger"
| "info"
| "primary"
| "secondary"
| "success"
| "warning"
| CSSProperties
| null;
};
};
@@ -6,3 +6,35 @@
opacity: 1;
}
}
@keyframes shine {
0% {
top: -150%;
left: -150%;
}
100% {
top: 150%;
left: 150%;
}
}
.shine {
position: relative;
overflow: hidden;
}
.shine::before {
background: linear-gradient(
transparent,
rgba(255, 255, 255, 0.5) 50%,
transparent
);
content: "";
display: block;
width: 150%;
height: 2rem;
position: absolute;
transform: rotate(-45deg);
animation: shine 4s infinite ease-in-out;
z-index: 100;
}