Merge pull request 'feat(): Add veritication mark' (#50) from v3 into preview
Reviewed-on: simrail/simrail.alekswilc.dev#50 Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
This commit is contained in:
commit
20c29173f5
@ -4,6 +4,7 @@ import { MBlacklist } from "../../mongo/blacklist.js";
|
|||||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { removeProperties } from "../../util/functions.js";
|
import { removeProperties } from "../../util/functions.js";
|
||||||
import { ILog, MLog } from "../../mongo/logs.js";
|
import { ILog, MLog } from "../../mongo/logs.js";
|
||||||
|
import { MProfile } from "../../mongo/profile.js";
|
||||||
|
|
||||||
|
|
||||||
export class LogRoute
|
export class LogRoute
|
||||||
@ -24,6 +25,7 @@ export class LogRoute
|
|||||||
}
|
}
|
||||||
|
|
||||||
const log = await MLog.findOne({ id }) || await MTrainLog.findOne({ id });
|
const log = await MLog.findOne({ id }) || await MTrainLog.findOne({ id });
|
||||||
|
|
||||||
if (!log)
|
if (!log)
|
||||||
{
|
{
|
||||||
res.status(404).json(new ErrorResponseBuilder()
|
res.status(404).json(new ErrorResponseBuilder()
|
||||||
@ -31,8 +33,13 @@ export class LogRoute
|
|||||||
.setData("Invalid Id parameter").toJSON());
|
.setData("Invalid Id parameter").toJSON());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const profile = await MProfile.findOne({ steam: log.userSteamId });
|
||||||
|
|
||||||
res.status(200).json(new SuccessResponseBuilder().setCode(200).setData(removeProperties<Omit<(ILog | ITrainLog), "_id" | "__v">>(log.toJSON(), [ "_id", "__v" ])));
|
|
||||||
|
res.status(200).json(new SuccessResponseBuilder().setCode(200).setData({
|
||||||
|
verified: profile?.verified,
|
||||||
|
...removeProperties<Omit<(ILog | ITrainLog), "_id" | "__v">>(log.toJSON(), [ "_id", "__v" ])
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
@ -4,6 +4,7 @@ import { MProfile } from "../../mongo/profile.js";
|
|||||||
import { MBlacklist } from "../../mongo/blacklist.js";
|
import { MBlacklist } from "../../mongo/blacklist.js";
|
||||||
import { SteamUtil } from "../../util/SteamUtil.js";
|
import { SteamUtil } from "../../util/SteamUtil.js";
|
||||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
|
import { removeProperties } from "../../util/functions.js";
|
||||||
|
|
||||||
export class ProfilesRoute
|
export class ProfilesRoute
|
||||||
{
|
{
|
||||||
@ -11,7 +12,6 @@ export class ProfilesRoute
|
|||||||
{
|
{
|
||||||
const app = Router();
|
const app = Router();
|
||||||
|
|
||||||
|
|
||||||
app.get("/:id", async (req, res) =>
|
app.get("/:id", async (req, res) =>
|
||||||
{
|
{
|
||||||
if (!req.params.id)
|
if (!req.params.id)
|
||||||
@ -19,13 +19,15 @@ export class ProfilesRoute
|
|||||||
res.redirect("/");
|
res.redirect("/");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = await MProfile.findOne({ steam: req.params.id });
|
const player = await MProfile.findOne({ steam: req.params.id });
|
||||||
if (!player)
|
if (!player)
|
||||||
{
|
{
|
||||||
res.status(404).json(new ErrorResponseBuilder()
|
res.status(404).json(new ErrorResponseBuilder()
|
||||||
.setCode(404).setData("Profile not found! (propably private)"));
|
.setCode(404).setData("Profile not found! (probably private)"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blacklist = await MBlacklist.findOne({ steam: req.params.id! });
|
const blacklist = await MBlacklist.findOne({ steam: req.params.id! });
|
||||||
if (blacklist && blacklist.status)
|
if (blacklist && blacklist.status)
|
||||||
{
|
{
|
||||||
@ -33,23 +35,18 @@ export class ProfilesRoute
|
|||||||
.setCode(403).setData("Profile blacklisted!"));
|
.setCode(403).setData("Profile blacklisted!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const steam = await SteamUtil.getPlayer(player?.steam!);
|
const steam = await SteamUtil.getPlayer(player?.steam!);
|
||||||
const steamStats = await SteamUtil.getPlayerStats(player?.steam!);
|
const steamStats = await SteamUtil.getPlayerStats(player?.steam!);
|
||||||
|
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({
|
.setData({
|
||||||
player, steam, steamStats,
|
player: removeProperties(player, ['_id', '__v']), steam, steamStats,
|
||||||
})
|
})
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
|
|
||||||
res.render("profiles/index.ejs", {
|
|
||||||
player, steam, steamStats: steamStats,
|
|
||||||
msToTime,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
@ -8,6 +8,7 @@ import { SteamUtil } from "../../util/SteamUtil.js";
|
|||||||
import { GitUtil } from "../../util/git.js";
|
import { GitUtil } from "../../util/git.js";
|
||||||
import { removeProperties } from "../../util/functions.js";
|
import { removeProperties } from "../../util/functions.js";
|
||||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
|
import { MProfile } from "../../mongo/profile.js";
|
||||||
|
|
||||||
const generateSearch = (regex: RegExp) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
{
|
{
|
||||||
@ -36,7 +37,7 @@ export class StationsRoute
|
|||||||
app.get("/", async (req, res) =>
|
app.get("/", async (req, res) =>
|
||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i"));
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i"));
|
||||||
|
const profiles = await MProfile.find({ verified: true });
|
||||||
const filter: PipelineStage[] = [];
|
const filter: PipelineStage[] = [];
|
||||||
|
|
||||||
|
|
||||||
@ -48,13 +49,20 @@ export class StationsRoute
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const records = await MLog.aggregate(filter)
|
const records = await MLog.aggregate(filter)
|
||||||
.sort({ leftDate: -1 })
|
.sort({ leftDate: -1 })
|
||||||
.limit(30);
|
.limit(30);
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<ILog, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder<{ records: Omit<ILog, "_id" | "__v">[] }>()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({ records: records.map(x => removeProperties<Omit<ILog, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
.setData({ records: records.map(x => {
|
||||||
|
return {
|
||||||
|
...removeProperties<Omit<ILog, "_id" | "__v">>(x, [ "_id", "__v" ]),
|
||||||
|
verified: profiles.find(xx => xx.steam === x.userSteamId)
|
||||||
|
}
|
||||||
|
}) })
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ import { SteamUtil } from "../../util/SteamUtil.js";
|
|||||||
import { GitUtil } from "../../util/git.js";
|
import { GitUtil } from "../../util/git.js";
|
||||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { removeProperties } from "../../util/functions.js";
|
import { removeProperties } from "../../util/functions.js";
|
||||||
|
import { MProfile } from "../../mongo/profile.js";
|
||||||
|
|
||||||
const generateSearch = (regex: RegExp) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
{
|
{
|
||||||
@ -33,6 +34,7 @@ export class TrainsRoute
|
|||||||
app.get("/", async (req, res) =>
|
app.get("/", async (req, res) =>
|
||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i"));
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i"));
|
||||||
|
const profiles = await MProfile.find({ verified: true });
|
||||||
|
|
||||||
const filter: PipelineStage[] = [];
|
const filter: PipelineStage[] = [];
|
||||||
|
|
||||||
@ -53,7 +55,15 @@ export class TrainsRoute
|
|||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<ITrainLog, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder<{ records: Omit<ITrainLog, "_id" | "__v">[] }>()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({ records: records.map(x => removeProperties<Omit<ITrainLog, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
.setData({
|
||||||
|
records: records.map(x =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
...removeProperties<Omit<ITrainLog, "_id" | "__v">>(x, [ "_id", "__v" ]),
|
||||||
|
verified: profiles.find(xx => xx.steam === x.userSteamId)
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
})
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -45,6 +45,11 @@ export const raw_schema = {
|
|||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
verified: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = new Schema<IProfile>(raw_schema);
|
const schema = new Schema<IProfile>(raw_schema);
|
||||||
@ -76,4 +81,5 @@ export interface IProfile
|
|||||||
trainPoints: number;
|
trainPoints: number;
|
||||||
steamName: string;
|
steamName: string;
|
||||||
trainDistance: number;
|
trainDistance: number;
|
||||||
|
verified: boolean;
|
||||||
}
|
}
|
@ -1,32 +1,35 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
import { Route, Routes, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { Loader } from './components/mini/loaders/PageLoader.tsx';
|
import { Loader } from "./components/mini/loaders/PageLoader.tsx";
|
||||||
import { Home } from './pages/Home';
|
import { Home } from "./pages/Home";
|
||||||
import DefaultLayout from './layout/DefaultLayout';
|
import DefaultLayout from "./layout/DefaultLayout";
|
||||||
import './i18n';
|
import "./i18n";
|
||||||
import { TrainLeaderboard } from './pages/leaderboard/TrainLeaderboard.tsx';
|
import { TrainLeaderboard } from "./pages/leaderboard/TrainLeaderboard.tsx";
|
||||||
import { StationLeaderboard } from './pages/leaderboard/StationsLeaderboard.tsx';
|
import { StationLeaderboard } from "./pages/leaderboard/StationsLeaderboard.tsx";
|
||||||
import { TrainLogs } from './pages/logs/TrainLogs.tsx';
|
import { TrainLogs } from "./pages/logs/TrainLogs.tsx";
|
||||||
import { StationLogs } from './pages/logs/StationLogs.tsx';
|
import { StationLogs } from "./pages/logs/StationLogs.tsx";
|
||||||
import { Profile } from './pages/profile/Profile.tsx';
|
import { Profile } from "./pages/profile/Profile.tsx";
|
||||||
import { Log } from './pages/log/Log.tsx';
|
import { Log } from "./pages/log/Log.tsx";
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from "react-toastify";
|
||||||
import useColorMode from './hooks/useColorMode.tsx';
|
import useColorMode from "./hooks/useColorMode.tsx";
|
||||||
import { HelmetProvider } from 'react-helmet-async';
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
import { PageMeta } from './components/mini/util/PageMeta.tsx';
|
import { PageMeta } from "./components/mini/util/PageMeta.tsx";
|
||||||
|
|
||||||
function App() {
|
function App()
|
||||||
|
{
|
||||||
const [ loading, setLoading ] = useState<boolean>(true);
|
const [ loading, setLoading ] = useState<boolean>(true);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const [ theme ] = useColorMode();
|
const [ theme ] = useColorMode();
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}, [ pathname ]);
|
}, [ pathname ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
setTimeout(() => setLoading(false), 400);
|
setTimeout(() => setLoading(false), 400);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -45,7 +48,7 @@ function App() {
|
|||||||
closeOnClick
|
closeOnClick
|
||||||
rtl={ false }
|
rtl={ false }
|
||||||
pauseOnHover
|
pauseOnHover
|
||||||
theme={theme as 'light' | 'dark'}
|
theme={ theme as "light" | "dark" }
|
||||||
/>
|
/>
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ErrorAlertIcon } from '../icons/AlertIcons.tsx';
|
import { ErrorAlertIcon } from "../icons/AlertIcons.tsx";
|
||||||
|
|
||||||
export const ErrorAlert = ({ title, description }: { title: string, description: string }) => <div
|
export const ErrorAlert = ({ title, description }: { title: string, description: string }) => <div
|
||||||
className="flex w-full border-l-6 border-[#F87171] bg-[#F87171] bg-opacity-[15%] dark:bg-[#1B1B24] px-7 py-8 shadow-md dark:bg-opacity-30 md:p-9">
|
className="flex w-full border-l-6 border-[#F87171] bg-[#F87171] bg-opacity-[15%] dark:bg-[#1B1B24] px-7 py-8 shadow-md dark:bg-opacity-30 md:p-9">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SuccessAlertIcon } from '../icons/AlertIcons.tsx';
|
import { SuccessAlertIcon } from "../icons/AlertIcons.tsx";
|
||||||
|
|
||||||
export const SuccessAlert = ({ title, description }: { title: string, description: string }) =>
|
export const SuccessAlert = ({ title, description }: { title: string, description: string }) =>
|
||||||
<div
|
<div
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { WarningAlertIcon } from '../icons/AlertIcons.tsx';
|
import { WarningAlertIcon } from "../icons/AlertIcons.tsx";
|
||||||
|
|
||||||
export const WarningAlert = ({ title, description }: { title: string, description: string }) =>
|
export const WarningAlert = ({ title, description }: { title: string, description: string }) =>
|
||||||
<div
|
<div
|
||||||
|
@ -1,28 +1,31 @@
|
|||||||
import useColorMode from '../../../hooks/useColorMode';
|
import useColorMode from "../../../hooks/useColorMode";
|
||||||
import { DarkIcon, LightIcon } from '../icons/DarkModeSwitchIcons.tsx';
|
import { DarkIcon, LightIcon } from "../icons/DarkModeSwitchIcons.tsx";
|
||||||
|
|
||||||
const DarkModeSwitcher = () => {
|
const DarkModeSwitcher = () =>
|
||||||
|
{
|
||||||
const [ colorMode, setColorMode ] = useColorMode();
|
const [ colorMode, setColorMode ] = useColorMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<label
|
<label
|
||||||
className={ `relative m-0 block h-7.5 w-14 rounded-full ${
|
className={ `relative m-0 block h-7.5 w-14 rounded-full ${
|
||||||
colorMode === 'dark' ? 'bg-primary' : 'bg-stroke'
|
colorMode === "dark" ? "bg-primary" : "bg-stroke"
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={() => {
|
onChange={ () =>
|
||||||
if (typeof setColorMode === 'function') {
|
{
|
||||||
setColorMode(colorMode === 'light' ? 'dark' : 'light');
|
if (typeof setColorMode === "function")
|
||||||
|
{
|
||||||
|
setColorMode(colorMode === "light" ? "dark" : "light");
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
className="dur absolute top-0 z-50 m-0 h-full w-full cursor-pointer opacity-0"
|
className="dur absolute top-0 z-50 m-0 h-full w-full cursor-pointer opacity-0"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={ `absolute top-1/2 left-[3px] flex h-6 w-6 -translate-y-1/2 translate-x-0 items-center justify-center rounded-full bg-white shadow-switcher duration-75 ease-linear ${
|
className={ `absolute top-1/2 left-[3px] flex h-6 w-6 -translate-y-1/2 translate-x-0 items-center justify-center rounded-full bg-white shadow-switcher duration-75 ease-linear ${
|
||||||
colorMode === 'dark' && '!right-[3px] !translate-x-full'
|
colorMode === "dark" && "!right-[3px] !translate-x-full"
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<span className="dark:hidden">
|
<span className="dark:hidden">
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import DarkModeSwitcher from './DarkModeSwitcher.tsx';
|
import DarkModeSwitcher from "./DarkModeSwitcher.tsx";
|
||||||
import ReactCountryFlag from 'react-country-flag';
|
import ReactCountryFlag from "react-country-flag";
|
||||||
import i18n from 'i18next';
|
import i18n from "i18next";
|
||||||
|
|
||||||
export const Header = (props: {
|
export const Header = (props: {
|
||||||
sidebarOpen: string | boolean | undefined;
|
sidebarOpen: string | boolean | undefined;
|
||||||
setSidebarOpen: (arg0: boolean) => void;
|
setSidebarOpen: (arg0: boolean) => void;
|
||||||
}) => {
|
}) =>
|
||||||
|
{
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-999 flex w-full bg-white drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none">
|
<header className="sticky top-0 z-999 flex w-full bg-white drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none">
|
||||||
<div className="flex flex-grow items-center justify-between px-4 py-4 shadow-2 md:px-6 2xl:px-11">
|
<div className="flex flex-grow items-center justify-between px-4 py-4 shadow-2 md:px-6 2xl:px-11">
|
||||||
<div className="flex items-center gap-2 sm:gap-4 lg:hidden">
|
<div className="flex items-center gap-2 sm:gap-4 lg:hidden">
|
||||||
<button
|
<button
|
||||||
aria-controls="sidebar"
|
aria-controls="sidebar"
|
||||||
onClick={(e) => {
|
onClick={ (e) =>
|
||||||
|
{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
props.setSidebarOpen(!props.sidebarOpen);
|
props.setSidebarOpen(!props.sidebarOpen);
|
||||||
} }
|
} }
|
||||||
@ -22,29 +24,29 @@ export const Header = (props: {
|
|||||||
<span className="du-block absolute right-0 h-full w-full">
|
<span className="du-block absolute right-0 h-full w-full">
|
||||||
<span
|
<span
|
||||||
className={ `relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-[0] duration-200 ease-in-out dark:bg-white ${
|
className={ `relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-[0] duration-200 ease-in-out dark:bg-white ${
|
||||||
!props.sidebarOpen && '!w-full delay-300'
|
!props.sidebarOpen && "!w-full delay-300"
|
||||||
}` }
|
}` }
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
className={ `relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-150 duration-200 ease-in-out dark:bg-white ${
|
className={ `relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-150 duration-200 ease-in-out dark:bg-white ${
|
||||||
!props.sidebarOpen && 'delay-400 !w-full'
|
!props.sidebarOpen && "delay-400 !w-full"
|
||||||
}` }
|
}` }
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
className={ `relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-200 duration-200 ease-in-out dark:bg-white ${
|
className={ `relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-200 duration-200 ease-in-out dark:bg-white ${
|
||||||
!props.sidebarOpen && '!w-full delay-500'
|
!props.sidebarOpen && "!w-full delay-500"
|
||||||
}` }
|
}` }
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute right-0 h-full w-full rotate-45">
|
<span className="absolute right-0 h-full w-full rotate-45">
|
||||||
<span
|
<span
|
||||||
className={ `absolute left-2.5 top-0 block h-full w-0.5 rounded-sm bg-black delay-300 duration-200 ease-in-out dark:bg-white ${
|
className={ `absolute left-2.5 top-0 block h-full w-0.5 rounded-sm bg-black delay-300 duration-200 ease-in-out dark:bg-white ${
|
||||||
!props.sidebarOpen && '!h-0 !delay-[0]'
|
!props.sidebarOpen && "!h-0 !delay-[0]"
|
||||||
}` }
|
}` }
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
className={ `delay-400 absolute left-0 top-2.5 block h-0.5 w-full rounded-sm bg-black duration-200 ease-in-out dark:bg-white ${
|
className={ `delay-400 absolute left-0 top-2.5 block h-0.5 w-full rounded-sm bg-black duration-200 ease-in-out dark:bg-white ${
|
||||||
!props.sidebarOpen && '!h-0 !delay-200'
|
!props.sidebarOpen && "!h-0 !delay-200"
|
||||||
}` }
|
}` }
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
@ -57,11 +59,11 @@ export const Header = (props: {
|
|||||||
|
|
||||||
<div className="flex items-center gap-3 2xsm:gap-7">
|
<div className="flex items-center gap-3 2xsm:gap-7">
|
||||||
<ul className="flex items-center gap-2 2xsm:gap-4">
|
<ul className="flex items-center gap-2 2xsm:gap-4">
|
||||||
<a className="cursor-pointer" onClick={() => i18n.changeLanguage('pl')}>
|
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("pl") }>
|
||||||
<ReactCountryFlag countryCode={'PL'} svg />
|
<ReactCountryFlag countryCode={ "PL" } svg/>
|
||||||
</a>
|
</a>
|
||||||
<a className="cursor-pointer" onClick={() => i18n.changeLanguage('en')}>
|
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("en") }>
|
||||||
<ReactCountryFlag countryCode={'US'} svg />
|
<ReactCountryFlag countryCode={ "US" } svg/>
|
||||||
</a>
|
</a>
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="flex items-center gap-2 2xsm:gap-4">
|
<ul className="flex items-center gap-2 2xsm:gap-4">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export const ArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
export const ArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
||||||
<svg
|
<svg
|
||||||
className={ `absolute right-4 top-1/2 -translate-y-1/2 fill-current ${
|
className={ `absolute right-4 top-1/2 -translate-y-1/2 fill-current ${
|
||||||
rotated && 'rotate-180'
|
rotated && "rotate-180"
|
||||||
}` }
|
}` }
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { ErrorAlertIcon } from '../icons/AlertIcons.tsx';
|
import { ErrorAlertIcon } from "../icons/AlertIcons.tsx";
|
||||||
|
|
||||||
|
|
||||||
export const LoadError = () => {
|
export const LoadError = () =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
@ -14,11 +15,11 @@ export const LoadError = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<h5 className="mb-3 font-semibold text-[#B45454]">
|
<h5 className="mb-3 font-semibold text-[#B45454]">
|
||||||
{t('content_loader.error.header')}
|
{ t("content_loader.error.header") }
|
||||||
</h5>
|
</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="leading-relaxed text-[#CD5D5D]">
|
<li className="leading-relaxed text-[#CD5D5D]">
|
||||||
{t('content_loader.error.description')}
|
{ t("content_loader.error.description") }
|
||||||
</li>
|
</li>
|
||||||
<li className="leading-relaxed text-[#CD5D5D]">
|
<li className="leading-relaxed text-[#CD5D5D]">
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
@ -26,12 +27,12 @@ export const LoadError = () => {
|
|||||||
<div className="mb-7.5 flex flex-wrap gap-4">
|
<div className="mb-7.5 flex flex-wrap gap-4">
|
||||||
<Link
|
<Link
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-70 lg:px-6 xl:px-8"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-70 lg:px-6 xl:px-8"
|
||||||
to="https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/issues/new">{t('content_loader.error.report')}</Link>
|
to="https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/issues/new">{ t("content_loader.error.report") }</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-70 lg:px-6 xl:px-8"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-70 lg:px-6 xl:px-8"
|
||||||
to="#"
|
to="#"
|
||||||
onClick={() => window.location.reload()}>{t('content_loader.error.refresh')}</Link>
|
onClick={ () => window.location.reload() }>{ t("content_loader.error.refresh") }</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -42,10 +43,13 @@ export const LoadError = () => {
|
|||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ContentLoader = () => {
|
export const ContentLoader = () =>
|
||||||
|
{
|
||||||
const [ error, setError ] = useState(false);
|
const [ error, setError ] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
new Promise(res => setTimeout(res, 5000)).then(() => {
|
{
|
||||||
|
new Promise(res => setTimeout(res, 5000)).then(() =>
|
||||||
|
{
|
||||||
setError(true);
|
setError(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export const Loader = () => {
|
export const Loader = () =>
|
||||||
|
{
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center bg-black">
|
<div className="flex h-screen items-center justify-center bg-black">
|
||||||
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"/>
|
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"/>
|
||||||
|
@ -1,68 +1,81 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from "react-router-dom";
|
||||||
import SidebarLinkGroup from './SidebarLinkGroup.tsx';
|
import SidebarLinkGroup from "./SidebarLinkGroup.tsx";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { HamburgerGoBackIcon } from '../icons/SidebarIcons.tsx';
|
import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx";
|
||||||
import { ArrowIcon } from '../icons/ArrowIcon.tsx';
|
import { ArrowIcon } from "../icons/ArrowIcon.tsx";
|
||||||
import { FaHome, FaClipboardList } from 'react-icons/fa';
|
import { FaHome, FaClipboardList } from "react-icons/fa";
|
||||||
import { FaChartSimple, FaTrain, FaBuildingFlag } from 'react-icons/fa6';
|
import { FaChartSimple, FaTrain, FaBuildingFlag } from "react-icons/fa6";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps
|
||||||
|
{
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
setSidebarOpen: (arg: boolean) => void;
|
setSidebarOpen: (arg: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||||
|
{
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
|
|
||||||
const trigger = useRef<any>(null);
|
const trigger = useRef<any>(null);
|
||||||
const sidebar = useRef<any>(null);
|
const sidebar = useRef<any>(null);
|
||||||
|
|
||||||
const storedSidebarExpanded = localStorage.getItem('sidebar-expanded');
|
const storedSidebarExpanded = localStorage.getItem("sidebar-expanded");
|
||||||
const [ sidebarExpanded, setSidebarExpanded ] = useState(
|
const [ sidebarExpanded, setSidebarExpanded ] = useState(
|
||||||
storedSidebarExpanded === null ? false : storedSidebarExpanded === 'true'
|
storedSidebarExpanded === null ? false : storedSidebarExpanded === "true",
|
||||||
);
|
);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// close on click outside
|
// close on click outside
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
const clickHandler = ({ target }: MouseEvent) => {
|
{
|
||||||
if (!sidebar.current || !trigger.current) {
|
const clickHandler = ({ target }: MouseEvent) =>
|
||||||
|
{
|
||||||
|
if (!sidebar.current || !trigger.current)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!sidebarOpen ||
|
!sidebarOpen ||
|
||||||
sidebar.current.contains(target) ||
|
sidebar.current.contains(target) ||
|
||||||
trigger.current.contains(target)
|
trigger.current.contains(target)
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
};
|
};
|
||||||
document.addEventListener('click', clickHandler);
|
document.addEventListener("click", clickHandler);
|
||||||
return () => document.removeEventListener('click', clickHandler);
|
return () => document.removeEventListener("click", clickHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
// close if the esc key is pressed
|
// close if the esc key is pressed
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
const keyHandler = ({ keyCode }: KeyboardEvent) => {
|
{
|
||||||
if (!sidebarOpen || keyCode !== 27) {
|
const keyHandler = ({ keyCode }: KeyboardEvent) =>
|
||||||
|
{
|
||||||
|
if (!sidebarOpen || keyCode !== 27)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', keyHandler);
|
document.addEventListener("keydown", keyHandler);
|
||||||
return () => document.removeEventListener('keydown', keyHandler);
|
return () => document.removeEventListener("keydown", keyHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
localStorage.setItem('sidebar-expanded', sidebarExpanded.toString());
|
{
|
||||||
if (sidebarExpanded) {
|
localStorage.setItem("sidebar-expanded", sidebarExpanded.toString());
|
||||||
document.querySelector('body')?.classList.add('sidebar-expanded');
|
if (sidebarExpanded)
|
||||||
} else {
|
{
|
||||||
document.querySelector('body')?.classList.remove('sidebar-expanded');
|
document.querySelector("body")?.classList.add("sidebar-expanded");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
document.querySelector("body")?.classList.remove("sidebar-expanded");
|
||||||
}
|
}
|
||||||
}, [ sidebarExpanded ]);
|
}, [ sidebarExpanded ]);
|
||||||
|
|
||||||
@ -70,7 +83,7 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
<aside
|
<aside
|
||||||
ref={ sidebar }
|
ref={ sidebar }
|
||||||
className={ `absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
|
className={ `absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
|
||||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
{/* <!-- SIDEBAR HEADER --> */ }
|
{/* <!-- SIDEBAR HEADER --> */ }
|
||||||
@ -95,12 +108,12 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
<NavLink
|
<NavLink
|
||||||
to="/"
|
to="/"
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||||
pathname === '/' &&
|
pathname === "/" &&
|
||||||
'bg-graydark dark:bg-meta-4'
|
"bg-graydark dark:bg-meta-4"
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<FaHome/>
|
<FaHome/>
|
||||||
{t('sidebar.home')}
|
{ t("sidebar.home") }
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@ -108,25 +121,27 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
</ul>
|
</ul>
|
||||||
<ul className="mb-6 flex flex-col gap-1.5">
|
<ul className="mb-6 flex flex-col gap-1.5">
|
||||||
<h3 className="mb-4 ml-4 text-sm font-semibold text-bodydark2">
|
<h3 className="mb-4 ml-4 text-sm font-semibold text-bodydark2">
|
||||||
{t('sidebar.info')}
|
{ t("sidebar.info") }
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<SidebarLinkGroup
|
<SidebarLinkGroup
|
||||||
activeCondition={
|
activeCondition={
|
||||||
pathname === '/logs' || pathname.includes('logs')
|
pathname === "/logs" || pathname.includes("logs")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(handleClick, open) => {
|
{ (handleClick, open) =>
|
||||||
|
{
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="#"
|
to="#"
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||||
(pathname === '/logs' ||
|
(pathname === "/logs" ||
|
||||||
pathname.includes('logs')) &&
|
pathname.includes("logs")) &&
|
||||||
'bg-graydark dark:bg-meta-4'
|
"bg-graydark dark:bg-meta-4"
|
||||||
}` }
|
}` }
|
||||||
onClick={(e) => {
|
onClick={ (e) =>
|
||||||
|
{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sidebarExpanded
|
sidebarExpanded
|
||||||
? handleClick()
|
? handleClick()
|
||||||
@ -134,13 +149,13 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<FaClipboardList/>
|
<FaClipboardList/>
|
||||||
{t('sidebar.logs')}
|
{ t("sidebar.logs") }
|
||||||
<ArrowIcon rotated={ open }/>
|
<ArrowIcon rotated={ open }/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={ `translate transform overflow-hidden ${
|
className={ `translate transform overflow-hidden ${
|
||||||
!open && 'hidden'
|
!open && "hidden"
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||||
@ -148,24 +163,24 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
<NavLink
|
<NavLink
|
||||||
to="/logs/stations"
|
to="/logs/stations"
|
||||||
className={ ({ isActive }) =>
|
className={ ({ isActive }) =>
|
||||||
'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
(isActive && '!text-white')
|
(isActive && "!text-white")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FaBuildingFlag/>
|
<FaBuildingFlag/>
|
||||||
{t('sidebar.stations')}
|
{ t("sidebar.stations") }
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/logs/trains"
|
to="/logs/trains"
|
||||||
className={ ({ isActive }) =>
|
className={ ({ isActive }) =>
|
||||||
'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
(isActive && '!text-white')
|
(isActive && "!text-white")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FaTrain/>
|
<FaTrain/>
|
||||||
{t('sidebar.trains')}
|
{ t("sidebar.trains") }
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -177,20 +192,22 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
|
|
||||||
<SidebarLinkGroup
|
<SidebarLinkGroup
|
||||||
activeCondition={
|
activeCondition={
|
||||||
pathname === '/leaderboard' || pathname.includes('leaderboard')
|
pathname === "/leaderboard" || pathname.includes("leaderboard")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(handleClick, open) => {
|
{ (handleClick, open) =>
|
||||||
|
{
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="#"
|
to="#"
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||||
(pathname === '/leaderboard' ||
|
(pathname === "/leaderboard" ||
|
||||||
pathname.includes('leaderboard')) &&
|
pathname.includes("leaderboard")) &&
|
||||||
'bg-graydark dark:bg-meta-4'
|
"bg-graydark dark:bg-meta-4"
|
||||||
}` }
|
}` }
|
||||||
onClick={(e) => {
|
onClick={ (e) =>
|
||||||
|
{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sidebarExpanded
|
sidebarExpanded
|
||||||
? handleClick()
|
? handleClick()
|
||||||
@ -198,12 +215,12 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<FaChartSimple/>
|
<FaChartSimple/>
|
||||||
{t('sidebar.leaderboard')}
|
{ t("sidebar.leaderboard") }
|
||||||
<ArrowIcon rotated={ open }/>
|
<ArrowIcon rotated={ open }/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div
|
<div
|
||||||
className={ `translate transform overflow-hidden ${
|
className={ `translate transform overflow-hidden ${
|
||||||
!open && 'hidden'
|
!open && "hidden"
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||||
@ -211,24 +228,24 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
|||||||
<NavLink
|
<NavLink
|
||||||
to="/leaderboard/stations"
|
to="/leaderboard/stations"
|
||||||
className={ ({ isActive }) =>
|
className={ ({ isActive }) =>
|
||||||
'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
(isActive && '!text-white')
|
(isActive && "!text-white")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FaBuildingFlag/>
|
<FaBuildingFlag/>
|
||||||
{t('sidebar.stations')}
|
{ t("sidebar.stations") }
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/leaderboard/trains"
|
to="/leaderboard/trains"
|
||||||
className={ ({ isActive }) =>
|
className={ ({ isActive }) =>
|
||||||
'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
(isActive && '!text-white')
|
(isActive && "!text-white")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FaTrain/>
|
<FaTrain/>
|
||||||
{t('sidebar.trains')}
|
{ t("sidebar.trains") }
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { ReactNode, useState } from 'react';
|
import { ReactNode, useState } from "react";
|
||||||
|
|
||||||
interface SidebarLinkGroupProps {
|
interface SidebarLinkGroupProps
|
||||||
|
{
|
||||||
children: (handleClick: () => void, open: boolean) => ReactNode;
|
children: (handleClick: () => void, open: boolean) => ReactNode;
|
||||||
activeCondition: boolean;
|
activeCondition: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) => {
|
const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) =>
|
||||||
|
{
|
||||||
const [ open, setOpen ] = useState<boolean>(activeCondition);
|
const [ open, setOpen ] = useState<boolean>(activeCondition);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () =>
|
||||||
|
{
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
interface CardDataStatsProps {
|
interface CardDataStatsProps
|
||||||
|
{
|
||||||
title: string;
|
title: string;
|
||||||
total: string;
|
total: string;
|
||||||
rate?: string;
|
rate?: string;
|
||||||
@ -13,8 +14,9 @@ export const CardDataStats: React.FC<CardDataStatsProps> = ({
|
|||||||
total,
|
total,
|
||||||
rate,
|
rate,
|
||||||
levelUp,
|
levelUp,
|
||||||
levelDown
|
levelDown,
|
||||||
}) => {
|
}) =>
|
||||||
|
{
|
||||||
return (
|
return (
|
||||||
<div className="rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark">
|
<div className="rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
|
|
||||||
@ -28,8 +30,8 @@ export const CardDataStats: React.FC<CardDataStatsProps> = ({
|
|||||||
|
|
||||||
{ rate && <span
|
{ rate && <span
|
||||||
className={ `flex items-center gap-1 text-sm font-medium ${
|
className={ `flex items-center gap-1 text-sm font-medium ${
|
||||||
levelUp && 'text-meta-3'
|
levelUp && "text-meta-3"
|
||||||
} ${levelDown && 'text-meta-5'} `}
|
} ${ levelDown && "text-meta-5" } ` }
|
||||||
>
|
>
|
||||||
{ rate }
|
{ rate }
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from "react-helmet-async";
|
||||||
|
|
||||||
// https://dev.to/facubotta/meta-data-in-react-1p93
|
// https://dev.to/facubotta/meta-data-in-react-1p93
|
||||||
export const PageMeta = ({ title = '', description = '', image = '', name = '' }) => {
|
export const PageMeta = ({ title = "", description = "", image = "", name = "" }) =>
|
||||||
|
{
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
{ /* Standard metadata tags */ }
|
{ /* Standard metadata tags */ }
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { ChangeEventHandler } from 'react';
|
import { ChangeEventHandler } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const Search = ({ searchItem, handleInputChange }: {
|
export const Search = ({ searchItem, handleInputChange }: {
|
||||||
searchItem: string;
|
searchItem: string;
|
||||||
handleInputChange: ChangeEventHandler
|
handleInputChange: ChangeEventHandler
|
||||||
}) => {
|
}) =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return <div
|
return <div
|
||||||
className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
||||||
@ -14,7 +15,7 @@ export const Search = ({ searchItem, handleInputChange }: {
|
|||||||
type="text"
|
type="text"
|
||||||
onChange={ handleInputChange }
|
onChange={ handleInputChange }
|
||||||
value={ searchItem }
|
value={ searchItem }
|
||||||
placeholder={t('logs.search')}
|
placeholder={ t("logs.search") }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { TLeaderboardRecord } from '../../../types/leaderboard.ts';
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
|
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
||||||
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
|
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
||||||
|
import { formatTime } from "../../../util/time.ts";
|
||||||
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
|
|
||||||
export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord[], error: number }) => {
|
export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord[], error: number }) =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
|
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
description={t('content_loader.notfound.description')} />}
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
{ error === 0 && <ContentLoader/> }
|
{ error === 0 && <ContentLoader/> }
|
||||||
{ error === 1 && <div
|
{ error === 1 && <div
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
@ -19,17 +22,17 @@ export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord
|
|||||||
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-3">
|
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-3">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.user')}
|
{ t("leaderboard.user") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.time')}
|
{ t("leaderboard.time") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.actions')}
|
{ t("leaderboard.actions") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,28 +40,28 @@ export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord
|
|||||||
{ stations.map((station, key) => (
|
{ stations.map((station, key) => (
|
||||||
<div
|
<div
|
||||||
className={ `grid grid-cols-2 sm:grid-cols-3 ${ stations.length === (key + 1) // todo: ...
|
className={ `grid grid-cols-2 sm:grid-cols-3 ${ stations.length === (key + 1) // todo: ...
|
||||||
? ''
|
? ""
|
||||||
: 'border-b border-stroke dark:border-strokedark'
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
}` }
|
}` }
|
||||||
key={ station.id }
|
key={ station.id }
|
||||||
>
|
>
|
||||||
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={'/profile/' + station.steam}
|
<Link to={ "/profile/" + station.steam }
|
||||||
className="color-orchid">{station.steamName}</Link>
|
className="color-orchid">{ station.steamName }</Link> { station.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-3">{Math.floor(station.dispatcherTime / 3600000)}h</p>
|
<p className="text-meta-3">{ formatTime(station.dispatcherTime) }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
<Link
|
<Link
|
||||||
to={'/profile/' + station.steam}
|
to={ "/profile/" + station.steam }
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{t('leaderboard.profile')}
|
{ t("leaderboard.profile") }
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { TLeaderboardRecord } from '../../../types/leaderboard.ts';
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
|
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
||||||
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
|
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
||||||
|
import { formatTime } from "../../../util/time.ts";
|
||||||
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
|
|
||||||
export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], error: number }) => {
|
export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], error: number }) =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
|
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
description={t('content_loader.notfound.description')} />}
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
{ error === 0 && <ContentLoader/> }
|
{ error === 0 && <ContentLoader/> }
|
||||||
{ error === 1 && <div
|
{ error === 1 && <div
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
@ -19,27 +22,27 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
|
|||||||
<div className="grid grid-cols-4 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-5">
|
<div className="grid grid-cols-4 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-5">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.user')}
|
{ t("leaderboard.user") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.points')}
|
{ t("leaderboard.points") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.distance')}
|
{ t("leaderboard.distance") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center sm:block xl:p-5">
|
<div className="p-2.5 text-center sm:block xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.time')}
|
{ t("leaderboard.time") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('leaderboard.actions')}
|
{ t("leaderboard.actions") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -47,15 +50,15 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
|
|||||||
{ trains.map((train, key) => (
|
{ trains.map((train, key) => (
|
||||||
<div
|
<div
|
||||||
className={ `grid grid-cols-4 sm:grid-cols-5 ${ trains.length === (key + 1)
|
className={ `grid grid-cols-4 sm:grid-cols-5 ${ trains.length === (key + 1)
|
||||||
? ''
|
? ""
|
||||||
: 'border-b border-stroke dark:border-strokedark'
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
}` }
|
}` }
|
||||||
key={ train.id }
|
key={ train.id }
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={'/profile/' + train.steam}
|
<Link to={ "/profile/" + train.steam }
|
||||||
className="color-orchid">{train.steamName}</Link>
|
className="color-orchid">{ train.steamName }</Link> { train.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -68,15 +71,15 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-3">{Math.floor(train.trainTime / 3600000)}h</p>
|
<p className="text-meta-3">{ formatTime(train.trainTime) }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
<Link
|
<Link
|
||||||
to={'/profile/' + train.steam}
|
to={ "/profile/" + train.steam }
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{t('leaderboard.profile')}
|
{ t("leaderboard.profile") }
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +1,26 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { TLogStationData } from '../../../types/log.ts';
|
import { TLogStationData } from "../../../types/log.ts";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from "dayjs";
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from "react-toastify";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
|
|
||||||
export const StationLog = ({ data }: { data: TLogStationData }) => {
|
export const StationLog = ({ data }: { data: TLogStationData }) =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const copyLink = () => {
|
const copyLink = () =>
|
||||||
|
{
|
||||||
void navigator.clipboard.writeText(location.href);
|
void navigator.clipboard.writeText(location.href);
|
||||||
toast.success(t('log.toasts.copied'));
|
toast.success(t("log.toasts.copied"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const report = () => {
|
const report = () =>
|
||||||
toast.info(t('log.toasts.report'), {
|
{
|
||||||
autoClose: 5000
|
toast.info(t("log.toasts.report"), {
|
||||||
|
autoClose: 5000,
|
||||||
});
|
});
|
||||||
void navigator.clipboard.writeText(`;user: \`${data.userUsername}\`\n;steam: \`${data.userSteamId}\`\n;left: <t:${data.leftDate}>${data.joinedDate ? `\n;joined: <t:${data.joinedDate}>` : ''}\n;station: \`${data.stationName}\`\n;link: ${location.href}\n\n`);
|
void navigator.clipboard.writeText(`;user: \`${ data.userUsername }\`\n;steam: \`${ data.userSteamId }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;station: \`${ data.stationName }\`\n;link: ${ location.href }\n\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
@ -29,7 +34,7 @@ export const StationLog = ({ data }: { data: TLogStationData }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{data.userUsername}
|
{ data.userUsername }{ data.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,22 +42,22 @@ export const StationLog = ({ data }: { data: TLogStationData }) => {
|
|||||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||||
<div className="flex flex-col sm:flex-row sm:flex-wrap sm:justify-end">
|
<div className="flex flex-col sm:flex-row sm:flex-wrap sm:justify-end">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h1 className="text-xl text-black dark:text-white pb-5">{t('log.station.header')}</h1>
|
<h1 className="text-xl text-black dark:text-white pb-5">{ t("log.station.header") }</h1>
|
||||||
<p>{t('log.station.server', { server: data.server.toUpperCase() })}</p>
|
<p>{ t("log.station.server", { server: data.server.toUpperCase() }) }</p>
|
||||||
<p>{t('log.station.station', { name: data.stationName, short: data.stationShort })}</p>
|
<p>{ t("log.station.station", { name: data.stationName, short: data.stationShort }) }</p>
|
||||||
|
|
||||||
{ data.joinedDate &&
|
{ data.joinedDate &&
|
||||||
<p>{t('log.station.joined', { date: dayjs(data.joinedDate).format('DD/MM/YYYY HH:mm') })}</p>}
|
<p>{ t("log.station.joined", { date: dayjs(data.joinedDate).format("DD/MM/YYYY HH:mm") }) }</p> }
|
||||||
<p>{t('log.station.left', { date: dayjs(data.leftDate).format('DD/MM/YYYY HH:mm') })}</p>
|
<p>{ t("log.station.left", { date: dayjs(data.leftDate).format("DD/MM/YYYY HH:mm") }) }</p>
|
||||||
{ data.joinedDate &&
|
{ data.joinedDate &&
|
||||||
<p>{t('log.station.spent', { date: dayjs.duration(data.leftDate - data.joinedDate).format('H[h] m[m]') })}</p>}
|
<p>{ t("log.station.spent", { date: dayjs.duration(data.leftDate - data.joinedDate).format("H[h] m[m]") }) }</p> }
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-5 mt-5 sm:mt-0 sm:ml-auto">
|
<div className="flex flex-col gap-5 mt-5 sm:mt-0 sm:ml-auto">
|
||||||
<a
|
<a
|
||||||
onClick={ report }
|
onClick={ report }
|
||||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-meta-7 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-meta-7 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
>
|
>
|
||||||
{t('log.buttons.report')}
|
{ t("log.buttons.report") }
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@ -60,8 +65,15 @@ export const StationLog = ({ data }: { data: TLogStationData }) => {
|
|||||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
>
|
>
|
||||||
|
|
||||||
{t('log.buttons.copy')}
|
{ t("log.buttons.copy") }
|
||||||
</a>
|
</a>
|
||||||
|
<Link
|
||||||
|
to={"/profile/" + data.userSteamId}
|
||||||
|
className="inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
|
>
|
||||||
|
|
||||||
|
{ t("log.buttons.profile") }
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +1,27 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { TLogTrainData } from '../../../types/log.ts';
|
import { TLogTrainData } from "../../../types/log.ts";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from "dayjs";
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from "react-toastify";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
|
|
||||||
export const TrainLog = ({ data }: { data: TLogTrainData }) => {
|
|
||||||
|
export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const copyLink = () => {
|
const copyLink = () =>
|
||||||
|
{
|
||||||
void navigator.clipboard.writeText(location.href);
|
void navigator.clipboard.writeText(location.href);
|
||||||
toast.success(t('log.toasts.copied'));
|
toast.success(t("log.toasts.copied"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const report = () => {
|
const report = () =>
|
||||||
toast.info(t('log.toasts.report'), {
|
{
|
||||||
autoClose: 5000
|
toast.info(t("log.toasts.report"), {
|
||||||
|
autoClose: 5000,
|
||||||
});
|
});
|
||||||
void navigator.clipboard.writeText(`;user: \`${data.userUsername}\`\n;steam: \`${data.userSteamId}\`\n;left: <t:${data.leftDate}>${data.joinedDate ? `\n;joined: <t:${data.joinedDate}>` : ''}\n;train: \`${data.trainNumber}\`\n;link: ${location.href}\n\n`);
|
void navigator.clipboard.writeText(`;user: \`${ data.userUsername }\`\n;steam: \`${ data.userSteamId }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;train: \`${ data.trainNumber }\`\n;link: ${ location.href }\n\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
@ -29,7 +35,7 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{data.userUsername}
|
{ data.userUsername } { data.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,26 +43,26 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) => {
|
|||||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||||
<div className="flex flex-col sm:flex-row sm:flex-wrap sm:justify-end">
|
<div className="flex flex-col sm:flex-row sm:flex-wrap sm:justify-end">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h1 className="text-xl text-black dark:text-white pb-5">{t('log.train.header')}</h1>
|
<h1 className="text-xl text-black dark:text-white pb-5">{ t("log.train.header") }</h1>
|
||||||
<p>{t('log.train.server', { server: data.server.toUpperCase() })}</p>
|
<p>{ t("log.train.server", { server: data.server.toUpperCase() }) }</p>
|
||||||
<p>{t('log.train.train', { name: data.trainName, number: data.trainNumber })}</p>
|
<p>{ t("log.train.train", { name: data.trainName, number: data.trainNumber }) }</p>
|
||||||
{ (data.distance || data.distance === 0) &&
|
{ (data.distance || data.distance === 0) &&
|
||||||
<p>{t('log.train.distance', { distance: (data.distance / 1000).toFixed(2) })}</p>}
|
<p>{ t("log.train.distance", { distance: (data.distance / 1000).toFixed(2) }) }</p> }
|
||||||
|
|
||||||
{(data.points || data.points === 0) && <p>{t('log.train.points', { points: data.points })}</p>}
|
{ (data.points || data.points === 0) && <p>{ t("log.train.points", { points: data.points }) }</p> }
|
||||||
|
|
||||||
{ data.joinedDate &&
|
{ data.joinedDate &&
|
||||||
<p>{t('log.train.joined', { date: dayjs(data.joinedDate).format('DD/MM/YYYY HH:mm') })}</p>}
|
<p>{ t("log.train.joined", { date: dayjs(data.joinedDate).format("DD/MM/YYYY HH:mm") }) }</p> }
|
||||||
<p>{t('log.train.left', { date: dayjs(data.leftDate).format('DD/MM/YYYY HH:mm') })}</p>
|
<p>{ t("log.train.left", { date: dayjs(data.leftDate).format("DD/MM/YYYY HH:mm") }) }</p>
|
||||||
{ data.joinedDate &&
|
{ data.joinedDate &&
|
||||||
<p>{t('log.train.spent', { date: dayjs.duration(data.leftDate - data.joinedDate).format('H[h] m[m]') })}</p>}
|
<p>{ t("log.train.spent", { date: dayjs.duration(data.leftDate - data.joinedDate).format("H[h] m[m]") }) }</p> }
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-5 mt-5 sm:mt-0 sm:ml-auto">
|
<div className="flex flex-col gap-5 mt-5 sm:mt-0 sm:ml-auto">
|
||||||
<a
|
<a
|
||||||
onClick={ report }
|
onClick={ report }
|
||||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-meta-7 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-meta-7 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
>
|
>
|
||||||
{t('log.buttons.report')}
|
{ t("log.buttons.report") }
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@ -64,8 +70,15 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) => {
|
|||||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
>
|
>
|
||||||
|
|
||||||
{t('log.buttons.copy')}
|
{ t("log.buttons.copy") }
|
||||||
</a>
|
</a>
|
||||||
|
<Link
|
||||||
|
to={ "/profile/" + data.userSteamId }
|
||||||
|
className="inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
|
>
|
||||||
|
|
||||||
|
{ t("log.buttons.profile") }
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from "dayjs";
|
||||||
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
|
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
||||||
import { TStationRecord } from '../../../types/station.ts';
|
import { TStationRecord } from "../../../types/station.ts";
|
||||||
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
|
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
||||||
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
|
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const StationTable = ({ stations, error }: {
|
export const StationTable = ({ stations, error }: {
|
||||||
stations: TStationRecord[], error: number
|
stations: TStationRecord[], error: number
|
||||||
}) => {
|
}) =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
|
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
description={t('content_loader.notfound.description')} />}
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
{ error === 0 && <ContentLoader/> }
|
{ error === 0 && <ContentLoader/> }
|
||||||
|
|
||||||
{ error === 1 && <div
|
{ error === 1 && <div
|
||||||
@ -24,22 +26,22 @@ export const StationTable = ({ stations, error }: {
|
|||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.user')}
|
{ t("logs.user") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.station')}
|
{ t("logs.station") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.time')}
|
{ t("logs.time") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.actions')}
|
{ t("logs.actions") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -47,39 +49,39 @@ export const StationTable = ({ stations, error }: {
|
|||||||
{ stations.map((station, key) => (
|
{ stations.map((station, key) => (
|
||||||
<div
|
<div
|
||||||
className={ `grid grid-cols-3 sm:grid-cols-4 ${ stations.length === (key + 1)
|
className={ `grid grid-cols-3 sm:grid-cols-4 ${ stations.length === (key + 1)
|
||||||
? ''
|
? ""
|
||||||
: 'border-b border-stroke dark:border-strokedark'
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
}` }
|
}` }
|
||||||
key={ station.id }
|
key={ station.id }
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={'/profile/' + station.userSteamId}
|
<Link to={ "/profile/" + station.userSteamId }
|
||||||
className="color-orchid">{station.userUsername}</Link>
|
className="color-orchid">{ station.userUsername }</Link> { station.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-6 sm:block break-all">{station.server.toUpperCase()} - {station.stationName ?? '--'}</p>
|
<p className="text-meta-6 sm:block break-all">{ station.server.toUpperCase() } - { station.stationName ?? "--" }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-3">{dayjs(station.leftDate).format('HH:mm DD/MM/YYYY')}</p>
|
<p className="text-meta-3">{ dayjs(station.leftDate).format("HH:mm DD/MM/YYYY") }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
||||||
<Link
|
<Link
|
||||||
to={'/profile/' + station.userSteamId}
|
to={ "/profile/" + station.userSteamId }
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{t('logs.profile')}
|
{ t("logs.profile") }
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={'/log/' + station.id}
|
to={ "/log/" + station.id }
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{t('logs.record')}
|
{ t("logs.record") }
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { TTrainRecord } from '../../../types/train.ts';
|
import { TTrainRecord } from "../../../types/train.ts";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from "dayjs";
|
||||||
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
|
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
||||||
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
|
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
||||||
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const TrainTable = ({ trains, error }: {
|
export const TrainTable = ({ trains, error }: {
|
||||||
trains: TTrainRecord[], error: number
|
trains: TTrainRecord[], error: number
|
||||||
}) => {
|
}) =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
|
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
description={t('content_loader.notfound.description')} />}
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
{ error === 0 && <ContentLoader/> }
|
{ error === 0 && <ContentLoader/> }
|
||||||
{ error === 1 && <div
|
{ error === 1 && <div
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
@ -23,32 +24,32 @@ export const TrainTable = ({ trains, error }: {
|
|||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-6">
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-6">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.user')}
|
{ t("logs.user") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.train')}
|
{ t("logs.train") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.points')}
|
{ t("logs.points") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.distance')}
|
{ t("logs.distance") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.time')}
|
{ t("logs.time") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('logs.actions')}
|
{ t("logs.actions") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,47 +57,47 @@ export const TrainTable = ({ trains, error }: {
|
|||||||
{ trains.map((train, key) => (
|
{ trains.map((train, key) => (
|
||||||
<div
|
<div
|
||||||
className={ `grid grid-cols-3 sm:grid-cols-6 ${ trains.length === (key + 1)
|
className={ `grid grid-cols-3 sm:grid-cols-6 ${ trains.length === (key + 1)
|
||||||
? ''
|
? ""
|
||||||
: 'border-b border-stroke dark:border-strokedark'
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
}` }
|
}` }
|
||||||
key={ train.id }
|
key={ train.id }
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={'/profile/' + train.userSteamId}
|
<Link to={ "/profile/" + train.userSteamId }
|
||||||
className="color-orchid">{train.userUsername}</Link>
|
className="color-orchid">{ train.userUsername }</Link> { train.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-6 sm:block break-all">{train.server.toUpperCase()} - {train.trainNumber ?? '--'}</p>
|
<p className="text-meta-6 sm:block break-all">{ train.server.toUpperCase() } - { train.trainNumber ?? "--" }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-6">{train.distance ? train.points : '--'}</p>
|
<p className="text-meta-6">{ train.distance ? train.points : "--" }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-5">{train.distance ? `${(train.distance / 1000).toFixed(2)}km` : '--'}</p>
|
<p className="text-meta-5">{ train.distance ? `${ (train.distance / 1000).toFixed(2) }km` : "--" }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-3">{dayjs(train.leftDate).format('HH:mm DD/MM/YYYY')}</p>
|
<p className="text-meta-3">{ dayjs(train.leftDate).format("HH:mm DD/MM/YYYY") }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
||||||
<Link
|
<Link
|
||||||
to={'/profile/' + train.userSteamId}
|
to={ "/profile/" + train.userSteamId }
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{t('logs.profile')}
|
{ t("logs.profile") }
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={'/log/' + train.id}
|
to={ "/log/" + train.id }
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{t('logs.record')}
|
{ t("logs.record") }
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { TProfileData } from '../../../types/profile.ts';
|
import { TProfileData } from "../../../types/profile.ts";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { ArrowIcon } from '../../mini/icons/ArrowIcon.tsx';
|
import { ArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||||
|
import { formatTime } from "../../../util/time.ts";
|
||||||
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
|
|
||||||
export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||||
|
{
|
||||||
|
|
||||||
const [ showTrains, setShowTrains ] = useState(false);
|
const [ showTrains, setShowTrains ] = useState(false);
|
||||||
const [ showStations, setShowStations ] = useState(false);
|
const [ showStations, setShowStations ] = useState(false);
|
||||||
@ -20,7 +23,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{data.steam.personname}
|
{ data.steam.personname } { data.player.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -30,14 +33,14 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
<span className="font-semibold text-black dark:text-white">
|
<span className="font-semibold text-black dark:text-white">
|
||||||
{ Math.floor(data.player.trainDistance / 1000) }km
|
{ Math.floor(data.player.trainDistance / 1000) }km
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-wrap">{t('profile.stats.distance')}</span>
|
<span className="text-sm text-wrap">{ t("profile.stats.distance") }</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
||||||
<span className="font-semibold text-black dark:text-white">
|
<span className="font-semibold text-black dark:text-white">
|
||||||
{Math.floor(data.player.dispatcherTime / 3600000)}h
|
{ formatTime(data.player.dispatcherTime) }
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-wrap">{t('profile.stats.time')}</span>
|
<span className="text-sm text-wrap">{ t("profile.stats.time") }</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -45,7 +48,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
{ Object.keys(data.player.trainStats || {}).length > 0 &&
|
{ Object.keys(data.player.trainStats || {}).length > 0 &&
|
||||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||||
<div className="group relative cursor-pointer" onClick={ () => setShowTrains(val => !val) }>
|
<div className="group relative cursor-pointer" onClick={ () => setShowTrains(val => !val) }>
|
||||||
<h1 className="text-xl text-black dark:text-white pb-5">{t('profile.trains.header')}</h1>
|
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.trains.header") }</h1>
|
||||||
<ArrowIcon rotated={ showTrains }/>
|
<ArrowIcon rotated={ showTrains }/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -54,27 +57,28 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('profile.trains.train')}
|
{ t("profile.trains.train") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('profile.trains.distance')}
|
{ t("profile.trains.distance") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('profile.trains.points')}
|
{ t("profile.trains.points") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('profile.trains.time')}
|
{ t("profile.trains.time") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Object.keys(data.player.trainStats).map(trainName => {
|
{ Object.keys(data.player.trainStats).map(trainName =>
|
||||||
|
{
|
||||||
const train = data.player.trainStats[ trainName ];
|
const train = data.player.trainStats[ trainName ];
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
@ -96,7 +100,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-3">{Math.floor(train.time / 3600000)}h</p>
|
<p className="text-meta-3">{ formatTime(train.time) }</p>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}) }
|
}) }
|
||||||
@ -108,7 +112,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
{ Object.keys(data.player.dispatcherStats || {}).length > 0 &&
|
{ Object.keys(data.player.dispatcherStats || {}).length > 0 &&
|
||||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||||
<div className="group relative cursor-pointer" onClick={ () => setShowStations(val => !val) }>
|
<div className="group relative cursor-pointer" onClick={ () => setShowStations(val => !val) }>
|
||||||
<h1 className="text-xl text-black dark:text-white pb-5">{t('profile.stations.header')}</h1>
|
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.stations.header") }</h1>
|
||||||
<ArrowIcon rotated={ showTrains }/>
|
<ArrowIcon rotated={ showTrains }/>
|
||||||
</div>
|
</div>
|
||||||
{ showStations &&
|
{ showStations &&
|
||||||
@ -116,17 +120,18 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
|
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('profile.stations.station')}
|
{ t("profile.stations.station") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{t('profile.stations.time')}
|
{ t("profile.stations.time") }
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{Object.keys(data.player.dispatcherStats).map(stationName => {
|
{ Object.keys(data.player.dispatcherStats).map(stationName =>
|
||||||
|
{
|
||||||
const station = data.player.dispatcherStats[ stationName ];
|
const station = data.player.dispatcherStats[ stationName ];
|
||||||
return <div
|
return <div
|
||||||
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
|
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
|
||||||
@ -139,7 +144,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
<p className="text-meta-3">{Math.floor(station.time / 3600000)}h</p>
|
<p className="text-meta-3">{formatTime(station.time) }</p>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}) }
|
}) }
|
||||||
|
@ -59,8 +59,8 @@
|
|||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"stats": {
|
"stats": {
|
||||||
"distance": "Kilometers traveled",
|
"distance": "Driver experience",
|
||||||
"time": "Dispatcher hours"
|
"time": "Dispatcher experience"
|
||||||
},
|
},
|
||||||
"trains": {
|
"trains": {
|
||||||
"header": "Train Statistics",
|
"header": "Train Statistics",
|
||||||
@ -87,12 +87,14 @@
|
|||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
"notfound": {
|
||||||
"title": "Record not found",
|
"title": "Record not found",
|
||||||
"description": "This record could not be found."
|
"description": "This record could not be found."
|
||||||
},
|
},
|
||||||
"blacklist": {
|
"blacklist": {
|
||||||
"title": "The record cannot be displayed",
|
"title": "The record cannot be displayed",
|
||||||
"description": "The record has been blocked."
|
"description": "The record has been blocked."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"station": {
|
"station": {
|
||||||
"header": "Leaving the station",
|
"header": "Leaving the station",
|
||||||
@ -118,7 +120,8 @@
|
|||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"report": "Report",
|
"report": "Report",
|
||||||
"copy": "Copy link"
|
"copy": "Copy link",
|
||||||
|
"profile": "Profile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
@ -59,8 +59,8 @@
|
|||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"stats": {
|
"stats": {
|
||||||
"distance": "Przejechanych kilometrów",
|
"distance": "Staż maszynisty",
|
||||||
"time": "Godzin dyżurów"
|
"time": "Staż dyżurnego ruchu"
|
||||||
},
|
},
|
||||||
"trains": {
|
"trains": {
|
||||||
"header": "Statystyki pociągów",
|
"header": "Statystyki pociągów",
|
||||||
@ -87,12 +87,14 @@
|
|||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
"notfound": {
|
||||||
"title": "Nie znaleziono rekordu",
|
"title": "Nie znaleziono rekordu",
|
||||||
"description": "Ten rekord nie został znaleziony."
|
"description": "Ten rekord nie został znaleziony."
|
||||||
},
|
},
|
||||||
"blacklist": {
|
"blacklist": {
|
||||||
"title": "Nie można wyświetlić rekordu",
|
"title": "Nie można wyświetlić rekordu",
|
||||||
"description": "Rekord został zablokowany."
|
"description": "Rekord został zablokowany."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"station": {
|
"station": {
|
||||||
"header": "Wyjście z posterunku",
|
"header": "Wyjście z posterunku",
|
||||||
@ -118,7 +120,8 @@
|
|||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"report": "Zgłoś",
|
"report": "Zgłoś",
|
||||||
"copy": "Kopiuj link"
|
"copy": "Kopiuj link",
|
||||||
|
"profile": "Profil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useState, ReactNode } from 'react';
|
import React, { useState, ReactNode } from "react";
|
||||||
import { Header } from '../components/mini/header/Header';
|
import { Header } from "../components/mini/header/Header";
|
||||||
import { Sidebar } from '../components/mini/sidebar/Sidebar';
|
import { Sidebar } from "../components/mini/sidebar/Sidebar";
|
||||||
|
|
||||||
export const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) => {
|
export const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
|
{
|
||||||
const [ sidebarOpen, setSidebarOpen ] = useState(false);
|
const [ sidebarOpen, setSidebarOpen ] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { TStatsResponse } from '../types/stats.ts';
|
import { TStatsResponse } from "../types/stats.ts";
|
||||||
import { WarningAlert } from '../components/mini/alerts/Warning.tsx';
|
import { WarningAlert } from "../components/mini/alerts/Warning.tsx";
|
||||||
import { CardDataStats } from '../components/mini/util/CardDataStats.tsx';
|
import { CardDataStats } from "../components/mini/util/CardDataStats.tsx";
|
||||||
|
|
||||||
export const Home: React.FC = () => {
|
export const Home: React.FC = () =>
|
||||||
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [commit, setCommit] = useState('');
|
const [ commit, setCommit ] = useState("");
|
||||||
const [version, setVersion] = useState('');
|
const [ version, setVersion ] = useState("");
|
||||||
const [ trains, setTrains ] = useState(0);
|
const [ trains, setTrains ] = useState(0);
|
||||||
const [ dispatchers, setDispatchers ] = useState(0);
|
const [ dispatchers, setDispatchers ] = useState(0);
|
||||||
const [ profiles, setProfiles ] = useState(0);
|
const [ profiles, setProfiles ] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
fetch(`${import.meta.env.VITE_API_URL}/stats/`).then(x => x.json()).then((data: TStatsResponse) => {
|
{
|
||||||
|
fetch(`${ import.meta.env.VITE_API_URL }/stats/`).then(x => x.json()).then((data: TStatsResponse) =>
|
||||||
|
{
|
||||||
data.data.git.commit && setCommit(data.data.git.commit);
|
data.data.git.commit && setCommit(data.data.git.commit);
|
||||||
data.data.git.version && setVersion(data.data.git.version);
|
data.data.git.version && setVersion(data.data.git.version);
|
||||||
|
|
||||||
@ -29,14 +32,14 @@ export const Home: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex pb-5">
|
<div className="flex pb-5">
|
||||||
<WarningAlert description={t('preview.description')} title={t('preview.title')} />
|
<WarningAlert description={ t("preview.description") } title={ t("preview.title") }/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6 xl:grid-cols-3 2xl:gap-7.5">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6 xl:grid-cols-3 2xl:gap-7.5">
|
||||||
<CardDataStats title={t('home.stats.trains')} total={trains.toString()} />
|
<CardDataStats title={ t("home.stats.trains") } total={ trains.toString() }/>
|
||||||
<CardDataStats title={t('home.stats.dispatchers')} total={dispatchers.toString()} />
|
<CardDataStats title={ t("home.stats.dispatchers") } total={ dispatchers.toString() }/>
|
||||||
<CardDataStats title={t('home.stats.profiles')} total={profiles.toString()} />
|
<CardDataStats title={ t("home.stats.profiles") } total={ profiles.toString() }/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -45,22 +48,22 @@ export const Home: React.FC = () => {
|
|||||||
<div className="px-4 pb-6 text-center">
|
<div className="px-4 pb-6 text-center">
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{t('home.title')}
|
{ t("home.title") }
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-medium">{t('home.description')}</p>
|
<p className="font-medium">{ t("home.description") }</p>
|
||||||
|
|
||||||
<div className="p-4 md:p-6 xl:p-9 flex gap-2 justify-center">
|
<div className="p-4 md:p-6 xl:p-9 flex gap-2 justify-center">
|
||||||
<Link
|
<Link
|
||||||
to="https://git.alekswilc.dev/simrail/simrail.alekswilc.dev"
|
to="https://git.alekswilc.dev/simrail/simrail.alekswilc.dev"
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
>
|
>
|
||||||
{t('home.buttons.project')}
|
{ t("home.buttons.project") }
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="https://forum.simrail.eu/topic/9142-logowanie-wyj%C5%9B%C4%87-z-posterunk%C3%B3w/"
|
to="https://forum.simrail.eu/topic/9142-logowanie-wyj%C5%9B%C4%87-z-posterunk%C3%B3w/"
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
>
|
>
|
||||||
{t('home.buttons.forum')}
|
{ t("home.buttons.forum") }
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -73,28 +76,29 @@ export const Home: React.FC = () => {
|
|||||||
|
|
||||||
<div className="mt-6.5">
|
<div className="mt-6.5">
|
||||||
<p><Trans
|
<p><Trans
|
||||||
i18nKey={t('home.footer.author')}
|
i18nKey={ t("home.footer.author") }
|
||||||
values={{ author: 'alekswilc' }}
|
values={ { author: "alekswilc" } }
|
||||||
components={ {
|
components={ {
|
||||||
anchor: <Link className="color-orchid" to={'https://www.alekswilc.dev'} />
|
anchor: <Link className="color-orchid" to={ "https://www.alekswilc.dev" }/>,
|
||||||
} }
|
} }
|
||||||
/></p>
|
/></p>
|
||||||
<p><Trans
|
<p><Trans
|
||||||
i18nKey={t('home.footer.thanks')}
|
i18nKey={ t("home.footer.thanks") }
|
||||||
components={ {
|
components={ {
|
||||||
bahu: <Link className="color-orchid" to={'https://bahu.pro/'} />,
|
bahu: <Link className="color-orchid" to={ "https://bahu.pro/" }/>,
|
||||||
simrailelite: <Link className="color-orchid"
|
simrailelite: <Link className="color-orchid"
|
||||||
to={'https://discord.gg/yDhy3pDrVr'} />
|
to={ "https://discord.gg/yDhy3pDrVr" }/>,
|
||||||
} }
|
} }
|
||||||
/></p>
|
/></p>
|
||||||
<p>{t('home.footer.license')} <Link className="color-orchid"
|
<p>{ t("home.footer.license") } <Link className="color-orchid"
|
||||||
to={'https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/src/branch/main/LICENSE'}>GNU
|
to={ "https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/src/branch/main/LICENSE" }>GNU
|
||||||
AGPL V3</Link></p>
|
AGPL V3</Link></p>
|
||||||
<p>{t('home.footer.powered')} <Link className="color-orchid"
|
<p>{ t("home.footer.powered") } <Link className="color-orchid"
|
||||||
to={'https://tailadmin.com/'}>TailAdmin</Link></p>
|
to={ "https://tailadmin.com/" }>TailAdmin</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>{ version && <Link className="color-orchid"
|
<p>{ version && <Link className="color-orchid"
|
||||||
to={`https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/releases/tag/${version}`}>{version}</Link>}{version && commit && ' | '}{commit &&
|
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/releases/tag/${ version }` }>{ version }</Link> }{ version && commit && " | " }{ commit &&
|
||||||
<Link className="color-orchid"
|
<Link className="color-orchid"
|
||||||
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/commit/${ commit }` }>{ commit }</Link> }</p>
|
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/commit/${ commit }` }>{ commit }</Link> }</p>
|
||||||
|
|
||||||
|
@ -3,24 +3,30 @@ import { ChangeEvent, useEffect, useState } from "react";
|
|||||||
import { StationTable } from "../../components/pages/leaderboard/StationTable.tsx";
|
import { StationTable } from "../../components/pages/leaderboard/StationTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export const StationLeaderboard = () =>
|
export const StationLeaderboard = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
|
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }leaderboard/station/`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }leaderboard/station/`).then(x => x.json()).then(x =>
|
||||||
{
|
{
|
||||||
|
|
||||||
setData(x.data.records);
|
setData(x.data.records);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [ searchItem, setSearchItem ] = useState("");
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
|
||||||
setData([]);
|
setData([]);
|
||||||
setError(0);
|
setError(0);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/station/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/station/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
||||||
@ -30,6 +36,11 @@ export const StationLeaderboard = () =>
|
|||||||
});
|
});
|
||||||
}, [ searchValue ]);
|
}, [ searchValue ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setSearchItem(searchParams.get("q") ?? "");
|
||||||
|
}, [ searchParams ]);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
|
@ -3,10 +3,14 @@ import { ChangeEvent, useEffect, useState } from "react";
|
|||||||
import { TrainTable } from "../../components/pages/leaderboard/TrainTable.tsx";
|
import { TrainTable } from "../../components/pages/leaderboard/TrainTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export const TrainLeaderboard = () =>
|
export const TrainLeaderboard = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
|
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/`).then(x => x.json()).then(x =>
|
||||||
@ -15,12 +19,14 @@ export const TrainLeaderboard = () =>
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [ searchItem, setSearchItem ] = useState("");
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
|
||||||
setData([]);
|
setData([]);
|
||||||
setError(0);
|
setError(0);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
||||||
@ -30,6 +36,11 @@ export const TrainLeaderboard = () =>
|
|||||||
});
|
});
|
||||||
}, [ searchValue ]);
|
}, [ searchValue ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setSearchItem(searchParams.get("q") ?? "");
|
||||||
|
}, [ searchParams ]);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from "react-router-dom";
|
||||||
import { ContentLoader } from '../../components/mini/loaders/ContentLoader.tsx';
|
import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
import { WarningAlert } from '../../components/mini/alerts/Warning.tsx';
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { TLogResponse, TLogStationData, TLogTrainData } from '../../types/log.ts';
|
import { TLogResponse, TLogStationData, TLogTrainData } from "../../types/log.ts";
|
||||||
import { StationLog } from '../../components/pages/log/StationLog.tsx';
|
import { StationLog } from "../../components/pages/log/StationLog.tsx";
|
||||||
import { TrainLog } from '../../components/pages/log/TrainLog.tsx';
|
import { TrainLog } from "../../components/pages/log/TrainLog.tsx";
|
||||||
import { PageMeta } from '../../components/mini/util/PageMeta.tsx';
|
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
||||||
|
|
||||||
export const Log = () => {
|
export const Log = () =>
|
||||||
|
{
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
|
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
|
||||||
const [ trainData, setTrainData ] = useState<TLogTrainData>(undefined!);
|
const [ trainData, setTrainData ] = useState<TLogTrainData>(undefined!);
|
||||||
const [ stationData, setStationData ] = useState<TLogStationData>(undefined!);
|
const [ stationData, setStationData ] = useState<TLogStationData>(undefined!);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
fetch(`${import.meta.env.VITE_API_URL}/log/${id}`).then(x => x.json()).then((data: TLogResponse) => {
|
{
|
||||||
switch (data.code) {
|
fetch(`${ import.meta.env.VITE_API_URL }/log/${ id }`).then(x => x.json()).then((data: TLogResponse) =>
|
||||||
|
{
|
||||||
|
switch (data.code)
|
||||||
|
{
|
||||||
case 404:
|
case 404:
|
||||||
setError(2);
|
setError(2);
|
||||||
break;
|
break;
|
||||||
@ -28,7 +32,7 @@ export const Log = () => {
|
|||||||
case 200:
|
case 200:
|
||||||
setError(1);
|
setError(1);
|
||||||
|
|
||||||
'trainNumber' in data.data ? setTrainData(data.data) : setStationData(data.data);
|
"trainNumber" in data.data ? setTrainData(data.data) : setStationData(data.data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -43,13 +47,13 @@ export const Log = () => {
|
|||||||
{/* NOT FOUND */ }
|
{/* NOT FOUND */ }
|
||||||
{ error === 2 && <PageMeta title="simrail.alekswilc.dev | Record not found"
|
{ error === 2 && <PageMeta title="simrail.alekswilc.dev | Record not found"
|
||||||
description="This record could not be found."/> }
|
description="This record could not be found."/> }
|
||||||
{error === 2 && <WarningAlert title={t('log.errors.notfound.title')}
|
{ error === 2 && <WarningAlert title={ t("log.errors.notfound.title") }
|
||||||
description={t('log.errors.notfound.description')} />}
|
description={ t("log.errors.notfound.description") }/> }
|
||||||
{/* BLACKLISTED LOG */ }
|
{/* BLACKLISTED LOG */ }
|
||||||
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted record"
|
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted record"
|
||||||
description="The record has been blocked."/> }
|
description="The record has been blocked."/> }
|
||||||
{error === 3 && <WarningAlert title={t('log.errors.blacklist.title')}
|
{ error === 3 && <WarningAlert title={ t("log.errors.blacklist.title") }
|
||||||
description={t('log.errors.blacklist.description')} />}
|
description={ t("log.errors.blacklist.description") }/> }
|
||||||
{/* SUCCESS */ }
|
{/* SUCCESS */ }
|
||||||
{ error === 1 && stationData && <PageMeta
|
{ error === 1 && stationData && <PageMeta
|
||||||
title={ `simrail.alekswilc.dev | ${ stationData.userUsername }` }
|
title={ `simrail.alekswilc.dev | ${ stationData.userUsername }` }
|
||||||
|
@ -3,10 +3,13 @@ import { StationTable } from "../../components/pages/logs/StationTable.tsx";
|
|||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { TStationRecord } from "../../types/station.ts";
|
import { TStationRecord } from "../../types/station.ts";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export const StationLogs = () =>
|
export const StationLogs = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TStationRecord[]>([]);
|
const [ data, setData ] = useState<TStationRecord[]>([]);
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/stations/`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }/stations/`).then(x => x.json()).then(x =>
|
||||||
@ -15,11 +18,13 @@ export const StationLogs = () =>
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
||||||
const [ searchItem, setSearchItem ] = useState("");
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
|
||||||
setData([]);
|
setData([]);
|
||||||
setError(0);
|
setError(0);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/stations/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }/stations/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
||||||
@ -29,6 +34,11 @@ export const StationLogs = () =>
|
|||||||
});
|
});
|
||||||
}, [ searchValue ]);
|
}, [ searchValue ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setSearchItem(searchParams.get("q") ?? "");
|
||||||
|
}, [ searchParams ]);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
|
@ -3,10 +3,14 @@ import { TTrainRecord } from "../../types/train.ts";
|
|||||||
import { TrainTable } from "../../components/pages/logs/TrainTable.tsx";
|
import { TrainTable } from "../../components/pages/logs/TrainTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export const TrainLogs = () =>
|
export const TrainLogs = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TTrainRecord[]>([]);
|
const [ data, setData ] = useState<TTrainRecord[]>([]);
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/trains/`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }/trains/`).then(x => x.json()).then(x =>
|
||||||
@ -14,12 +18,15 @@ export const TrainLogs = () =>
|
|||||||
setData(x.data.records);
|
setData(x.data.records);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
||||||
const [ searchItem, setSearchItem ] = useState("");
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
|
||||||
setData([]);
|
setData([]);
|
||||||
setError(0);
|
setError(0);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/trains/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
fetch(`${ import.meta.env.VITE_API_URL }/trains/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
||||||
@ -29,6 +36,11 @@ export const TrainLogs = () =>
|
|||||||
});
|
});
|
||||||
}, [ searchValue ]);
|
}, [ searchValue ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setSearchItem(searchParams.get("q") ?? "");
|
||||||
|
}, [ searchParams ]);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from "react-router-dom";
|
||||||
import { TProfileData, TProfileResponse } from '../../types/profile.ts';
|
import { TProfileData, TProfileResponse } from "../../types/profile.ts";
|
||||||
import { ContentLoader } from '../../components/mini/loaders/ContentLoader.tsx';
|
import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
import { WarningAlert } from '../../components/mini/alerts/Warning.tsx';
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
import { ProfileCard } from '../../components/pages/profile/Profile.tsx';
|
import { ProfileCard } from "../../components/pages/profile/Profile.tsx";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { PageMeta } from '../../components/mini/util/PageMeta.tsx';
|
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
||||||
|
import { formatTime } from "../../util/time.ts";
|
||||||
|
|
||||||
export const Profile = () => {
|
export const Profile = () =>
|
||||||
|
{
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
|
|
||||||
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
|
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
|
||||||
const [ data, setData ] = useState<TProfileData>(undefined!);
|
const [ data, setData ] = useState<TProfileData>(undefined!);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
fetch(`${import.meta.env.VITE_API_URL}/profiles/${id}`).then(x => x.json()).then((data: TProfileResponse) => {
|
{
|
||||||
switch (data.code) {
|
fetch(`${ import.meta.env.VITE_API_URL }/profiles/${ id }`).then(x => x.json()).then((data: TProfileResponse) =>
|
||||||
|
{
|
||||||
|
switch (data.code)
|
||||||
|
{
|
||||||
case 404:
|
case 404:
|
||||||
setError(2);
|
setError(2);
|
||||||
break;
|
break;
|
||||||
@ -40,18 +43,18 @@ export const Profile = () => {
|
|||||||
{/* NOT FOUND */ }
|
{/* NOT FOUND */ }
|
||||||
{ error === 2 && <PageMeta title="simrail.alekswilc.dev | Profile not found"
|
{ error === 2 && <PageMeta title="simrail.alekswilc.dev | Profile not found"
|
||||||
description="Player's profile could not be found or the player has a private Steam profile."/> }
|
description="Player's profile could not be found or the player has a private Steam profile."/> }
|
||||||
{error === 2 && <WarningAlert title={t('profile.errors.notfound.title')}
|
{ error === 2 && <WarningAlert title={ t("profile.errors.notfound.title") }
|
||||||
description={t('profile.errors.notfound.description')} />}
|
description={ t("profile.errors.notfound.description") }/> }
|
||||||
{/* BLACKLISTED PROFILE */ }
|
{/* BLACKLISTED PROFILE */ }
|
||||||
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted profile"
|
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted profile"
|
||||||
description="This player's profile has been blocked."/> }
|
description="This player's profile has been blocked."/> }
|
||||||
{error === 3 && <WarningAlert title={t('profile.errors.blacklist.title')}
|
{ error === 3 && <WarningAlert title={ t("profile.errors.blacklist.title") }
|
||||||
description={t('profile.errors.blacklist.description')} />}
|
description={ t("profile.errors.blacklist.description") }/> }
|
||||||
{/* SUCCESS */ }
|
{/* SUCCESS */ }
|
||||||
{ error === 1 && <PageMeta image={ data.steam.avatarfull }
|
{ error === 1 && <PageMeta image={ data.steam.avatarfull }
|
||||||
title={ `simrail.alekswilc.dev | ${ data.steam.personname }'s profile` }
|
title={ `simrail.alekswilc.dev | ${ data.steam.personname }'s profile` }
|
||||||
description={`${data.player.trainDistance ? 0 : ((data.player.trainDistance / 1000).toFixed(2))} kilometers travelled |
|
description={ `${ data.player.trainDistance ? 0 : ((data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
||||||
${data.player.dispatcherTime ? 0 : Math.floor(data.player.dispatcherTime / 3600000)}h dispatchers hours`} />}
|
${ data.player.dispatcherTime ? 0 : formatTime(data.player.dispatcherTime) } dispatcher experience` }/> }
|
||||||
{ error === 1 && <ProfileCard data={ data }/> }
|
{ error === 1 && <ProfileCard data={ data }/> }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -21,6 +21,7 @@ export interface TLeaderboardRecord
|
|||||||
dispatcherTime: number;
|
dispatcherTime: number;
|
||||||
dispatcherStats?: { [ key: string ]: TLeaderboardDispatcherStat };
|
dispatcherStats?: { [ key: string ]: TLeaderboardDispatcherStat };
|
||||||
trainStats?: { [ key: string ]: TLeaderboardTrainStat };
|
trainStats?: { [ key: string ]: TLeaderboardTrainStat };
|
||||||
|
verified: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLeaderboardDispatcherStat
|
export interface TLeaderboardDispatcherStat
|
||||||
|
@ -18,7 +18,7 @@ export interface TLogTrainData
|
|||||||
server: string;
|
server: string;
|
||||||
trainName: string;
|
trainName: string;
|
||||||
joinedDate?: number;
|
joinedDate?: number;
|
||||||
|
verified: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLogStationData
|
export interface TLogStationData
|
||||||
@ -32,4 +32,6 @@ export interface TLogStationData
|
|||||||
stationShort: string;
|
stationShort: string;
|
||||||
server: string;
|
server: string;
|
||||||
joinedDate?: number;
|
joinedDate?: number;
|
||||||
|
verified: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ export interface TProfilePlayer
|
|||||||
trainStats: Record<string, TProfileTrainStatsRecord>;
|
trainStats: Record<string, TProfileTrainStatsRecord>;
|
||||||
trainDistance: number;
|
trainDistance: number;
|
||||||
trainPoints: number;
|
trainPoints: number;
|
||||||
|
verified: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TProfileDispatcherStatsRecord
|
export interface TProfileDispatcherStatsRecord
|
||||||
|
@ -21,4 +21,5 @@ export interface TStationRecord
|
|||||||
stationShort: string;
|
stationShort: string;
|
||||||
server: string;
|
server: string;
|
||||||
joinedDate?: number;
|
joinedDate?: number;
|
||||||
|
verified: boolean;
|
||||||
}
|
}
|
@ -23,5 +23,6 @@ export interface TTrainRecord
|
|||||||
points: number;
|
points: number;
|
||||||
server: string;
|
server: string;
|
||||||
trainName: string;
|
trainName: string;
|
||||||
|
verified: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
packages/frontend/src/util/time.ts
Normal file
14
packages/frontend/src/util/time.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export const formatTime = (time: number) =>
|
||||||
|
{
|
||||||
|
if (Math.floor(time / 3600000) > 0)
|
||||||
|
{
|
||||||
|
return `${ Math.floor(time / 3600000) }h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.floor(time / 60000) > 0)
|
||||||
|
{
|
||||||
|
return `${ Math.floor(time / 60000) }m`
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0h';
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user