fix: stupid crlfisms

This commit is contained in:
zkldi
2023-02-20 01:54:37 +00:00
parent 6addf80ad7
commit 1a135e8274
23 changed files with 1524 additions and 1524 deletions
+4 -4
View File
@@ -1,5 +1,5 @@
# Tachi Bot
This is a discord bot that interacts with an instance of Tachi.
# Tachi Bot
This is a discord bot that interacts with an instance of Tachi.
It's currently not documented, so you'll have to read the source to figure things out. Sorry!
+38 -38
View File
@@ -1,39 +1,39 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"baseUrl": "src",
"outDir": "js",
"lib": [
"DOM" // hack workaround. URLSearchParams must be imported, yet
// discordJS carelessly uses it without being imported
// as if it were a DOM type.
// Sad!
],
"types": [
"node",
],
"paths": {
"tachi-common": [
"../../common/src"
],
"tachi-common/*": [
"../../common/src/*"
]
},
"composite": true,
"tsBuildInfoFile": ".tsbuildinfo"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.test.ts"
],
"references": [
{
"path": "../common"
}
],
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"baseUrl": "src",
"outDir": "js",
"lib": [
"DOM" // hack workaround. URLSearchParams must be imported, yet
// discordJS carelessly uses it without being imported
// as if it were a DOM type.
// Sad!
],
"types": [
"node",
],
"paths": {
"tachi-common": [
"../../common/src"
],
"tachi-common/*": [
"../../common/src/*"
]
},
"composite": true,
"tsBuildInfoFile": ".tsbuildinfo"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.test.ts"
],
"references": [
{
"path": "../common"
}
],
}
+46 -46
View File
@@ -1,46 +1,46 @@
import { MaterialThemeProvider } from "components/layout/misc/MaterialThemeProvider";
import { LoadingScreen } from "components/layout/screens/LoadingScreen";
import { AllLUGPTStatsContextProvider } from "context/AllLUGPTStatsContext";
import { BannedContextProvider } from "context/BannedContext";
import { NotificationsContextProvider } from "context/NotificationsContext";
import { SubheaderContextProvider } from "context/SubheaderContext";
import { UserContextProvider } from "context/UserContext";
import { UserSettingsContextProvider } from "context/UserSettingsContext";
import React from "react";
import { Toaster } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "react-query";
import { BrowserRouter } from "react-router-dom";
import { Routes } from "./routes/AppRoutes";
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 1000 * 60 * 5, retry: false } },
});
export default function App({ basename }: { basename: string }) {
return (
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BannedContextProvider>
<UserContextProvider>
<NotificationsContextProvider>
<UserSettingsContextProvider>
<AllLUGPTStatsContextProvider>
<LoadingScreen>
<BrowserRouter basename={basename}>
<MaterialThemeProvider>
<Toaster />
<SubheaderContextProvider>
<Routes />
</SubheaderContextProvider>
</MaterialThemeProvider>
</BrowserRouter>
</LoadingScreen>
</AllLUGPTStatsContextProvider>
</UserSettingsContextProvider>
</NotificationsContextProvider>
</UserContextProvider>
</BannedContextProvider>
</QueryClientProvider>
</React.StrictMode>
);
}
import { MaterialThemeProvider } from "components/layout/misc/MaterialThemeProvider";
import { LoadingScreen } from "components/layout/screens/LoadingScreen";
import { AllLUGPTStatsContextProvider } from "context/AllLUGPTStatsContext";
import { BannedContextProvider } from "context/BannedContext";
import { NotificationsContextProvider } from "context/NotificationsContext";
import { SubheaderContextProvider } from "context/SubheaderContext";
import { UserContextProvider } from "context/UserContext";
import { UserSettingsContextProvider } from "context/UserSettingsContext";
import React from "react";
import { Toaster } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "react-query";
import { BrowserRouter } from "react-router-dom";
import { Routes } from "./routes/AppRoutes";
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 1000 * 60 * 5, retry: false } },
});
export default function App({ basename }: { basename: string }) {
return (
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BannedContextProvider>
<UserContextProvider>
<NotificationsContextProvider>
<UserSettingsContextProvider>
<AllLUGPTStatsContextProvider>
<LoadingScreen>
<BrowserRouter basename={basename}>
<MaterialThemeProvider>
<Toaster />
<SubheaderContextProvider>
<Routes />
</SubheaderContextProvider>
</MaterialThemeProvider>
</BrowserRouter>
</LoadingScreen>
</AllLUGPTStatsContextProvider>
</UserSettingsContextProvider>
</NotificationsContextProvider>
</UserContextProvider>
</BannedContextProvider>
</QueryClientProvider>
</React.StrictMode>
);
}
+83 -83
View File
@@ -1,83 +1,83 @@
import { ToCDNURL } from "util/api";
import { HistorySafeGoBack } from "util/misc";
import useSetSubheader from "components/layout/header/useSetSubheader";
import React from "react";
import { useHistory } from "react-router-dom";
export function ErrorPage({
statusCode,
customMessage,
}: {
statusCode: number;
customMessage?: string;
}) {
useSetSubheader(statusCode ? statusCode.toString() : "Completely Screwed.");
let message;
const history = useHistory();
if (!customMessage) {
switch (statusCode) {
case 400:
message = "The server refused this request.";
break;
case 401:
message = "Looks like you're not authorised. Try logging in.";
break;
case 403:
message = "Looks like you're not allowed to be here.";
break;
case 404:
message = "This page does not exist. If you think it should exist, let us know!";
break;
case 429:
message = "You're being rate limited. If this was unexpected, please report this.";
break;
case 500:
message = "Looks like the server has failed. This has been reported!";
break;
case undefined:
message =
"Looks like the server has completely crashed. Ah well. Try reloading, but there's no promises.";
break;
default:
message = "An unexpected error has occurred. This has been reported!";
break;
}
} else {
message = customMessage;
}
return (
<div
className="fullscreen text-center"
style={
statusCode === 999
? {
background: "red",
backgroundImage: `url(${ToCDNURL("/eggs/michael.png")})`,
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
backgroundSize: "cover",
}
: undefined
}
>
<div className="d-flex flex-column flex-root">
<div className="d-flex flex-row-fluid flex-column bgi-size-cover bgi-position-center bgi-no-repeat p-10 p-sm-30">
<h1
className="font-size-sm-100 font-weight-boldest mt-15"
style={{ fontSize: "150px" }}
>
{statusCode || "Completely Screwed."}
</h1>
<p className="font-size-h3 font-weight-light">{message}</p>
<a className="text-primary" onClick={() => HistorySafeGoBack(history)}>
Go Back.
</a>
</div>
</div>
</div>
);
}
import { ToCDNURL } from "util/api";
import { HistorySafeGoBack } from "util/misc";
import useSetSubheader from "components/layout/header/useSetSubheader";
import React from "react";
import { useHistory } from "react-router-dom";
export function ErrorPage({
statusCode,
customMessage,
}: {
statusCode: number;
customMessage?: string;
}) {
useSetSubheader(statusCode ? statusCode.toString() : "Completely Screwed.");
let message;
const history = useHistory();
if (!customMessage) {
switch (statusCode) {
case 400:
message = "The server refused this request.";
break;
case 401:
message = "Looks like you're not authorised. Try logging in.";
break;
case 403:
message = "Looks like you're not allowed to be here.";
break;
case 404:
message = "This page does not exist. If you think it should exist, let us know!";
break;
case 429:
message = "You're being rate limited. If this was unexpected, please report this.";
break;
case 500:
message = "Looks like the server has failed. This has been reported!";
break;
case undefined:
message =
"Looks like the server has completely crashed. Ah well. Try reloading, but there's no promises.";
break;
default:
message = "An unexpected error has occurred. This has been reported!";
break;
}
} else {
message = customMessage;
}
return (
<div
className="fullscreen text-center"
style={
statusCode === 999
? {
background: "red",
backgroundImage: `url(${ToCDNURL("/eggs/michael.png")})`,
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
backgroundSize: "cover",
}
: undefined
}
>
<div className="d-flex flex-column flex-root">
<div className="d-flex flex-row-fluid flex-column bgi-size-cover bgi-position-center bgi-no-repeat p-10 p-sm-30">
<h1
className="font-size-sm-100 font-weight-boldest mt-15"
style={{ fontSize: "150px" }}
>
{statusCode || "Completely Screwed."}
</h1>
<p className="font-size-h3 font-weight-light">{message}</p>
<a className="text-primary" onClick={() => HistorySafeGoBack(history)}>
Go Back.
</a>
</div>
</div>
</div>
);
}
+281 -281
View File
@@ -1,281 +1,281 @@
import { APIFetchV1 } from "util/api";
import { ChangeOpacity } from "util/color-opacity";
import { CreateGoalMap } from "util/data";
import { RFA } from "util/misc";
import { NumericSOV } from "util/sorts";
import { heySplashes } from "util/splashes";
import Activity from "components/activity/Activity";
import DashboardActivity from "components/dashboard/DashboardActivity";
import { DashboardHeader } from "components/dashboard/DashboardHeader";
import useSetSubheader from "components/layout/header/useSetSubheader";
import SessionCard from "components/sessions/SessionCard";
import ApiError from "components/util/ApiError";
import AsyncLoader from "components/util/AsyncLoader";
import Divider from "components/util/Divider";
import GoalLink from "components/util/GoalLink";
import LinkButton from "components/util/LinkButton";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { UserContext } from "context/UserContext";
import { UserSettingsContext } from "context/UserSettingsContext";
import { ColourConfig, TachiConfig } from "lib/config";
import React, { useContext, useMemo } from "react";
import { Alert } from "react-bootstrap";
import { Link, Route, Switch } from "react-router-dom";
import { COLOUR_SET, GetGameConfig, UserDocument } from "tachi-common";
import { UGSWithRankingData, UserRecentSummary } from "types/api-returns";
import { GameStatContainer } from "./users/UserGamesPage";
export function DashboardPage() {
const { settings } = useContext(UserSettingsContext);
useSetSubheader("Home", [settings]);
const { user } = useContext(UserContext);
if (!user) {
return <DashboardNotLoggedIn />;
}
return <DashboardLoggedIn user={user} />;
}
function DashboardLoggedIn({ user }: { user: UserDocument }) {
const splash = useMemo(() => RFA(heySplashes), [user]);
return (
<div>
<span className="display-4">
{splash}, {user.username}.
</span>
<div className="card my-4">
<DashboardHeader />
</div>
<Divider />
<RecentInfo user={user} />
<Switch>
<Route exact path="/">
<DashboardActivity user={user} />
</Route>
<Route exact path="/profiles">
<UserGameStatsInfo user={user} />
</Route>
<Route exact path="/global-activity">
<Activity url="/activity" />
</Route>
</Switch>
</div>
);
}
function RecentInfo({ user }: { user: UserDocument }) {
const { data, error } = useApiQuery<UserRecentSummary>(`/users/${user.id}/recent-summary`);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
const folderInfoMap = new Map();
for (const folderInfo of data.recentFolderStats) {
folderInfoMap.set(folderInfo.folderID, folderInfo);
}
const goalMap = CreateGoalMap(data.recentGoals);
return (
<>
{data.recentSessions.length !== 0 && (
<>
<Alert
style={{
backgroundColor: `${ColourConfig.primary}44`,
color: "white",
}}
>
<div className="text-center">
<h1>Today's Summary</h1>
You've gotten <b>{data.recentPlaycount}</b> new score
{data.recentPlaycount !== 1 ? "s" : ""} today!
</div>
</Alert>
<Divider />
<h1>New Sessions</h1>
You've had <b>{data.recentSessions.length}</b> session
{data.recentSessions.length !== 1 ? "s" : ""} today!
<Divider />
{data.recentSessions.sort(NumericSOV((x) => x.timeEnded, true)).map((e) => (
<>
<SessionCard sessionID={e.sessionID} key={e.sessionID} />
<Divider />
</>
))}
</>
)}
{data.recentAchievedGoals.length !== 0 && (
<>
<Alert
style={{
backgroundColor: ChangeOpacity(COLOUR_SET.gold, 0.2),
}}
>
<div className="text-center text-white">
<h1>
{RFA([
"Sweet!",
"Nice!",
"Lookin' good!",
"Good Stuff!",
"owo",
"Cool!",
"Awesome!",
"Epic!",
])}
</h1>
You've achieved <b>{data.recentAchievedGoals.length}</b> new goal
{data.recentAchievedGoals.length !== 1 ? "s" : ""} today!
</div>
<Divider />
<div className="text-white">
<ul>
{data.recentAchievedGoals.map((e) => {
const goal = goalMap.get(e.goalID);
if (!goal) {
return <span>whoops, couldn't find this goal.</span>;
}
return (
<li key={e.goalID}>
<GoalLink goal={goal} />
</li>
);
})}
</ul>
</div>
</Alert>
<Divider />
</>
)}
{/* {data.recentFolders.length !== 0 && (
<>
<h1>Here's some folders you checked out recently.</h1>
<Divider />
<div className="row">
{data.recentFolders.map((e) => (
<FolderInfoComponent
key={e.folderID}
folder={e}
game={e.game}
playtype={e.playtype}
reqUser={user}
folderStats={folderInfoMap.get(e.folderID)!}
/>
))}
</div>
<Divider />
</>
)} */}
</>
);
}
function UserGameStatsInfo({ user }: { user: UserDocument }) {
return (
<div className="row">
<AsyncLoader
promiseFn={async () => {
const res = await APIFetchV1<UGSWithRankingData[]>(
`/users/${user.id}/game-stats`
);
if (!res.success) {
throw new Error(res.description);
}
return res.body.sort((a, b) => {
if (a.game === b.game) {
const gameConfig = GetGameConfig(a.game);
return (
gameConfig.playtypes.indexOf(a.playtype) -
gameConfig.playtypes.indexOf(b.playtype)
);
}
const i1 = TachiConfig.games.indexOf(a.game);
const i2 = TachiConfig.games.indexOf(b.game);
return i1 - i2;
});
}}
>
{(ugs) =>
ugs.map((e) => (
<GameStatContainer ugs={e} reqUser={user} key={`${e.game}:${e.playtype}`} />
))
}
</AsyncLoader>
</div>
);
}
function DashboardNotLoggedIn() {
return (
<div style={{ fontSize: "1.3rem" }}>
<h1 className="mb-4">Welcome to {TachiConfig.name}!</h1>
<h4>
Looks like you're not logged in. If you've got an account,{" "}
<Link to="/login">Login!</Link>
</h4>
<Divider />
<h1 className="my-4">I'm New Around Here, What is this?</h1>
<span>
<b>{TachiConfig.name}</b> is a Rhythm Game Score Tracker. That means we...
</span>
<Divider />
<FeatureContainer
tagline="Track Your Scores."
description={`${TachiConfig.name} supports a bunch of your favourite games, and integrates with many existing services to make sure no score is lost to the void. Furthermore, it's backed by an Open-Source API, so your scores are always available!`}
/>
<FeatureContainer
tagline="Analyse Your Scores."
description={`${TachiConfig.name} analyses your scores for you, breaking them down into all the statistics you'll ever need. No more spreadsheets!`}
/>
<FeatureContainer
tagline="Provide Cool Features."
description={`${TachiConfig.name} implements the features rhythm gamers already talk about. Break your scores down into sessions, Showcase your best metrics on your profile, study your progress on folders - it's all there, and done for you!`}
/>
<Divider />
<div className="col-12 text-center" style={{ paddingTop: 50, paddingBottom: 50 }}>
Interested? You can register right now for <b>free</b>!
<br />
<LinkButton to="/register" className="mt-4 btn-outline-primary">
Register!
</LinkButton>
</div>
<Divider />
<div className="col-12 text-center" style={{ paddingTop: 25 }}>
Nosey? Here's what our users are up to.
<br />
<div style={{ fontSize: "1.0rem" }}>
<Activity url="/activity" />
</div>
</div>
</div>
);
}
function FeatureContainer({ tagline, description }: { tagline: string; description: string }) {
return (
<div className="row my-4 mb-16" style={{ lineHeight: "1.3", fontSize: "1.15rem" }}>
<div className="col-12 col-lg-6">
<h1 className="display-4">{tagline}</h1>
<span>{description}</span>
</div>
</div>
);
}
import { APIFetchV1 } from "util/api";
import { ChangeOpacity } from "util/color-opacity";
import { CreateGoalMap } from "util/data";
import { RFA } from "util/misc";
import { NumericSOV } from "util/sorts";
import { heySplashes } from "util/splashes";
import Activity from "components/activity/Activity";
import DashboardActivity from "components/dashboard/DashboardActivity";
import { DashboardHeader } from "components/dashboard/DashboardHeader";
import useSetSubheader from "components/layout/header/useSetSubheader";
import SessionCard from "components/sessions/SessionCard";
import ApiError from "components/util/ApiError";
import AsyncLoader from "components/util/AsyncLoader";
import Divider from "components/util/Divider";
import GoalLink from "components/util/GoalLink";
import LinkButton from "components/util/LinkButton";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { UserContext } from "context/UserContext";
import { UserSettingsContext } from "context/UserSettingsContext";
import { ColourConfig, TachiConfig } from "lib/config";
import React, { useContext, useMemo } from "react";
import { Alert } from "react-bootstrap";
import { Link, Route, Switch } from "react-router-dom";
import { COLOUR_SET, GetGameConfig, UserDocument } from "tachi-common";
import { UGSWithRankingData, UserRecentSummary } from "types/api-returns";
import { GameStatContainer } from "./users/UserGamesPage";
export function DashboardPage() {
const { settings } = useContext(UserSettingsContext);
useSetSubheader("Home", [settings]);
const { user } = useContext(UserContext);
if (!user) {
return <DashboardNotLoggedIn />;
}
return <DashboardLoggedIn user={user} />;
}
function DashboardLoggedIn({ user }: { user: UserDocument }) {
const splash = useMemo(() => RFA(heySplashes), [user]);
return (
<div>
<span className="display-4">
{splash}, {user.username}.
</span>
<div className="card my-4">
<DashboardHeader />
</div>
<Divider />
<RecentInfo user={user} />
<Switch>
<Route exact path="/">
<DashboardActivity user={user} />
</Route>
<Route exact path="/profiles">
<UserGameStatsInfo user={user} />
</Route>
<Route exact path="/global-activity">
<Activity url="/activity" />
</Route>
</Switch>
</div>
);
}
function RecentInfo({ user }: { user: UserDocument }) {
const { data, error } = useApiQuery<UserRecentSummary>(`/users/${user.id}/recent-summary`);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
const folderInfoMap = new Map();
for (const folderInfo of data.recentFolderStats) {
folderInfoMap.set(folderInfo.folderID, folderInfo);
}
const goalMap = CreateGoalMap(data.recentGoals);
return (
<>
{data.recentSessions.length !== 0 && (
<>
<Alert
style={{
backgroundColor: `${ColourConfig.primary}44`,
color: "white",
}}
>
<div className="text-center">
<h1>Today's Summary</h1>
You've gotten <b>{data.recentPlaycount}</b> new score
{data.recentPlaycount !== 1 ? "s" : ""} today!
</div>
</Alert>
<Divider />
<h1>New Sessions</h1>
You've had <b>{data.recentSessions.length}</b> session
{data.recentSessions.length !== 1 ? "s" : ""} today!
<Divider />
{data.recentSessions.sort(NumericSOV((x) => x.timeEnded, true)).map((e) => (
<>
<SessionCard sessionID={e.sessionID} key={e.sessionID} />
<Divider />
</>
))}
</>
)}
{data.recentAchievedGoals.length !== 0 && (
<>
<Alert
style={{
backgroundColor: ChangeOpacity(COLOUR_SET.gold, 0.2),
}}
>
<div className="text-center text-white">
<h1>
{RFA([
"Sweet!",
"Nice!",
"Lookin' good!",
"Good Stuff!",
"owo",
"Cool!",
"Awesome!",
"Epic!",
])}
</h1>
You've achieved <b>{data.recentAchievedGoals.length}</b> new goal
{data.recentAchievedGoals.length !== 1 ? "s" : ""} today!
</div>
<Divider />
<div className="text-white">
<ul>
{data.recentAchievedGoals.map((e) => {
const goal = goalMap.get(e.goalID);
if (!goal) {
return <span>whoops, couldn't find this goal.</span>;
}
return (
<li key={e.goalID}>
<GoalLink goal={goal} />
</li>
);
})}
</ul>
</div>
</Alert>
<Divider />
</>
)}
{/* {data.recentFolders.length !== 0 && (
<>
<h1>Here's some folders you checked out recently.</h1>
<Divider />
<div className="row">
{data.recentFolders.map((e) => (
<FolderInfoComponent
key={e.folderID}
folder={e}
game={e.game}
playtype={e.playtype}
reqUser={user}
folderStats={folderInfoMap.get(e.folderID)!}
/>
))}
</div>
<Divider />
</>
)} */}
</>
);
}
function UserGameStatsInfo({ user }: { user: UserDocument }) {
return (
<div className="row">
<AsyncLoader
promiseFn={async () => {
const res = await APIFetchV1<UGSWithRankingData[]>(
`/users/${user.id}/game-stats`
);
if (!res.success) {
throw new Error(res.description);
}
return res.body.sort((a, b) => {
if (a.game === b.game) {
const gameConfig = GetGameConfig(a.game);
return (
gameConfig.playtypes.indexOf(a.playtype) -
gameConfig.playtypes.indexOf(b.playtype)
);
}
const i1 = TachiConfig.games.indexOf(a.game);
const i2 = TachiConfig.games.indexOf(b.game);
return i1 - i2;
});
}}
>
{(ugs) =>
ugs.map((e) => (
<GameStatContainer ugs={e} reqUser={user} key={`${e.game}:${e.playtype}`} />
))
}
</AsyncLoader>
</div>
);
}
function DashboardNotLoggedIn() {
return (
<div style={{ fontSize: "1.3rem" }}>
<h1 className="mb-4">Welcome to {TachiConfig.name}!</h1>
<h4>
Looks like you're not logged in. If you've got an account,{" "}
<Link to="/login">Login!</Link>
</h4>
<Divider />
<h1 className="my-4">I'm New Around Here, What is this?</h1>
<span>
<b>{TachiConfig.name}</b> is a Rhythm Game Score Tracker. That means we...
</span>
<Divider />
<FeatureContainer
tagline="Track Your Scores."
description={`${TachiConfig.name} supports a bunch of your favourite games, and integrates with many existing services to make sure no score is lost to the void. Furthermore, it's backed by an Open-Source API, so your scores are always available!`}
/>
<FeatureContainer
tagline="Analyse Your Scores."
description={`${TachiConfig.name} analyses your scores for you, breaking them down into all the statistics you'll ever need. No more spreadsheets!`}
/>
<FeatureContainer
tagline="Provide Cool Features."
description={`${TachiConfig.name} implements the features rhythm gamers already talk about. Break your scores down into sessions, Showcase your best metrics on your profile, study your progress on folders - it's all there, and done for you!`}
/>
<Divider />
<div className="col-12 text-center" style={{ paddingTop: 50, paddingBottom: 50 }}>
Interested? You can register right now for <b>free</b>!
<br />
<LinkButton to="/register" className="mt-4 btn-outline-primary">
Register!
</LinkButton>
</div>
<Divider />
<div className="col-12 text-center" style={{ paddingTop: 25 }}>
Nosey? Here's what our users are up to.
<br />
<div style={{ fontSize: "1.0rem" }}>
<Activity url="/activity" />
</div>
</div>
</div>
);
}
function FeatureContainer({ tagline, description }: { tagline: string; description: string }) {
return (
<div className="row my-4 mb-16" style={{ lineHeight: "1.3", fontSize: "1.15rem" }}>
<div className="col-12 col-lg-6">
<h1 className="display-4">{tagline}</h1>
<span>{description}</span>
</div>
</div>
);
}
+95 -95
View File
@@ -1,95 +1,95 @@
import { HistorySafeGoBack } from "util/misc";
import { ErrorPage } from "app/pages/ErrorPage";
import ForgotPasswordPage from "app/pages/ForgotPasswordPage";
import LoginPage from "app/pages/LoginPage";
import OAuthRequestAuthPage from "app/pages/OAuthRequestAuthPage";
import RegisterPage from "app/pages/RegisterPage";
import ResetPasswordPage from "app/pages/ResetPasswordPage";
import VerifyEmailPage from "app/pages/VerifyEmailPage";
import CenterPage from "components/util/CenterPage";
import ErrorBoundary from "components/util/ErrorBoundary";
import MainPageTitleContainer from "components/util/MainPageTitleContainer";
import { UserContext } from "context/UserContext";
import { ClientConfig } from "lib/config";
import React, { useContext } from "react";
import { Redirect, Route, Switch, useHistory } from "react-router-dom";
import ClientFileFlowRoutes from "./ClientFileFlowRoutes";
import DashboardRoutes from "./DashboardRoutes";
import OAuth2CallbackRoutes from "./OAuth2CallbackRoutes";
import RedirectLegacyRoutes from "./RedirectLegacyRoutes";
/**
* Core Routes for Tachi-Client.
* These are things that involve redirecting to non-dashboard apps.
*/
export function Routes() {
const { user } = useContext(UserContext);
const history = useHistory();
return (
<ErrorBoundary>
<Switch>
{/* redirects for legacy urls */}
<Route path="/dashboard">
<RedirectLegacyRoutes />
</Route>
<Route path="/michael">
<ErrorPage statusCode={999} customMessage="He has been summoned." />
</Route>
<Route path="/verify-email">
<VerifyEmailPage />
</Route>
<Route path="/oauth2-callback">
<OAuth2CallbackRoutes />
</Route>
<Route path="/oauth/request-auth">
<OAuthRequestAuthPage />
</Route>
<Route path="/client-file-flow">
<ClientFileFlowRoutes />
</Route>
<Route exact path="/login">
{user ? <Redirect to="/" /> : <LoginPage />}
</Route>
<Route exact path="/register">
{user ? <Redirect to="/" /> : <RegisterPage />}
</Route>
<Route exact path="/forgot-password">
{user ? <Redirect to="/" /> : <ForgotPasswordPage />}
</Route>
<Route exact path="/reset-password">
{user ? <Redirect to="/" /> : <ResetPasswordPage />}
</Route>
<Route exact path="/screwed">
{user ? (
<Redirect to="/" />
) : (
<CenterPage>
<MainPageTitleContainer
title="You can't."
desc="If you signed up with a fake email, you're now locked out of this account. Nice one."
/>
<span
onClick={() => HistorySafeGoBack(history)}
tabIndex={4}
className="btn btn-outline-danger"
>
Back
</span>
</CenterPage>
)}
</Route>
<Route path="*">
{!user && ClientConfig.MANDATE_LOGIN ? <LoginPage /> : <DashboardRoutes />}
</Route>
</Switch>
</ErrorBoundary>
);
}
import { HistorySafeGoBack } from "util/misc";
import { ErrorPage } from "app/pages/ErrorPage";
import ForgotPasswordPage from "app/pages/ForgotPasswordPage";
import LoginPage from "app/pages/LoginPage";
import OAuthRequestAuthPage from "app/pages/OAuthRequestAuthPage";
import RegisterPage from "app/pages/RegisterPage";
import ResetPasswordPage from "app/pages/ResetPasswordPage";
import VerifyEmailPage from "app/pages/VerifyEmailPage";
import CenterPage from "components/util/CenterPage";
import ErrorBoundary from "components/util/ErrorBoundary";
import MainPageTitleContainer from "components/util/MainPageTitleContainer";
import { UserContext } from "context/UserContext";
import { ClientConfig } from "lib/config";
import React, { useContext } from "react";
import { Redirect, Route, Switch, useHistory } from "react-router-dom";
import ClientFileFlowRoutes from "./ClientFileFlowRoutes";
import DashboardRoutes from "./DashboardRoutes";
import OAuth2CallbackRoutes from "./OAuth2CallbackRoutes";
import RedirectLegacyRoutes from "./RedirectLegacyRoutes";
/**
* Core Routes for Tachi-Client.
* These are things that involve redirecting to non-dashboard apps.
*/
export function Routes() {
const { user } = useContext(UserContext);
const history = useHistory();
return (
<ErrorBoundary>
<Switch>
{/* redirects for legacy urls */}
<Route path="/dashboard">
<RedirectLegacyRoutes />
</Route>
<Route path="/michael">
<ErrorPage statusCode={999} customMessage="He has been summoned." />
</Route>
<Route path="/verify-email">
<VerifyEmailPage />
</Route>
<Route path="/oauth2-callback">
<OAuth2CallbackRoutes />
</Route>
<Route path="/oauth/request-auth">
<OAuthRequestAuthPage />
</Route>
<Route path="/client-file-flow">
<ClientFileFlowRoutes />
</Route>
<Route exact path="/login">
{user ? <Redirect to="/" /> : <LoginPage />}
</Route>
<Route exact path="/register">
{user ? <Redirect to="/" /> : <RegisterPage />}
</Route>
<Route exact path="/forgot-password">
{user ? <Redirect to="/" /> : <ForgotPasswordPage />}
</Route>
<Route exact path="/reset-password">
{user ? <Redirect to="/" /> : <ResetPasswordPage />}
</Route>
<Route exact path="/screwed">
{user ? (
<Redirect to="/" />
) : (
<CenterPage>
<MainPageTitleContainer
title="You can't."
desc="If you signed up with a fake email, you're now locked out of this account. Nice one."
/>
<span
onClick={() => HistorySafeGoBack(history)}
tabIndex={4}
className="btn btn-outline-danger"
>
Back
</span>
</CenterPage>
)}
</Route>
<Route path="*">
{!user && ClientConfig.MANDATE_LOGIN ? <LoginPage /> : <DashboardRoutes />}
</Route>
</Switch>
</ErrorBoundary>
);
}
+134 -134
View File
@@ -1,134 +1,134 @@
import { APIFetchV1, ToAPIURL } from "util/api";
import SupportMePage from "app/pages/dashboard/misc/SupportMePage";
import { ErrorPage } from "app/pages/ErrorPage";
import PrivacyPolicyPage from "app/pages/PrivacyPolicyPage";
import { Layout } from "components/layout/Layout";
import EmailVerify from "components/layout/misc/EmailVerify";
import DashboardErrorBoundary from "components/util/DashboardErrorBoundary";
import { BannedContext } from "context/BannedContext";
import { UserContext } from "context/UserContext";
import React, { useContext, useEffect, useState } from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import { BackgroundContext } from "context/BackgroundContext";
import NotificationsPage from "app/pages/dashboard/users/NotificationsPage";
import SearchPage from "app/pages/dashboard/search/SearchPage";
import { DashboardPage } from "../pages/dashboard/DashboardPage";
import CreditsPage from "../pages/dashboard/misc/CreditsPage";
import GameRoutes from "./GameRoutes";
import ImportRoutes from "./ImportRoutes";
import UserRoutes from "./UserRoutes";
import UtilRoutes from "./UtilRoutes";
import { RedirectLegacyUserRoutes } from "./RedirectLegacyRoutes";
export default function DashboardRoutes() {
const { user } = useContext(UserContext);
const { banned } = useContext(BannedContext);
const { setBackground } = useContext(BackgroundContext);
const [hasVerifiedEmail, setHasVerifiedEmail] = useState<boolean | null>(null);
useEffect(() => {
if (!user) {
return setHasVerifiedEmail(null);
}
(async () => {
const hasVerified = await APIFetchV1<boolean>(`/users/${user.id}/is-email-verified`);
if (hasVerified.success) {
setHasVerifiedEmail(hasVerified.body);
}
})();
}, [user]);
useEffect(() => {
if (user) {
setBackground(ToAPIURL(`/users/${user.id}/banner`));
} else {
setBackground(null);
}
return () => {
setBackground(null);
};
}, [user]);
if (hasVerifiedEmail === false) {
return (
<Layout>
<EmailVerify setHasVerifiedEmail={setHasVerifiedEmail} />
</Layout>
);
}
if (banned) {
return <ErrorPage statusCode={403} customMessage="You are banned." />;
}
return (
<Layout>
<DashboardErrorBoundary>
<Switch>
{/* this is the easiest (read: least sucky) way of handling */}
{/* these routes */}
<Route exact path={["/", "/profiles", "/global-activity"]}>
<DashboardPage />
</Route>
<Route path="/search">
<SearchPage />
</Route>
<Route exact path="/privacy">
<PrivacyPolicyPage />
</Route>
<Route exact path="/credits">
<CreditsPage />
</Route>
<Route exact path="/support">
<SupportMePage />
</Route>
{/* this used to be called /dashboard/users/username, now it's called /u/username */}
<Route path="/users">
<RedirectLegacyUserRoutes />
</Route>
<Route exact path="/u">
<Redirect to="/" />
</Route>
<Route exact path="/g">
<Redirect to="/" />
</Route>
<Route path="/u/:userID">
<UserRoutes />
</Route>
<Route path="/games/:game">
<GameRoutes />
</Route>
<Route path="/import">
<ImportRoutes />
</Route>
<Route path="/utils">
<UtilRoutes />
</Route>
<Route path="/notifications">
<NotificationsPage />
</Route>
<Route path="*">
<ErrorPage statusCode={404} />
</Route>
</Switch>
</DashboardErrorBoundary>
</Layout>
);
}
import { APIFetchV1, ToAPIURL } from "util/api";
import SupportMePage from "app/pages/dashboard/misc/SupportMePage";
import { ErrorPage } from "app/pages/ErrorPage";
import PrivacyPolicyPage from "app/pages/PrivacyPolicyPage";
import { Layout } from "components/layout/Layout";
import EmailVerify from "components/layout/misc/EmailVerify";
import DashboardErrorBoundary from "components/util/DashboardErrorBoundary";
import { BannedContext } from "context/BannedContext";
import { UserContext } from "context/UserContext";
import React, { useContext, useEffect, useState } from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import { BackgroundContext } from "context/BackgroundContext";
import NotificationsPage from "app/pages/dashboard/users/NotificationsPage";
import SearchPage from "app/pages/dashboard/search/SearchPage";
import { DashboardPage } from "../pages/dashboard/DashboardPage";
import CreditsPage from "../pages/dashboard/misc/CreditsPage";
import GameRoutes from "./GameRoutes";
import ImportRoutes from "./ImportRoutes";
import UserRoutes from "./UserRoutes";
import UtilRoutes from "./UtilRoutes";
import { RedirectLegacyUserRoutes } from "./RedirectLegacyRoutes";
export default function DashboardRoutes() {
const { user } = useContext(UserContext);
const { banned } = useContext(BannedContext);
const { setBackground } = useContext(BackgroundContext);
const [hasVerifiedEmail, setHasVerifiedEmail] = useState<boolean | null>(null);
useEffect(() => {
if (!user) {
return setHasVerifiedEmail(null);
}
(async () => {
const hasVerified = await APIFetchV1<boolean>(`/users/${user.id}/is-email-verified`);
if (hasVerified.success) {
setHasVerifiedEmail(hasVerified.body);
}
})();
}, [user]);
useEffect(() => {
if (user) {
setBackground(ToAPIURL(`/users/${user.id}/banner`));
} else {
setBackground(null);
}
return () => {
setBackground(null);
};
}, [user]);
if (hasVerifiedEmail === false) {
return (
<Layout>
<EmailVerify setHasVerifiedEmail={setHasVerifiedEmail} />
</Layout>
);
}
if (banned) {
return <ErrorPage statusCode={403} customMessage="You are banned." />;
}
return (
<Layout>
<DashboardErrorBoundary>
<Switch>
{/* this is the easiest (read: least sucky) way of handling */}
{/* these routes */}
<Route exact path={["/", "/profiles", "/global-activity"]}>
<DashboardPage />
</Route>
<Route path="/search">
<SearchPage />
</Route>
<Route exact path="/privacy">
<PrivacyPolicyPage />
</Route>
<Route exact path="/credits">
<CreditsPage />
</Route>
<Route exact path="/support">
<SupportMePage />
</Route>
{/* this used to be called /dashboard/users/username, now it's called /u/username */}
<Route path="/users">
<RedirectLegacyUserRoutes />
</Route>
<Route exact path="/u">
<Redirect to="/" />
</Route>
<Route exact path="/g">
<Redirect to="/" />
</Route>
<Route path="/u/:userID">
<UserRoutes />
</Route>
<Route path="/games/:game">
<GameRoutes />
</Route>
<Route path="/import">
<ImportRoutes />
</Route>
<Route path="/utils">
<UtilRoutes />
</Route>
<Route path="/notifications">
<NotificationsPage />
</Route>
<Route path="*">
<ErrorPage statusCode={404} />
</Route>
</Switch>
</DashboardErrorBoundary>
</Layout>
);
}
+45 -45
View File
@@ -1,45 +1,45 @@
import BackgroundImage from "components/layout/misc/BackgroundImage";
import { BackgroundContextProvider } from "context/BackgroundContext";
import React, { useState } from "react";
import { JustChildren } from "types/react";
import { Footer } from "./footer/Footer";
import { Header } from "./header/Header";
import { HeaderMobile } from "./header/HeaderMobile";
import { SubHeader } from "./subheader/SubHeader";
export function Layout({ children }: JustChildren) {
const [mobileShow, setMobileShow] = useState(false);
return (
<>
<HeaderMobile setMobileShow={setMobileShow} mobileShow={mobileShow} />
<div className="d-flex flex-column flex-root">
<div className="d-flex flex-row flex-column-fluid page">
<div
className="d-flex flex-column flex-row-fluid wrapper"
id="kt_wrapper"
style={{ overflowX: "hidden" }}
>
<Header mobileShow={mobileShow} setMobileShow={setMobileShow} />
<BackgroundContextProvider>
<BackgroundImage />
<div
id="kt_content"
className="content d-flex flex-column flex-column-fluid"
>
<SubHeader />
<div className="container">{children}</div>
</div>
</BackgroundContextProvider>
<Footer />
</div>
</div>
</div>
</>
);
}
import BackgroundImage from "components/layout/misc/BackgroundImage";
import { BackgroundContextProvider } from "context/BackgroundContext";
import React, { useState } from "react";
import { JustChildren } from "types/react";
import { Footer } from "./footer/Footer";
import { Header } from "./header/Header";
import { HeaderMobile } from "./header/HeaderMobile";
import { SubHeader } from "./subheader/SubHeader";
export function Layout({ children }: JustChildren) {
const [mobileShow, setMobileShow] = useState(false);
return (
<>
<HeaderMobile setMobileShow={setMobileShow} mobileShow={mobileShow} />
<div className="d-flex flex-column flex-root">
<div className="d-flex flex-row flex-column-fluid page">
<div
className="d-flex flex-column flex-row-fluid wrapper"
id="kt_wrapper"
style={{ overflowX: "hidden" }}
>
<Header mobileShow={mobileShow} setMobileShow={setMobileShow} />
<BackgroundContextProvider>
<BackgroundImage />
<div
id="kt_content"
className="content d-flex flex-column flex-column-fluid"
>
<SubHeader />
<div className="container">{children}</div>
</div>
</BackgroundContextProvider>
<Footer />
</div>
</div>
</div>
</>
);
}
+102 -102
View File
@@ -1,102 +1,102 @@
import { APIFetchV1 } from "util/api";
import Divider from "components/util/Divider";
import ExternalLink from "components/util/ExternalLink";
import { BannedContext } from "context/BannedContext";
import React, { useContext, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { ServerStatus } from "types/api-returns";
export function Footer() {
const [serverVersion, setServerVersion] = useState("Loading...");
const { setBanned } = useContext(BannedContext);
useEffect(() => {
APIFetchV1<ServerStatus>("/status").then((r) => {
if (r.statusCode === 403) {
setBanned(true);
}
if (!r.success) {
setServerVersion("Error Fetching data!");
} else {
setServerVersion(r.body.version);
}
});
}, []);
return (
<>
<Divider className="mt-8" />
<div className="footer py-4 d-flex flex-lg-column" id="kt_footer">
<div className="container d-flex flex-column flex-md-row align-items-center justify-content-between">
<div className="order-2 order-md-1">
{/* is there a better way to do this? mt-md-3 is the intent */}
<ExternalLink
href="https://en.wikipedia.org/wiki/Dummy_(album)"
className="gentle-link"
>
{serverVersion}
</ExternalLink>
</div>
<div className="nav nav-dark order-1 order-md-2 justify-content-center">
<Link
to="/support"
className="nav-link px-3"
onClick={() => window.scrollTo(0, 0)}
>
Support
</Link>
<ExternalLink
href="https://docs.bokutachi.xyz/wiki/rules"
className="nav-link px-3"
>
Rules
</ExternalLink>
<Link
to="/privacy"
className="nav-link px-3"
onClick={() => window.scrollTo(0, 0)}
>
GDPR
</Link>
<Link
to="/credits"
className="nav-link px-3"
onClick={() => window.scrollTo(0, 0)}
>
Credits
</Link>
{process.env.VITE_DISCORD && (
<a
href={process.env.VITE_DISCORD}
target="_blank"
rel="noopener noreferrer"
className="nav-link px-3"
>
Discord
</a>
)}
<a
href="https://github.com/tng-dev/tachi"
target="_blank"
rel="noopener noreferrer"
className="nav-link px-3"
>
Source Code
</a>
<a
href="https://docs.bokutachi.xyz/"
target="_blank"
rel="noopener noreferrer"
className="nav-link px-3"
>
Developer Documentation
</a>
</div>
</div>
</div>
</>
);
}
import { APIFetchV1 } from "util/api";
import Divider from "components/util/Divider";
import ExternalLink from "components/util/ExternalLink";
import { BannedContext } from "context/BannedContext";
import React, { useContext, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { ServerStatus } from "types/api-returns";
export function Footer() {
const [serverVersion, setServerVersion] = useState("Loading...");
const { setBanned } = useContext(BannedContext);
useEffect(() => {
APIFetchV1<ServerStatus>("/status").then((r) => {
if (r.statusCode === 403) {
setBanned(true);
}
if (!r.success) {
setServerVersion("Error Fetching data!");
} else {
setServerVersion(r.body.version);
}
});
}, []);
return (
<>
<Divider className="mt-8" />
<div className="footer py-4 d-flex flex-lg-column" id="kt_footer">
<div className="container d-flex flex-column flex-md-row align-items-center justify-content-between">
<div className="order-2 order-md-1">
{/* is there a better way to do this? mt-md-3 is the intent */}
<ExternalLink
href="https://en.wikipedia.org/wiki/Dummy_(album)"
className="gentle-link"
>
{serverVersion}
</ExternalLink>
</div>
<div className="nav nav-dark order-1 order-md-2 justify-content-center">
<Link
to="/support"
className="nav-link px-3"
onClick={() => window.scrollTo(0, 0)}
>
Support
</Link>
<ExternalLink
href="https://docs.bokutachi.xyz/wiki/rules"
className="nav-link px-3"
>
Rules
</ExternalLink>
<Link
to="/privacy"
className="nav-link px-3"
onClick={() => window.scrollTo(0, 0)}
>
GDPR
</Link>
<Link
to="/credits"
className="nav-link px-3"
onClick={() => window.scrollTo(0, 0)}
>
Credits
</Link>
{process.env.VITE_DISCORD && (
<a
href={process.env.VITE_DISCORD}
target="_blank"
rel="noopener noreferrer"
className="nav-link px-3"
>
Discord
</a>
)}
<a
href="https://github.com/tng-dev/tachi"
target="_blank"
rel="noopener noreferrer"
className="nav-link px-3"
>
Source Code
</a>
<a
href="https://docs.bokutachi.xyz/"
target="_blank"
rel="noopener noreferrer"
className="nav-link px-3"
>
Developer Documentation
</a>
</div>
</div>
</div>
</>
);
}
+36 -36
View File
@@ -1,36 +1,36 @@
import { ToCDNURL } from "util/api";
import React from "react";
import { Link } from "react-router-dom";
import { SetState } from "types/react";
import { HeaderMenu } from "./HeaderMenu";
import { Topbar } from "./Topbar";
export function Header({
mobileShow,
setMobileShow,
}: {
mobileShow: boolean;
setMobileShow: SetState<boolean>;
}) {
return (
<div className="header header-fixed" id="kt_header">
<div className="container d-flex align-items-stretch justify-content-between">
<div className="d-flex align-items-stretch mr-3">
<div className="header-logo">
<Link to="/">
<img
className="logo-default max-h-40px"
alt="Logo"
src={ToCDNURL("/logos/logo-mark.png")}
/>
</Link>
</div>
<HeaderMenu mobileShow={mobileShow} setMobileShow={setMobileShow} />
</div>
<Topbar />
</div>
</div>
);
}
import { ToCDNURL } from "util/api";
import React from "react";
import { Link } from "react-router-dom";
import { SetState } from "types/react";
import { HeaderMenu } from "./HeaderMenu";
import { Topbar } from "./Topbar";
export function Header({
mobileShow,
setMobileShow,
}: {
mobileShow: boolean;
setMobileShow: SetState<boolean>;
}) {
return (
<div className="header header-fixed" id="kt_header">
<div className="container d-flex align-items-stretch justify-content-between">
<div className="d-flex align-items-stretch mr-3">
<div className="header-logo">
<Link to="/">
<img
className="logo-default max-h-40px"
alt="Logo"
src={ToCDNURL("/logos/logo-mark.png")}
/>
</Link>
</div>
<HeaderMenu mobileShow={mobileShow} setMobileShow={setMobileShow} />
</div>
<Topbar />
</div>
</div>
);
}
+171 -171
View File
@@ -1,171 +1,171 @@
import { APIFetchV1, ToCDNURL } from "util/api";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import useComponentVisible from "components/util/useComponentVisible";
import { AllLUGPTStatsContext } from "context/AllLUGPTStatsContext";
import { UserContext } from "context/UserContext";
import { UserSettingsContext } from "context/UserSettingsContext";
import { TachiConfig } from "lib/config";
import React, { useContext, useEffect } from "react";
import { Button } from "react-bootstrap";
import toast from "react-hot-toast";
import { useQuery } from "react-query";
import { Link } from "react-router-dom";
import { FormatGame, GetGameConfig, UserGameStats } from "tachi-common";
import { SetState } from "types/react";
import AllGames from "./AllGames";
import ImportScoresLink from "./ImportScoresLink";
import MenuDropdown from "./MenuDropdown";
import MenuLink from "./MenuLink";
import UtilsDropdown from "./UtilsDropdown";
export function HeaderMenu({
mobileShow,
setMobileShow,
}: {
mobileShow: boolean;
setMobileShow: SetState<boolean>;
}) {
const { user, setUser } = useContext(UserContext);
const { ugs, setUGS } = useContext(AllLUGPTStatsContext);
const { settings } = useContext(UserSettingsContext);
const { isLoading, error } = useQuery([user?.id, "game_stats"], async () => {
if (!user) {
return null;
}
const res = await APIFetchV1<UserGameStats[]>("/users/me/game-stats");
if (!res.success) {
throw res;
}
return setUGS(res.body);
});
if (error) {
console.error(error);
}
const { ref, isComponentVisible, setIsComponentVisible } = useComponentVisible(mobileShow);
// how does this not cause a problem. What?
useEffect(() => {
setIsComponentVisible(mobileShow);
}, [mobileShow]);
useEffect(() => {
setMobileShow(isComponentVisible);
}, [isComponentVisible]);
if (isLoading) {
return <Loading />;
}
const userProfileLinks = [];
if (user && ugs && ugs.length !== 0) {
const ugsMap = new Map();
for (const s of ugs) {
ugsMap.set(`${s.game}:${s.playtype}`, s);
}
for (const game of TachiConfig.games) {
for (const playtype of GetGameConfig(game).playtypes) {
const e = ugsMap.get(`${game}:${playtype}`);
if (!e) {
continue;
}
userProfileLinks.push(
<MenuLink
key={`${e.game}:${e.playtype}`}
name={FormatGame(e.game, e.playtype)}
to={`/u/${user.username}/games/${e.game}/${e.playtype}`}
/>
);
}
}
}
return (
<div
className={`header-menu-wrapper header-menu-wrapper-left ${
isComponentVisible ? "header-menu-wrapper-on" : ""
}`}
ref={ref}
id="kt_header_menu_wrapper"
>
<div className="d-flex h-100" style={{ flexDirection: "column" }}>
<div
id="kt_header_menu"
className="header-menu header-menu-left header-menu-mobile header-menu-layout-default header-menu-root-arrow"
>
<div className="d-lg-none">
<Link to="/">
<img
src={ToCDNURL("/logos/logo-wordmark.png")}
width="100%"
className="px-10 mt-4"
/>
</Link>
<div className="mt-4">
<Divider />
</div>
</div>
<ul className="menu-nav">
{user && ugs && ugs.length !== 0 && (
<MenuDropdown name="Your Profiles">{userProfileLinks}</MenuDropdown>
)}
<AllGames />
{user && <ImportScoresLink />}
{settings?.preferences.developerMode && <UtilsDropdown />}
</ul>
</div>
{user && (
<div
className="d-flex d-lg-none"
style={{ paddingLeft: "30px", marginBottom: "2rem", flex: "1 1 auto" }}
>
<div>
<Divider />
</div>
<Button
style={{ alignSelf: "flex-end" }}
variant="outline-danger"
onClick={async () => {
if (confirm("Are you sure you want to sign out?")) {
const rj = await APIFetchV1("/auth/logout", {
method: "POST",
});
if (rj.success) {
toast.success("Logged out.");
setTimeout(() => {
setUser(null);
localStorage.removeItem("isLoggedIn");
// This has to be the case.
// Otherwise, react just ruins its own
// state. I hate react state.
window.location.href = "/";
}, 500);
}
}
}}
>
<Icon type="sign-out-alt" />
Sign Out
</Button>
</div>
)}
</div>
</div>
);
}
import { APIFetchV1, ToCDNURL } from "util/api";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import useComponentVisible from "components/util/useComponentVisible";
import { AllLUGPTStatsContext } from "context/AllLUGPTStatsContext";
import { UserContext } from "context/UserContext";
import { UserSettingsContext } from "context/UserSettingsContext";
import { TachiConfig } from "lib/config";
import React, { useContext, useEffect } from "react";
import { Button } from "react-bootstrap";
import toast from "react-hot-toast";
import { useQuery } from "react-query";
import { Link } from "react-router-dom";
import { FormatGame, GetGameConfig, UserGameStats } from "tachi-common";
import { SetState } from "types/react";
import AllGames from "./AllGames";
import ImportScoresLink from "./ImportScoresLink";
import MenuDropdown from "./MenuDropdown";
import MenuLink from "./MenuLink";
import UtilsDropdown from "./UtilsDropdown";
export function HeaderMenu({
mobileShow,
setMobileShow,
}: {
mobileShow: boolean;
setMobileShow: SetState<boolean>;
}) {
const { user, setUser } = useContext(UserContext);
const { ugs, setUGS } = useContext(AllLUGPTStatsContext);
const { settings } = useContext(UserSettingsContext);
const { isLoading, error } = useQuery([user?.id, "game_stats"], async () => {
if (!user) {
return null;
}
const res = await APIFetchV1<UserGameStats[]>("/users/me/game-stats");
if (!res.success) {
throw res;
}
return setUGS(res.body);
});
if (error) {
console.error(error);
}
const { ref, isComponentVisible, setIsComponentVisible } = useComponentVisible(mobileShow);
// how does this not cause a problem. What?
useEffect(() => {
setIsComponentVisible(mobileShow);
}, [mobileShow]);
useEffect(() => {
setMobileShow(isComponentVisible);
}, [isComponentVisible]);
if (isLoading) {
return <Loading />;
}
const userProfileLinks = [];
if (user && ugs && ugs.length !== 0) {
const ugsMap = new Map();
for (const s of ugs) {
ugsMap.set(`${s.game}:${s.playtype}`, s);
}
for (const game of TachiConfig.games) {
for (const playtype of GetGameConfig(game).playtypes) {
const e = ugsMap.get(`${game}:${playtype}`);
if (!e) {
continue;
}
userProfileLinks.push(
<MenuLink
key={`${e.game}:${e.playtype}`}
name={FormatGame(e.game, e.playtype)}
to={`/u/${user.username}/games/${e.game}/${e.playtype}`}
/>
);
}
}
}
return (
<div
className={`header-menu-wrapper header-menu-wrapper-left ${
isComponentVisible ? "header-menu-wrapper-on" : ""
}`}
ref={ref}
id="kt_header_menu_wrapper"
>
<div className="d-flex h-100" style={{ flexDirection: "column" }}>
<div
id="kt_header_menu"
className="header-menu header-menu-left header-menu-mobile header-menu-layout-default header-menu-root-arrow"
>
<div className="d-lg-none">
<Link to="/">
<img
src={ToCDNURL("/logos/logo-wordmark.png")}
width="100%"
className="px-10 mt-4"
/>
</Link>
<div className="mt-4">
<Divider />
</div>
</div>
<ul className="menu-nav">
{user && ugs && ugs.length !== 0 && (
<MenuDropdown name="Your Profiles">{userProfileLinks}</MenuDropdown>
)}
<AllGames />
{user && <ImportScoresLink />}
{settings?.preferences.developerMode && <UtilsDropdown />}
</ul>
</div>
{user && (
<div
className="d-flex d-lg-none"
style={{ paddingLeft: "30px", marginBottom: "2rem", flex: "1 1 auto" }}
>
<div>
<Divider />
</div>
<Button
style={{ alignSelf: "flex-end" }}
variant="outline-danger"
onClick={async () => {
if (confirm("Are you sure you want to sign out?")) {
const rj = await APIFetchV1("/auth/logout", {
method: "POST",
});
if (rj.success) {
toast.success("Logged out.");
setTimeout(() => {
setUser(null);
localStorage.removeItem("isLoggedIn");
// This has to be the case.
// Otherwise, react just ruins its own
// state. I hate react state.
window.location.href = "/";
}, 500);
}
}
}}
>
<Icon type="sign-out-alt" />
Sign Out
</Button>
</div>
)}
</div>
</div>
);
}
@@ -1,63 +1,63 @@
import { ToAPIURL } from "util/api";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { Link } from "react-router-dom";
import { SetState } from "types/react";
import { UserNotificationButton } from "./UserNotificationButton";
import { SearchButton } from "./SearchButton";
export function HeaderMobile({
mobileShow,
setMobileShow,
}: {
mobileShow: boolean;
setMobileShow: SetState<boolean>;
}) {
const { user } = useContext(UserContext);
return (
<>
<div id="kt_header_mobile" className="header-mobile header-mobile-fixed">
<div className="d-flex align-items-center justify-content-start">
<button
className="btn p-0 burger-icon burger-icon-right ml-2 mr-auto"
id="kt_header_mobile_toggle"
onClick={() => setMobileShow(!mobileShow)}
>
<span />
</button>
</div>
<div className="d-flex align-items-center">
<SearchButton />
{user ? (
<>
<UserNotificationButton user={user} />
<button
className="btn btn-icon btn-hover-transparent-white p-0 ml-3"
id="kt_header_mobile_topbar_toggle"
>
<Link to={`/u/${user.username}`}>
<div className="px-4">
<img
className="rounded"
src={ToAPIURL("/users/me/pfp")}
style={{
width: "30px",
height: "30px",
display: "inline-block",
}}
/>
</div>
</Link>
</button>
</>
) : (
<></>
)}
</div>
</div>
</>
);
}
import { ToAPIURL } from "util/api";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { Link } from "react-router-dom";
import { SetState } from "types/react";
import { UserNotificationButton } from "./UserNotificationButton";
import { SearchButton } from "./SearchButton";
export function HeaderMobile({
mobileShow,
setMobileShow,
}: {
mobileShow: boolean;
setMobileShow: SetState<boolean>;
}) {
const { user } = useContext(UserContext);
return (
<>
<div id="kt_header_mobile" className="header-mobile header-mobile-fixed">
<div className="d-flex align-items-center justify-content-start">
<button
className="btn p-0 burger-icon burger-icon-right ml-2 mr-auto"
id="kt_header_mobile_toggle"
onClick={() => setMobileShow(!mobileShow)}
>
<span />
</button>
</div>
<div className="d-flex align-items-center">
<SearchButton />
{user ? (
<>
<UserNotificationButton user={user} />
<button
className="btn btn-icon btn-hover-transparent-white p-0 ml-3"
id="kt_header_mobile_topbar_toggle"
>
<Link to={`/u/${user.username}`}>
<div className="px-4">
<img
className="rounded"
src={ToAPIURL("/users/me/pfp")}
style={{
width: "30px",
height: "30px",
display: "inline-block",
}}
/>
</div>
</Link>
</button>
</>
) : (
<></>
)}
</div>
</div>
</>
);
}
@@ -1,18 +1,18 @@
import Icon from "components/util/Icon";
import useApiQuery from "components/util/query/useApiQuery";
import { NotificationsContext } from "context/NotificationsContext";
import React, { useContext, useMemo } from "react";
import { Link } from "react-router-dom";
import { NotificationDocument, UserDocument } from "tachi-common";
export function SearchButton() {
return (
<div className="topbar-item">
<Link to="/search">
<div className="btn btn-icon btn-hover-transparent-white btn-dropdown btn-lg mr-1">
<Icon type="search" colour="muted" />
</div>
</Link>
</div>
);
}
import Icon from "components/util/Icon";
import useApiQuery from "components/util/query/useApiQuery";
import { NotificationsContext } from "context/NotificationsContext";
import React, { useContext, useMemo } from "react";
import { Link } from "react-router-dom";
import { NotificationDocument, UserDocument } from "tachi-common";
export function SearchButton() {
return (
<div className="topbar-item">
<Link to="/search">
<div className="btn btn-icon btn-hover-transparent-white btn-dropdown btn-lg mr-1">
<Icon type="search" colour="muted" />
</div>
</Link>
</div>
);
}
+34 -34
View File
@@ -1,34 +1,34 @@
import { UserProfileDropdown } from "components/layout/header/UserProfileDropdown";
import LinkButton from "components/util/LinkButton";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { UserNotificationButton } from "./UserNotificationButton";
import { SearchButton } from "./SearchButton";
export function Topbar() {
const { user } = useContext(UserContext);
return (
<div className="topbar" style={{ minWidth: "35%" }}>
<SearchButton />
{user ? (
<>
<UserNotificationButton user={user} />
<UserProfileDropdown user={user} />
</>
) : (
<>
<div className="topbar-item mr-3">
<LinkButton to="/login" className="btn-outline-primary">
Log In
</LinkButton>
</div>
<div className="topbar-item">
<LinkButton to="/register">Create Account</LinkButton>
</div>
</>
)}
</div>
);
}
import { UserProfileDropdown } from "components/layout/header/UserProfileDropdown";
import LinkButton from "components/util/LinkButton";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { UserNotificationButton } from "./UserNotificationButton";
import { SearchButton } from "./SearchButton";
export function Topbar() {
const { user } = useContext(UserContext);
return (
<div className="topbar" style={{ minWidth: "35%" }}>
<SearchButton />
{user ? (
<>
<UserNotificationButton user={user} />
<UserProfileDropdown user={user} />
</>
) : (
<>
<div className="topbar-item mr-3">
<LinkButton to="/login" className="btn-outline-primary">
Log In
</LinkButton>
</div>
<div className="topbar-item">
<LinkButton to="/register">Create Account</LinkButton>
</div>
</>
)}
</div>
);
}
@@ -1,35 +1,35 @@
import Icon from "components/util/Icon";
import useApiQuery from "components/util/query/useApiQuery";
import { NotificationsContext } from "context/NotificationsContext";
import React, { useContext, useMemo } from "react";
import { Link } from "react-router-dom";
import { NotificationDocument, UserDocument } from "tachi-common";
export function UserNotificationButton({ user }: { user: UserDocument }) {
const { notifications } = useContext(NotificationsContext);
const unread = useMemo(() => {
if (!notifications) {
return 0;
}
return notifications.filter((e) => e.read === false).length;
}, [notifications]);
return (
<div className="topbar-item">
<Link to="/notifications">
<div
className="btn btn-icon btn-hover-transparent-white btn-dropdown btn-lg mr-1"
id="kt_quick_notifications_toggle"
>
{unread > 0 ? (
<Icon type="envelope" colour="primary" />
) : (
<Icon type="envelope" regular colour="muted" />
)}
</div>
</Link>
</div>
);
}
import Icon from "components/util/Icon";
import useApiQuery from "components/util/query/useApiQuery";
import { NotificationsContext } from "context/NotificationsContext";
import React, { useContext, useMemo } from "react";
import { Link } from "react-router-dom";
import { NotificationDocument, UserDocument } from "tachi-common";
export function UserNotificationButton({ user }: { user: UserDocument }) {
const { notifications } = useContext(NotificationsContext);
const unread = useMemo(() => {
if (!notifications) {
return 0;
}
return notifications.filter((e) => e.read === false).length;
}, [notifications]);
return (
<div className="topbar-item">
<Link to="/notifications">
<div
className="btn btn-icon btn-hover-transparent-white btn-dropdown btn-lg mr-1"
id="kt_quick_notifications_toggle"
>
{unread > 0 ? (
<Icon type="envelope" colour="primary" />
) : (
<Icon type="envelope" regular colour="muted" />
)}
</div>
</Link>
</div>
);
}
@@ -1,135 +1,135 @@
import { APIFetchV1, ToAPIURL } from "util/api";
import { RFA } from "util/misc";
import { heySplashes } from "util/splashes";
import Divider from "components/util/Divider";
import DropdownToggleOverride from "components/util/DropdownToggleOverride";
import Icon from "components/util/Icon";
import { UserContext } from "context/UserContext";
import React, { useContext, useState } from "react";
import { Button } from "react-bootstrap";
import Dropdown from "react-bootstrap/Dropdown";
import toast from "react-hot-toast";
import { Link } from "react-router-dom";
import { UserDocument } from "tachi-common";
export function UserProfileDropdown({ user }: { user: UserDocument }) {
const { setUser } = useContext(UserContext);
const [heySplash] = useState(RFA(heySplashes));
return (
<Dropdown drop="down" alignRight>
<Dropdown.Toggle as={DropdownToggleOverride} id="dropdown-toggle-user-profile">
<div
className={
"btn btn-icon btn-hover-transparent-white d-flex align-items-center btn-lg px-md-2 w-md-auto"
}
>
<span className="text-white opacity-70 font-weight-bold font-size-base d-none d-md-inline mr-1">
{heySplash},
</span>{" "}
<span className="text-white opacity-90 font-weight-bolder font-size-base d-none d-md-inline mr-2">
{user.username}
</span>
<span className="symbol symbol-35">
<img alt="Pic" className="hidden" src={ToAPIURL("/users/me/pfp")} />
</span>
</div>
</Dropdown.Toggle>
<Dropdown.Menu className="p-0 m-0 dropdown-menu-right dropdown-menu-anim dropdown-menu-top-unround dropdown-menu-xl">
<div className="navi navi-spacer-x-0 pt-5">
<Link
to={`/u/${user.username}`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="user" colour="primary" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">My Profile</div>
<div className="text-muted">View your profile!</div>
</div>
</div>
</Link>
<Link
to={`/u/${user.username}`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="envelope" colour="success" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">Notifications</div>
<div className="text-muted">View your notifications!</div>
</div>
</div>
</Link>
<Link
to={`/u/${user.username}/settings`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="cog" colour="info" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">
Profile Settings
</div>
<div className="text-muted">
Manage your profile picture, status, and more!
</div>
</div>
</div>
</Link>
<Link
to={`/u/${user.username}/integrations`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="wrench" colour="danger" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">
My Integrations
</div>
<div className="text-muted">
Manage your API Keys and integrations with other services.
</div>
</div>
</div>
</Link>
<Divider />
<div className="navi-footer px-8 py-5 justify-content-end">
<Button
onClick={async () => {
const rj = await APIFetchV1("/auth/logout", {
method: "POST",
});
if (rj.success) {
toast.success("Logged out.");
setTimeout(() => {
setUser(null);
localStorage.removeItem("isLoggedIn");
// This has to be the case.
// Otherwise, react just ruins its own
// state. I hate react state.
window.location.href = "/";
}, 500);
}
}}
className="btn-outline-danger font-weight-bold"
>
Sign Out
</Button>
</div>
</div>
</Dropdown.Menu>
</Dropdown>
);
}
import { APIFetchV1, ToAPIURL } from "util/api";
import { RFA } from "util/misc";
import { heySplashes } from "util/splashes";
import Divider from "components/util/Divider";
import DropdownToggleOverride from "components/util/DropdownToggleOverride";
import Icon from "components/util/Icon";
import { UserContext } from "context/UserContext";
import React, { useContext, useState } from "react";
import { Button } from "react-bootstrap";
import Dropdown from "react-bootstrap/Dropdown";
import toast from "react-hot-toast";
import { Link } from "react-router-dom";
import { UserDocument } from "tachi-common";
export function UserProfileDropdown({ user }: { user: UserDocument }) {
const { setUser } = useContext(UserContext);
const [heySplash] = useState(RFA(heySplashes));
return (
<Dropdown drop="down" alignRight>
<Dropdown.Toggle as={DropdownToggleOverride} id="dropdown-toggle-user-profile">
<div
className={
"btn btn-icon btn-hover-transparent-white d-flex align-items-center btn-lg px-md-2 w-md-auto"
}
>
<span className="text-white opacity-70 font-weight-bold font-size-base d-none d-md-inline mr-1">
{heySplash},
</span>{" "}
<span className="text-white opacity-90 font-weight-bolder font-size-base d-none d-md-inline mr-2">
{user.username}
</span>
<span className="symbol symbol-35">
<img alt="Pic" className="hidden" src={ToAPIURL("/users/me/pfp")} />
</span>
</div>
</Dropdown.Toggle>
<Dropdown.Menu className="p-0 m-0 dropdown-menu-right dropdown-menu-anim dropdown-menu-top-unround dropdown-menu-xl">
<div className="navi navi-spacer-x-0 pt-5">
<Link
to={`/u/${user.username}`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="user" colour="primary" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">My Profile</div>
<div className="text-muted">View your profile!</div>
</div>
</div>
</Link>
<Link
to={`/u/${user.username}`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="envelope" colour="success" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">Notifications</div>
<div className="text-muted">View your notifications!</div>
</div>
</div>
</Link>
<Link
to={`/u/${user.username}/settings`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="cog" colour="info" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">
Profile Settings
</div>
<div className="text-muted">
Manage your profile picture, status, and more!
</div>
</div>
</div>
</Link>
<Link
to={`/u/${user.username}/integrations`}
className="navi-item px-8 cursor-pointer"
>
<div className="navi-link">
<div className="navi-icon mr-2">
<Icon type="wrench" colour="danger" />
</div>
<div className="navi-text">
<div className="font-weight-bold cursor-pointer">
My Integrations
</div>
<div className="text-muted">
Manage your API Keys and integrations with other services.
</div>
</div>
</div>
</Link>
<Divider />
<div className="navi-footer px-8 py-5 justify-content-end">
<Button
onClick={async () => {
const rj = await APIFetchV1("/auth/logout", {
method: "POST",
});
if (rj.success) {
toast.success("Logged out.");
setTimeout(() => {
setUser(null);
localStorage.removeItem("isLoggedIn");
// This has to be the case.
// Otherwise, react just ruins its own
// state. I hate react state.
window.location.href = "/";
}, 500);
}
}}
className="btn-outline-danger font-weight-bold"
>
Sign Out
</Button>
</div>
</div>
</Dropdown.Menu>
</Dropdown>
);
}
@@ -1,30 +1,30 @@
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { ColourConfig } from "lib/config";
import React from "react";
import { JustChildren } from "types/react";
const theme = createTheme({
typography: {
fontFamily: "Poppins",
},
palette: {
secondary: {
main: ColourConfig.primary,
},
primary: {
main: ColourConfig.primary,
},
},
components: {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
},
});
export function MaterialThemeProvider({ children }: JustChildren) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { ColourConfig } from "lib/config";
import React from "react";
import { JustChildren } from "types/react";
const theme = createTheme({
typography: {
fontFamily: "Poppins",
},
palette: {
secondary: {
main: ColourConfig.primary,
},
primary: {
main: ColourConfig.primary,
},
},
components: {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
},
});
export function MaterialThemeProvider({ children }: JustChildren) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
@@ -1,57 +1,57 @@
import { APIFetchV1 } from "util/api";
import { UserContext } from "context/UserContext";
import { TachiConfig } from "lib/config";
import React, { useContext, useEffect, useState } from "react";
import { UserDocument } from "tachi-common";
import { JustChildren } from "types/react";
import { SplashScreen } from "./SplashScreen";
export function LoadingScreen({ children }: JustChildren) {
const [loading, setLoading] = useState(true);
// if we've been waiting around for too long, maybe we're down.
const [broke, setBroke] = useState("");
const { setUser } = useContext(UserContext);
useEffect(() => {
if (!localStorage.getItem("isLoggedIn")) {
setLoading(false);
return;
}
const timeout = setTimeout(
() =>
setBroke(
`It's taking a long time for ${TachiConfig.name} to load. We might be under strain, or completely down, sorry!`
),
4_000
);
Promise.all([
APIFetchV1<UserDocument>("/users/me").then((rj) => {
if (rj.success) {
setUser(rj.body);
} else {
localStorage.removeItem("isLoggedIn");
}
}),
])
.then(() => {
setLoading(false);
clearTimeout(timeout);
})
.catch((err) => {
console.error(err);
setBroke(
`${TachiConfig.name} is currently down, sadly. Check back in a while, or we might be doing a quick server restart. Sorry!`
);
clearTimeout(timeout);
});
}, []);
if (loading) {
return <SplashScreen broke={broke} />;
}
return <>{children}</>;
}
import { APIFetchV1 } from "util/api";
import { UserContext } from "context/UserContext";
import { TachiConfig } from "lib/config";
import React, { useContext, useEffect, useState } from "react";
import { UserDocument } from "tachi-common";
import { JustChildren } from "types/react";
import { SplashScreen } from "./SplashScreen";
export function LoadingScreen({ children }: JustChildren) {
const [loading, setLoading] = useState(true);
// if we've been waiting around for too long, maybe we're down.
const [broke, setBroke] = useState("");
const { setUser } = useContext(UserContext);
useEffect(() => {
if (!localStorage.getItem("isLoggedIn")) {
setLoading(false);
return;
}
const timeout = setTimeout(
() =>
setBroke(
`It's taking a long time for ${TachiConfig.name} to load. We might be under strain, or completely down, sorry!`
),
4_000
);
Promise.all([
APIFetchV1<UserDocument>("/users/me").then((rj) => {
if (rj.success) {
setUser(rj.body);
} else {
localStorage.removeItem("isLoggedIn");
}
}),
])
.then(() => {
setLoading(false);
clearTimeout(timeout);
})
.catch((err) => {
console.error(err);
setBroke(
`${TachiConfig.name} is currently down, sadly. Check back in a while, or we might be doing a quick server restart. Sorry!`
);
clearTimeout(timeout);
});
}, []);
if (loading) {
return <SplashScreen broke={broke} />;
}
return <>{children}</>;
}
@@ -1,13 +1,13 @@
import { CircularProgress } from "@mui/material";
import React from "react";
import SplashImage from "../misc/SplashImage";
export function SplashScreen({ broke }: { broke: string }) {
return (
<div id="splash-screen" className="splash-screen">
<SplashImage />
{!broke && <CircularProgress className="splash-screen-spinner" />}
{broke && <p className="mt-4">{broke}</p>}
</div>
);
}
import { CircularProgress } from "@mui/material";
import React from "react";
import SplashImage from "../misc/SplashImage";
export function SplashScreen({ broke }: { broke: string }) {
return (
<div id="splash-screen" className="splash-screen">
<SplashImage />
{!broke && <CircularProgress className="splash-screen-spinner" />}
{broke && <p className="mt-4">{broke}</p>}
</div>
);
}
@@ -1,32 +1,32 @@
import Divider from "components/util/Divider";
import React, { useContext } from "react";
import { SubheaderContext } from "../../../context/SubheaderContext";
import { Breadcrumbs } from "./components/Breadcrumbs";
export function SubHeader() {
const subheader = useContext(SubheaderContext);
return (
<>
<div id="kt_subheader" className="subheader">
<div className="container d-flex align-items-center justify-content-start">
<div className="d-flex w-100 d-md-none align-items-center justify-content-center">
<h2 className="text-white font-weight-bold">{subheader.title}</h2>
</div>
<div className="d-none d-md-flex flex-wrap mr-1 justify-content-start">
<div className="d-flex flex-column">
<h2 className="text-white font-weight-bold my-2 mr-5">
{subheader.title}
</h2>
<Breadcrumbs items={subheader.breadcrumbs} />
</div>
</div>
</div>
</div>
<div className="container d-flex mb-8 mt-8 w-100">
<Divider className="w-100" />
</div>
</>
);
}
import Divider from "components/util/Divider";
import React, { useContext } from "react";
import { SubheaderContext } from "../../../context/SubheaderContext";
import { Breadcrumbs } from "./components/Breadcrumbs";
export function SubHeader() {
const subheader = useContext(SubheaderContext);
return (
<>
<div id="kt_subheader" className="subheader">
<div className="container d-flex align-items-center justify-content-start">
<div className="d-flex w-100 d-md-none align-items-center justify-content-center">
<h2 className="text-white font-weight-bold">{subheader.title}</h2>
</div>
<div className="d-none d-md-flex flex-wrap mr-1 justify-content-start">
<div className="d-flex flex-column">
<h2 className="text-white font-weight-bold my-2 mr-5">
{subheader.title}
</h2>
<Breadcrumbs items={subheader.breadcrumbs} />
</div>
</div>
</div>
</div>
<div className="container d-flex mb-8 mt-8 w-100">
<Divider className="w-100" />
</div>
</>
);
}
@@ -1,40 +1,40 @@
import React, { Fragment } from "react";
import { Link } from "react-router-dom";
import { GetGameConfig } from "tachi-common";
export function Breadcrumbs({ items }: { items: string[] }) {
const url = location.pathname;
// we also need to remove the first param, as it is an empty string
const parts = url.split("/").slice(1);
return (
<div className="align-items-center font-weight-bold my-2 d-flex">
<Link className="opacity-75 hover-opacity-100" to="/">
<i className="flaticon2-shelter text-white icon-1x" />
</Link>
{items.map((name, index) => {
// Skip playtype in the breadcrumbs if the game only has one playtype.
// @hack
// this only works if the game has one playtype called "Single".
// Some games (like pop'n) have one playtype, but it's called
// 9B. They'll just have to cope.
if (items[index - 2] === "Games" && name === "Single") {
return;
}
return (
<Fragment key={index}>
<span className="label label-dot label-sm bg-white opacity-75 mx-3" />
<Link
className="text-white text-hover-white opacity-75 hover-opacity-100"
to={`/${parts.slice(0, index + 1).join("/")}`}
>
{name}
</Link>
</Fragment>
);
})}
</div>
);
}
import React, { Fragment } from "react";
import { Link } from "react-router-dom";
import { GetGameConfig } from "tachi-common";
export function Breadcrumbs({ items }: { items: string[] }) {
const url = location.pathname;
// we also need to remove the first param, as it is an empty string
const parts = url.split("/").slice(1);
return (
<div className="align-items-center font-weight-bold my-2 d-flex">
<Link className="opacity-75 hover-opacity-100" to="/">
<i className="flaticon2-shelter text-white icon-1x" />
</Link>
{items.map((name, index) => {
// Skip playtype in the breadcrumbs if the game only has one playtype.
// @hack
// this only works if the game has one playtype called "Single".
// Some games (like pop'n) have one playtype, but it's called
// 9B. They'll just have to cope.
if (items[index - 2] === "Games" && name === "Single") {
return;
}
return (
<Fragment key={index}>
<span className="label label-dot label-sm bg-white opacity-75 mx-3" />
<Link
className="text-white text-hover-white opacity-75 hover-opacity-100"
to={`/${parts.slice(0, index + 1).join("/")}`}
>
{name}
</Link>
</Fragment>
);
})}
</div>
);
}
+20 -20
View File
@@ -1,20 +1,20 @@
import { NO_OP } from "util/misc";
import React, { createContext, useState } from "react";
import { JustChildren, SetState } from "types/react";
export const SubheaderContext = createContext({
title: "",
setTitle: NO_OP as SetState<string>,
breadcrumbs: [] as string[],
setBreadcrumbs: NO_OP as SetState<string[]>,
});
export function SubheaderContextProvider({ children }: JustChildren) {
const [title, setTitle] = useState("");
const [breadcrumbs, setBreadcrumbs] = useState<string[]>([]);
return (
<SubheaderContext.Provider value={{ title, setTitle, breadcrumbs, setBreadcrumbs }}>
{children}
</SubheaderContext.Provider>
);
}
import { NO_OP } from "util/misc";
import React, { createContext, useState } from "react";
import { JustChildren, SetState } from "types/react";
export const SubheaderContext = createContext({
title: "",
setTitle: NO_OP as SetState<string>,
breadcrumbs: [] as string[],
setBreadcrumbs: NO_OP as SetState<string[]>,
});
export function SubheaderContextProvider({ children }: JustChildren) {
const [title, setTitle] = useState("");
const [breadcrumbs, setBreadcrumbs] = useState<string[]>([]);
return (
<SubheaderContext.Provider value={{ title, setTitle, breadcrumbs, setBreadcrumbs }}>
{children}
</SubheaderContext.Provider>
);
}
+12 -12
View File
@@ -1,12 +1,12 @@
import "@fortawesome/fontawesome-free/css/all.min.css";
import React from "react";
import ReactDOM from "react-dom";
import "./_style/stub.scss";
import "./_assets/flaticon/flaticon.css";
import "./_assets/flaticon2/flaticon.css";
import "./_assets/keenthemes-icons/font/ki.css";
import App from "app/App";
const { PUBLIC_URL } = process.env;
ReactDOM.render(<App basename={PUBLIC_URL!} />, document.getElementById("root"));
import "@fortawesome/fontawesome-free/css/all.min.css";
import React from "react";
import ReactDOM from "react-dom";
import "./_style/stub.scss";
import "./_assets/flaticon/flaticon.css";
import "./_assets/flaticon2/flaticon.css";
import "./_assets/keenthemes-icons/font/ki.css";
import App from "app/App";
const { PUBLIC_URL } = process.env;
ReactDOM.render(<App basename={PUBLIC_URL!} />, document.getElementById("root"));