ARC support.

This commit is contained in:
zkldi
2021-11-14 06:06:40 +00:00
parent bea729f829
commit 6752e68ab4
15 changed files with 324 additions and 42 deletions
+1 -1
View File
@@ -80,7 +80,7 @@
"resolve": "1.12.0",
"resolve-url-loader": "3.1.0",
"socicon": "3.0.5",
"tachi-common": "0.3.0"
"tachi-common": "0.3.2"
},
"devDependencies": {
"@types/humanize-duration": "3.25.1",
+4 -4
View File
@@ -82,7 +82,7 @@ specifiers:
serve: 11.2.0
socicon: 3.0.5
style-loader: 1.0.0
tachi-common: 0.3.0
tachi-common: 0.3.2
terser-webpack-plugin: 1.4.1
ts-pnp: 1.1.4
typescript: 4.3.5
@@ -152,7 +152,7 @@ dependencies:
resolve: 1.12.0
resolve-url-loader: 3.1.0
socicon: 3.0.5
tachi-common: 0.3.0
tachi-common: 0.3.2
devDependencies:
'@types/humanize-duration': 3.25.1
@@ -11211,8 +11211,8 @@ packages:
strip-ansi: 6.0.0
dev: true
/tachi-common/0.3.0:
resolution: {integrity: sha512-ch7Msau97/on1d18bqRMUNUQhF9ZixzvNxMsLJNN14DBfS1AVxaoAfIRiQzWQ1jdfvwmcpP4Ge0x1w1prPZVAQ==}
/tachi-common/0.3.2:
resolution: {integrity: sha512-fX1ffleAJlP6WtjnGxVDfLsz7PQDArIFCMcvOjLB0+PwJ5FwhAO/8CX2B8ksYGuzp8PGWLxQGsPahwN9HV7YVQ==}
dependencies:
monk: 7.3.4
transitivePeerDependencies:
@@ -1,4 +1,5 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import QuickTooltip from "components/layout/misc/QuickTooltip";
import Card from "components/layout/page/Card";
import LampCell from "components/tables/cells/LampCell";
import ScoreCell from "components/tables/cells/ScoreCell";
@@ -31,6 +32,7 @@ import { PBDataset } from "types/tables";
import { APIFetchV1, UnsuccessfulAPIFetchResponse } from "util/api";
import { CreateUserMap } from "util/data";
import { SelectRightChart } from "util/misc";
import { FormatDate, MillisToSince } from "util/time";
// This component forms a wrapper around the Real GPT Chart Page
// which handles the case where activeChart == null.
@@ -319,6 +321,13 @@ function PlayCard({
<LampCell score={pb} />
</tr>
</MiniTable>
<div className="text-center">
<Muted>
{pb.timeAchieved
? MillisToSince(pb.timeAchieved) ?? ""
: "No Timestamp Info"}
</Muted>
</div>
</Col>
</Row>
</Card>
@@ -0,0 +1,123 @@
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";
import React, { useEffect, useState } from "react";
import { Button, Col, Form } from "react-bootstrap";
import { ARCSavedProfileDocument, PublicUserDocument } from "tachi-common";
import { SetState } from "types/react";
import { APIFetchV1 } from "util/api";
export default function ARCIntegrationPage({ reqUser }: { reqUser: PublicUserDocument }) {
const [iidxID, setIIDXID] = useState("");
const [sdvxID, setSDVXID] = useState("");
const { data, isLoading, error } = useApiQuery<{
iidx: ARCSavedProfileDocument | null;
sdvx: ARCSavedProfileDocument | null;
}>(`/users/${reqUser.id}/integrations/arc`);
useEffect(() => {
if (data) {
setIIDXID(data.iidx?.accountID ?? "");
setSDVXID(data.sdvx?.accountID ?? "");
}
}, [data]);
if (error) {
return <ApiError error={error} />;
}
if (!data || isLoading) {
return <Loading />;
}
return (
<>
<Col xs={12}>
<h4>ARC Integration</h4>
<span>To import scores from ARC, just put your AccountIDs in the tabs below.</span>
<br />
<Muted>
Note: Only the latest versions of IIDX and SDVX are supported for imports.
</Muted>
<Divider />
<h5>How to get your AccountID</h5>
<ol className="text-left">
<li>Open the ARC website.</li>
<li>Go to the game you care about, and select My Bests.</li>
<li>
In the URL, after <code>/profiles</code>, there'll be a string that looks
like <code>H4JxPV3vQH_</code>. Copy that into the text fields below!
</li>
<li>To unset an integration, remove the account ID and hit save.</li>
</ol>
</Col>
<Col xs={12} className="mt-4">
<IntegrationStatus name="IIDX" value={iidxID} setValue={setIIDXID} />
{iidxID && iidxID.length !== 11 && (
<span className="text-danger">
Invalid IIDX ID, it should be 11 characters long.
</span>
)}
<IntegrationStatus name="SDVX" value={sdvxID} setValue={setSDVXID} />
{sdvxID && sdvxID.length !== 11 && (
<span className="text-danger">
Invalid SDVX ID, it should be 11 characters long.
</span>
)}
<Divider />
<Button
disabled={
!!(iidxID && iidxID.length !== 11) || !!(sdvxID && sdvxID.length !== 11)
}
onClick={() => {
APIFetchV1(
`/users/${reqUser.id}/integrations/arc`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
iidx: iidxID || null,
sdvx: sdvxID || null,
}),
},
true,
true
);
}}
>
Submit Settings
</Button>
</Col>
</>
);
}
function IntegrationStatus({
name,
value,
setValue,
}: {
name: string;
value: string;
setValue: SetState<string>;
}) {
return (
<Form.Group className="mt-4">
<Form.Label>{`${name} Account ID`}</Form.Label>
<Form.Control
type="text"
value={value}
placeholder="H4JxPV3vQH_"
onChange={e => setValue(e.target.value)}
/>
</Form.Group>
);
}
@@ -1,7 +1,6 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import ApiError from "components/util/ApiError";
import DebugContent from "components/util/DebugContent";
import Divider from "components/util/Divider";
import ExternalLink from "components/util/ExternalLink";
import FormInput from "components/util/FormInput";
@@ -16,12 +15,13 @@ import { Alert, Button, Col, Form, Modal, Row } from "react-bootstrap";
import {
APIPermissions,
APITokenDocument,
TachiAPIClientDocument,
PublicUserDocument,
TachiAPIClientDocument,
} from "tachi-common";
import { SetState } from "types/react";
import { APIFetchV1 } from "util/api";
import { allPermissions, DelayedPageReload } from "util/misc";
import ARCIntegrationPage from "./ARCIntegrationPage";
import FervidexIntegrationPage from "./FervidexIntegrationPage";
export default function UserIntegrationsPage({ reqUser }: { reqUser: PublicUserDocument }) {
@@ -41,7 +41,7 @@ export default function UserIntegrationsPage({ reqUser }: { reqUser: PublicUserD
{mode !== "btchi" && (
<SelectButton value={page} setValue={setPage} id="services">
<Icon type="network-wired" />
Integrations
Service Configuration
</SelectButton>
)}
<SelectButton value={page} setValue={setPage} id="api-keys">
@@ -57,7 +57,7 @@ export default function UserIntegrationsPage({ reqUser }: { reqUser: PublicUserD
</Col>
<Col xs={12}>
{page === "services" ? (
<IntegrationsPage reqUser={reqUser} />
<ServicesPage reqUser={reqUser} />
) : page === "api-keys" ? (
<APIKeysPage reqUser={reqUser} />
) : (
@@ -605,7 +605,7 @@ function EditClientModal({
);
}
function IntegrationsPage({ reqUser }: { reqUser: PublicUserDocument }) {
function ServicesPage({ reqUser }: { reqUser: PublicUserDocument }) {
if (mode === "btchi") {
return (
<Row className="text-center">
@@ -619,7 +619,7 @@ function IntegrationsPage({ reqUser }: { reqUser: PublicUserDocument }) {
return (
<Row className="text-center justify-content-center">
<Col xs={12}>
<h3>Services</h3>
<h3>Service Configuration</h3>
<Muted>
Some services have had their names truncated to their first three characters for
privacy reasons.
@@ -637,7 +637,11 @@ function IntegrationsPage({ reqUser }: { reqUser: PublicUserDocument }) {
</div>
<Divider />
</Col>
{page === "fervidex" ? <FervidexIntegrationPage reqUser={reqUser} /> : <></>}
{page === "fervidex" ? (
<FervidexIntegrationPage reqUser={reqUser} />
) : (
<ARCIntegrationPage reqUser={reqUser} />
)}
</Row>
);
}
@@ -76,6 +76,7 @@ function PreferencesForm({ reqUser }: { reqUser: PublicUserDocument }) {
developerMode: settings?.preferences.developerMode ?? false,
invisible: settings?.preferences.invisible ?? false,
contentiousContent: settings?.preferences.contentiousContent ?? false,
advancedMode: settings?.preferences.advancedMode ?? false,
},
onSubmit: async values => {
const res = await APIFetchV1<UserSettings>(
@@ -107,6 +108,18 @@ function PreferencesForm({ reqUser }: { reqUser: PublicUserDocument }) {
<Form.Text>Enable debug information and other useful debugging buttons.</Form.Text>
</Form.Group>
<Form.Group>
<Form.Check
type="checkbox"
id="advancedMode"
checked={formik.values.developerMode}
onChange={formik.handleChange}
label="Advanced Mode"
/>
<Form.Text>
Enable advanced stuff, like being able to copy a tables contents into a CSV.
</Form.Text>
</Form.Group>
{/* <Form.Group>
<Form.Check
type="checkbox"
id="invisible"
@@ -115,7 +128,7 @@ function PreferencesForm({ reqUser }: { reqUser: PublicUserDocument }) {
label="Invisible Mode"
/>
<Form.Text>Hide your last seen status.</Form.Text>
</Form.Group>
</Form.Group> */}
<Form.Group>
<Form.Check
type="checkbox"
@@ -320,7 +333,7 @@ function FileUploadController({
DelayedPageReload();
}
}}
disabled={!reqUser.customPfp}
disabled={!reqUser.customPfpLocation}
className="mr-auto"
variant="secondary"
>
+9
View File
@@ -12,6 +12,7 @@ import MerJSONPage from "app/pages/dashboard/import/MerJSONPage";
import SSSXMLPage from "app/pages/dashboard/import/SSSXMLPage";
import USCDBPage from "app/pages/dashboard/import/USCDBPage";
import USCIRPage from "app/pages/dashboard/import/USCIRPage";
import ARCImportPage from "components/imports/ARCImportPage";
import KAIIntegrationPage from "components/imports/KAIIntegrationPage";
import Divider from "components/util/Divider";
import { UserContext } from "context/UserContext";
@@ -98,6 +99,14 @@ export default function ImportRoutes() {
<ChunitachiPage />
</Route>
<Route exact path="/dashboard/import/iidx-arc">
<ARCImportPage game="iidx" />
</Route>
<Route exact path="/dashboard/import/sdvx-arc">
<ARCImportPage game="sdvx" />
</Route>
<Route exact path="/dashboard/import/iidx-flo">
<KAIIntegrationPage
hash="6f64b82107cea90aa4c51a33705cd57c1883c8cdc22a634730ca461a431744b3"
@@ -1,7 +1,7 @@
import { ErrorPage } from "app/pages/ErrorPage";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { useHistory, useParams, generatePath } from "react-router-dom";
import { generatePath, useHistory, useParams } from "react-router-dom";
import { JustChildren } from "types/react";
export default function RequireAuthAsUserParam({ children }: JustChildren) {
@@ -0,0 +1,123 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import UpdateUserGameStats from "components/util/UpdateUserGameStats";
import { UserContext } from "context/UserContext";
import { UserGameStatsContext } from "context/UserGameStatsContext";
import React, { useContext, useState } from "react";
import { Alert, Button } from "react-bootstrap";
import { Link } from "react-router-dom";
import {
APIImportTypes,
ARCSavedProfileDocument,
GetGameConfig,
ImportDocument,
} from "tachi-common";
import { ImportStates, NotStartedState } from "types/import";
import { APIFetchV1 } from "util/api";
import ImportStateRenderer from "./ImportStateRenderer";
export default function ARCImportPage({ game }: { game: "sdvx" | "iidx" }) {
const importType: APIImportTypes = game === "iidx" ? "api/arc-iidx" : "api/arc-sdvx";
const { setUGS } = useContext(UserGameStatsContext);
const { user } = useContext(UserContext);
useSetSubheader(["Imports", `ARC ${GetGameConfig(game).name} Synchronisation`]);
if (!user) {
return <>No user? How did you get here!</>;
}
const [importState, setImportState] = useState<ImportStates>(NotStartedState);
const { data, isLoading, error } = useApiQuery<{
iidx: ARCSavedProfileDocument | null;
sdvx: ARCSavedProfileDocument | null;
}>(`/users/${user.id}/integrations/arc`);
if (error) {
return <ApiError error={error} />;
}
if (!data || isLoading) {
return <Loading />;
}
if (!data[game]) {
return (
<div>
<h2>You are not authenticated for ARC {GetGameConfig(game).name}.</h2>
<br />
<h4>
Please go to the{" "}
<Link to={`/users/${user.id}/integrations`}>Service Settings for ARC</Link>, and
link your account.
</h4>
</div>
);
}
return (
<div>
<h2 className="text-center mb-4">Authenticated with ARC!</h2>
<Divider />
<div className="d-flex w-100 justify-content-center">
<Button
className="mx-auto"
variant="primary"
onClick={async () => {
setImportState({ state: "waiting" });
const res = await APIFetchV1<ImportDocument>(
"/import/from-api",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
importType,
}),
},
true,
true
);
if (res.success) {
setImportState({ state: "done", import: res.body });
UpdateUserGameStats(setUGS);
} else {
setImportState({ state: "failed", error: res.description });
}
}}
disabled={importState.state === "waiting"}
>
{importState.state === "waiting" ? "Syncing..." : "Click to Sync!"}
</Button>
</div>
<Divider />
<Alert variant="warning">
Play on ARC a lot? Please consider setting up{" "}
{game === "iidx" ? (
<Link style={{ color: "purple" }} to="/dashboard/import/fervidex">
Fervidex
</Link>
) : (
<Link style={{ color: "purple" }} to="/dashboard/import/barbatos">
Barbatos
</Link>
)}{" "}
instead.
<br />
ARC exports poor quality data, and can only save one score on a chart at a time.
<br />
In an ideal world, you should only have to use this sync once, and then should
switch to an import method that exports scores better!
</Alert>
<Divider />
<ImportStateRenderer state={importState} />
</div>
);
}
@@ -14,7 +14,6 @@ import { Button, Form, InputGroup } from "react-bootstrap";
import { APIImportTypes, GetGameConfig, ImportDocument } from "tachi-common";
import { ImportStates, NotStartedState } from "types/import";
import { APIFetchV1 } from "util/api";
import ImportInfo from "./ImportInfo";
import ImportStateRenderer from "./ImportStateRenderer";
interface Props {
@@ -165,27 +165,29 @@ export default function TachiTable<D>({
<div className="row">
<div className="col-lg-4 align-self-center">{displayStr}</div>
<div className="d-none d-lg-flex col-lg-4 justify-content-center align-items-center">
<Button
onClick={async () => {
let data = dataset;
if (search !== "") {
data = filteredDataset;
}
{settings?.preferences.advancedMode && (
<Button
onClick={async () => {
let data = dataset;
if (search !== "") {
data = filteredDataset;
}
CopyToClipboard(
// Faulty types with json2csv.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await json2csvAsync(data as any, {
sortHeader: true,
})
);
}}
variant="outline-info"
className="w-50"
>
<Icon type="table" />
Export {search !== "" ? "Filtered Data" : "Table"} (CSV)
</Button>
CopyToClipboard(
// Faulty types with json2csv.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await json2csvAsync(data as any, {
sortHeader: true,
})
);
}}
variant="outline-info"
className="w-50"
>
<Icon type="table" />
Export {search !== "" ? "Filtered Data" : "Table"} (CSV)
</Button>
)}
{settings?.preferences.developerMode && (
<Button
className="ml-4 w-50"
@@ -1,7 +1,6 @@
import React from "react";
import { Button } from "react-bootstrap";
import { ButtonVariant } from "react-bootstrap/esm/types";
import { JustChildren, SetState } from "types/react";
import { JustChildren } from "types/react";
import { DoesMatchRoute } from "util/routing";
import LinkButton from "./LinkButton";
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { createContext, useState } from "react";
import { PublicUserDocument, UGPTSettings } from "tachi-common";
import { UGPTSettings } from "tachi-common";
import { JustChildren, SetState } from "types/react";
/**
+3 -3
View File
@@ -2,15 +2,15 @@
* Create React App entry point. This and `public/index.html` files can not be
* changed or moved.
*/
import "@fortawesome/fontawesome-free/css/all.min.css";
import React from "react";
import ReactDOM from "react-dom";
import "socicon/css/socicon.css";
import App from "./app/App";
import "./index.scss";
import "./_metronic/_assets/plugins/keenthemes-icons/font/ki.css";
import "socicon/css/socicon.css";
import "@fortawesome/fontawesome-free/css/all.min.css";
import "./_metronic/_assets/plugins/flaticon/flaticon.css";
import "./_metronic/_assets/plugins/flaticon2/flaticon.css";
import "./_metronic/_assets/plugins/keenthemes-icons/font/ki.css";
const { PUBLIC_URL } = process.env;
+1
View File
@@ -83,6 +83,7 @@ export const loggedInSplashes = [
"You wish you know who the hibble gibble gobbler was.",
`The current day is ${FormatDate(Date.now())}, probably.`,
"I have always thought of myself as an ambassador for the dance game community.",
"When lava pours out near the sea surface, tremendous volcanic explosions sometimes occur.",
];
// Stuff that's bad for the kids, you know?