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:
Aleksander Wilczyński 2024-11-09 19:58:36 +01:00
commit 20c29173f5
Signed by: gitea
GPG Key ID: CECFC30736A3D1C8
45 changed files with 1258 additions and 1072 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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(),
); );
}); });

View File

@ -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(),
); );
}); });

View File

@ -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;
} }

View File

@ -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>

View File

@ -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">

View File

@ -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

View File

@ -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

View File

@ -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">

View File

@ -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">

View File

@ -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"

View File

@ -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);
}); });
}, []); }, []);

View File

@ -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"/>

View File

@ -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>

View File

@ -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);
}; };

View File

@ -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 }

View File

@ -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 */ }

View File

@ -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>;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>;
}) } }) }

View File

@ -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": {

View File

@ -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": {

View File

@ -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 (

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -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 }` }

View File

@ -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);

View File

@ -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);

View File

@ -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 }/> }
</> </>
); );

View File

@ -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

View File

@ -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;
} }

View File

@ -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

View File

@ -21,4 +21,5 @@ export interface TStationRecord
stationShort: string; stationShort: string;
server: string; server: string;
joinedDate?: number; joinedDate?: number;
verified: boolean;
} }

View File

@ -23,5 +23,6 @@ export interface TTrainRecord
points: number; points: number;
server: string; server: string;
trainName: string; trainName: string;
verified: boolean;
} }

View 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';
}