feat(): Add veritication mark #50

Merged
alekswilc merged 2 commits from v3 into preview 2024-11-09 19:58:37 +01:00
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 { removeProperties } from "../../util/functions.js";
import { ILog, MLog } from "../../mongo/logs.js";
import { MProfile } from "../../mongo/profile.js";
export class LogRoute
@ -24,6 +25,7 @@ export class LogRoute
}
const log = await MLog.findOne({ id }) || await MTrainLog.findOne({ id });
if (!log)
{
res.status(404).json(new ErrorResponseBuilder()
@ -31,8 +33,13 @@ export class LogRoute
.setData("Invalid Id parameter").toJSON());
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;

View File

@ -4,6 +4,7 @@ import { MProfile } from "../../mongo/profile.js";
import { MBlacklist } from "../../mongo/blacklist.js";
import { SteamUtil } from "../../util/SteamUtil.js";
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
import { removeProperties } from "../../util/functions.js";
export class ProfilesRoute
{
@ -11,7 +12,6 @@ export class ProfilesRoute
{
const app = Router();
app.get("/:id", async (req, res) =>
{
if (!req.params.id)
@ -19,13 +19,15 @@ export class ProfilesRoute
res.redirect("/");
return;
}
const player = await MProfile.findOne({ steam: req.params.id });
if (!player)
{
res.status(404).json(new ErrorResponseBuilder()
.setCode(404).setData("Profile not found! (propably private)"));
.setCode(404).setData("Profile not found! (probably private)"));
return;
}
const blacklist = await MBlacklist.findOne({ steam: req.params.id! });
if (blacklist && blacklist.status)
{
@ -33,23 +35,18 @@ export class ProfilesRoute
.setCode(403).setData("Profile blacklisted!"));
return;
}
const steam = await SteamUtil.getPlayer(player?.steam!);
const steamStats = await SteamUtil.getPlayerStats(player?.steam!);
res.json(
new SuccessResponseBuilder()
.setCode(200)
.setData({
player, steam, steamStats,
player: removeProperties(player, ['_id', '__v']), steam, steamStats,
})
.toJSON(),
);
res.render("profiles/index.ejs", {
player, steam, steamStats: steamStats,
msToTime,
});
});
return app;

View File

@ -8,6 +8,7 @@ import { SteamUtil } from "../../util/SteamUtil.js";
import { GitUtil } from "../../util/git.js";
import { removeProperties } from "../../util/functions.js";
import { SuccessResponseBuilder } from "../responseBuilder.js";
import { MProfile } from "../../mongo/profile.js";
const generateSearch = (regex: RegExp) => [
{
@ -36,7 +37,7 @@ export class StationsRoute
app.get("/", async (req, res) =>
{
const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i"));
const profiles = await MProfile.find({ verified: true });
const filter: PipelineStage[] = [];
@ -48,13 +49,20 @@ export class StationsRoute
},
});
const records = await MLog.aggregate(filter)
.sort({ leftDate: -1 })
.limit(30);
res.json(
new SuccessResponseBuilder<{ records: Omit<ILog, "_id" | "__v">[] }>()
.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(),
);
});

View File

@ -8,6 +8,7 @@ import { SteamUtil } from "../../util/SteamUtil.js";
import { GitUtil } from "../../util/git.js";
import { SuccessResponseBuilder } from "../responseBuilder.js";
import { removeProperties } from "../../util/functions.js";
import { MProfile } from "../../mongo/profile.js";
const generateSearch = (regex: RegExp) => [
{
@ -33,6 +34,7 @@ export class TrainsRoute
app.get("/", async (req, res) =>
{
const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i"));
const profiles = await MProfile.find({ verified: true });
const filter: PipelineStage[] = [];
@ -53,7 +55,15 @@ export class TrainsRoute
res.json(
new SuccessResponseBuilder<{ records: Omit<ITrainLog, "_id" | "__v">[] }>()
.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(),
);
});

View File

@ -45,6 +45,11 @@ export const raw_schema = {
required: false,
default: 0,
},
verified: {
type: Boolean,
required: true,
default: false
}
};
const schema = new Schema<IProfile>(raw_schema);
@ -76,4 +81,5 @@ export interface IProfile
trainPoints: number;
steamName: string;
trainDistance: number;
verified: boolean;
}

View File

@ -1,32 +1,35 @@
import { useEffect, useState } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { useEffect, useState } from "react";
import { Route, Routes, useLocation } from "react-router-dom";
import { Loader } from './components/mini/loaders/PageLoader.tsx';
import { Home } from './pages/Home';
import DefaultLayout from './layout/DefaultLayout';
import './i18n';
import { TrainLeaderboard } from './pages/leaderboard/TrainLeaderboard.tsx';
import { StationLeaderboard } from './pages/leaderboard/StationsLeaderboard.tsx';
import { TrainLogs } from './pages/logs/TrainLogs.tsx';
import { StationLogs } from './pages/logs/StationLogs.tsx';
import { Profile } from './pages/profile/Profile.tsx';
import { Log } from './pages/log/Log.tsx';
import { Loader } from "./components/mini/loaders/PageLoader.tsx";
import { Home } from "./pages/Home";
import DefaultLayout from "./layout/DefaultLayout";
import "./i18n";
import { TrainLeaderboard } from "./pages/leaderboard/TrainLeaderboard.tsx";
import { StationLeaderboard } from "./pages/leaderboard/StationsLeaderboard.tsx";
import { TrainLogs } from "./pages/logs/TrainLogs.tsx";
import { StationLogs } from "./pages/logs/StationLogs.tsx";
import { Profile } from "./pages/profile/Profile.tsx";
import { Log } from "./pages/log/Log.tsx";
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
import useColorMode from './hooks/useColorMode.tsx';
import { HelmetProvider } from 'react-helmet-async';
import { PageMeta } from './components/mini/util/PageMeta.tsx';
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import useColorMode from "./hooks/useColorMode.tsx";
import { HelmetProvider } from "react-helmet-async";
import { PageMeta } from "./components/mini/util/PageMeta.tsx";
function App() {
function App()
{
const [ loading, setLoading ] = useState<boolean>(true);
const { pathname } = useLocation();
const [ theme ] = useColorMode();
useEffect(() => {
useEffect(() =>
{
window.scrollTo(0, 0);
}, [ pathname ]);
useEffect(() => {
useEffect(() =>
{
setTimeout(() => setLoading(false), 400);
}, []);
@ -45,7 +48,7 @@ function App() {
closeOnClick
rtl={ false }
pauseOnHover
theme={theme as 'light' | 'dark'}
theme={ theme as "light" | "dark" }
/>
<DefaultLayout>
<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
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 }) =>
<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 }) =>
<div

View File

@ -1,28 +1,31 @@
import useColorMode from '../../../hooks/useColorMode';
import { DarkIcon, LightIcon } from '../icons/DarkModeSwitchIcons.tsx';
import useColorMode from "../../../hooks/useColorMode";
import { DarkIcon, LightIcon } from "../icons/DarkModeSwitchIcons.tsx";
const DarkModeSwitcher = () => {
const DarkModeSwitcher = () =>
{
const [ colorMode, setColorMode ] = useColorMode();
return (
<li>
<label
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
type="checkbox"
onChange={() => {
if (typeof setColorMode === 'function') {
setColorMode(colorMode === 'light' ? 'dark' : 'light');
onChange={ () =>
{
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"
/>
<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 ${
colorMode === 'dark' && '!right-[3px] !translate-x-full'
colorMode === "dark" && "!right-[3px] !translate-x-full"
}` }
>
<span className="dark:hidden">

View File

@ -1,18 +1,20 @@
import DarkModeSwitcher from './DarkModeSwitcher.tsx';
import ReactCountryFlag from 'react-country-flag';
import i18n from 'i18next';
import DarkModeSwitcher from "./DarkModeSwitcher.tsx";
import ReactCountryFlag from "react-country-flag";
import i18n from "i18next";
export const Header = (props: {
sidebarOpen: string | boolean | undefined;
setSidebarOpen: (arg0: boolean) => void;
}) => {
}) =>
{
return (
<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 items-center gap-2 sm:gap-4 lg:hidden">
<button
aria-controls="sidebar"
onClick={(e) => {
onClick={ (e) =>
{
e.stopPropagation();
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={ `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
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
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 className="absolute right-0 h-full w-full rotate-45">
<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 ${
!props.sidebarOpen && '!h-0 !delay-[0]'
!props.sidebarOpen && "!h-0 !delay-[0]"
}` }
></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 ${
!props.sidebarOpen && '!h-0 !delay-200'
!props.sidebarOpen && "!h-0 !delay-200"
}` }
></span>
</span>
@ -57,11 +59,11 @@ export const Header = (props: {
<div className="flex items-center gap-3 2xsm:gap-7">
<ul className="flex items-center gap-2 2xsm:gap-4">
<a className="cursor-pointer" onClick={() => i18n.changeLanguage('pl')}>
<ReactCountryFlag countryCode={'PL'} svg />
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("pl") }>
<ReactCountryFlag countryCode={ "PL" } svg/>
</a>
<a className="cursor-pointer" onClick={() => i18n.changeLanguage('en')}>
<ReactCountryFlag countryCode={'US'} svg />
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("en") }>
<ReactCountryFlag countryCode={ "US" } svg/>
</a>
</ul>
<ul className="flex items-center gap-2 2xsm:gap-4">

View File

@ -1,7 +1,7 @@
export const ArrowIcon = ({ rotated }: { rotated?: boolean }) =>
<svg
className={ `absolute right-4 top-1/2 -translate-y-1/2 fill-current ${
rotated && 'rotate-180'
rotated && "rotate-180"
}` }
width="20"
height="20"

View File

@ -1,10 +1,11 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ErrorAlertIcon } from '../icons/AlertIcons.tsx';
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { ErrorAlertIcon } from "../icons/AlertIcons.tsx";
export const LoadError = () => {
export const LoadError = () =>
{
const { t } = useTranslation();
return <div
@ -14,11 +15,11 @@ export const LoadError = () => {
</div>
<div className="w-full">
<h5 className="mb-3 font-semibold text-[#B45454]">
{t('content_loader.error.header')}
{ t("content_loader.error.header") }
</h5>
<ul>
<li className="leading-relaxed text-[#CD5D5D]">
{t('content_loader.error.description')}
{ t("content_loader.error.description") }
</li>
<li className="leading-relaxed text-[#CD5D5D]">
<div className="pt-4">
@ -26,12 +27,12 @@ export const LoadError = () => {
<div className="mb-7.5 flex flex-wrap gap-4">
<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"
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
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="#"
onClick={() => window.location.reload()}>{t('content_loader.error.refresh')}</Link>
onClick={ () => window.location.reload() }>{ t("content_loader.error.refresh") }</Link>
</div>
</div>
@ -42,10 +43,13 @@ export const LoadError = () => {
</div>;
};
export const ContentLoader = () => {
export const ContentLoader = () =>
{
const [ error, setError ] = useState(false);
useEffect(() => {
new Promise(res => setTimeout(res, 5000)).then(() => {
useEffect(() =>
{
new Promise(res => setTimeout(res, 5000)).then(() =>
{
setError(true);
});
}, []);

View File

@ -1,4 +1,5 @@
export const Loader = () => {
export const Loader = () =>
{
return (
<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"/>

View File

@ -1,68 +1,81 @@
import React, { useEffect, useRef, useState } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import SidebarLinkGroup from './SidebarLinkGroup.tsx';
import { useTranslation } from 'react-i18next';
import { HamburgerGoBackIcon } from '../icons/SidebarIcons.tsx';
import { ArrowIcon } from '../icons/ArrowIcon.tsx';
import { FaHome, FaClipboardList } from 'react-icons/fa';
import { FaChartSimple, FaTrain, FaBuildingFlag } from 'react-icons/fa6';
import React, { useEffect, useRef, useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import SidebarLinkGroup from "./SidebarLinkGroup.tsx";
import { useTranslation } from "react-i18next";
import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx";
import { ArrowIcon } from "../icons/ArrowIcon.tsx";
import { FaHome, FaClipboardList } from "react-icons/fa";
import { FaChartSimple, FaTrain, FaBuildingFlag } from "react-icons/fa6";
interface SidebarProps {
interface SidebarProps
{
sidebarOpen: boolean;
setSidebarOpen: (arg: boolean) => void;
}
export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
{
const location = useLocation();
const { pathname } = location;
const trigger = useRef<any>(null);
const sidebar = useRef<any>(null);
const storedSidebarExpanded = localStorage.getItem('sidebar-expanded');
const storedSidebarExpanded = localStorage.getItem("sidebar-expanded");
const [ sidebarExpanded, setSidebarExpanded ] = useState(
storedSidebarExpanded === null ? false : storedSidebarExpanded === 'true'
storedSidebarExpanded === null ? false : storedSidebarExpanded === "true",
);
const { t } = useTranslation();
// close on click outside
useEffect(() => {
const clickHandler = ({ target }: MouseEvent) => {
if (!sidebar.current || !trigger.current) {
useEffect(() =>
{
const clickHandler = ({ target }: MouseEvent) =>
{
if (!sidebar.current || !trigger.current)
{
return;
}
if (
!sidebarOpen ||
sidebar.current.contains(target) ||
trigger.current.contains(target)
) {
)
{
return;
}
setSidebarOpen(false);
};
document.addEventListener('click', clickHandler);
return () => document.removeEventListener('click', clickHandler);
document.addEventListener("click", clickHandler);
return () => document.removeEventListener("click", clickHandler);
});
// close if the esc key is pressed
useEffect(() => {
const keyHandler = ({ keyCode }: KeyboardEvent) => {
if (!sidebarOpen || keyCode !== 27) {
useEffect(() =>
{
const keyHandler = ({ keyCode }: KeyboardEvent) =>
{
if (!sidebarOpen || keyCode !== 27)
{
return;
}
setSidebarOpen(false);
};
document.addEventListener('keydown', keyHandler);
return () => document.removeEventListener('keydown', keyHandler);
document.addEventListener("keydown", keyHandler);
return () => document.removeEventListener("keydown", keyHandler);
});
useEffect(() => {
localStorage.setItem('sidebar-expanded', sidebarExpanded.toString());
if (sidebarExpanded) {
document.querySelector('body')?.classList.add('sidebar-expanded');
} else {
document.querySelector('body')?.classList.remove('sidebar-expanded');
useEffect(() =>
{
localStorage.setItem("sidebar-expanded", sidebarExpanded.toString());
if (sidebarExpanded)
{
document.querySelector("body")?.classList.add("sidebar-expanded");
}
else
{
document.querySelector("body")?.classList.remove("sidebar-expanded");
}
}, [ sidebarExpanded ]);
@ -70,7 +83,7 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
<aside
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 ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
sidebarOpen ? "translate-x-0" : "-translate-x-full"
}` }
>
{/* <!-- SIDEBAR HEADER --> */ }
@ -95,12 +108,12 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
<NavLink
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 ${
pathname === '/' &&
'bg-graydark dark:bg-meta-4'
pathname === "/" &&
"bg-graydark dark:bg-meta-4"
}` }
>
<FaHome/>
{t('sidebar.home')}
{ t("sidebar.home") }
</NavLink>
</li>
@ -108,25 +121,27 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
</ul>
<ul className="mb-6 flex flex-col gap-1.5">
<h3 className="mb-4 ml-4 text-sm font-semibold text-bodydark2">
{t('sidebar.info')}
{ t("sidebar.info") }
</h3>
<SidebarLinkGroup
activeCondition={
pathname === '/logs' || pathname.includes('logs')
pathname === "/logs" || pathname.includes("logs")
}
>
{(handleClick, open) => {
{ (handleClick, open) =>
{
return (
<React.Fragment>
<NavLink
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 ${
(pathname === '/logs' ||
pathname.includes('logs')) &&
'bg-graydark dark:bg-meta-4'
(pathname === "/logs" ||
pathname.includes("logs")) &&
"bg-graydark dark:bg-meta-4"
}` }
onClick={(e) => {
onClick={ (e) =>
{
e.preventDefault();
sidebarExpanded
? handleClick()
@ -134,13 +149,13 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
} }
>
<FaClipboardList/>
{t('sidebar.logs')}
{ t("sidebar.logs") }
<ArrowIcon rotated={ open }/>
</NavLink>
<div
className={ `translate transform overflow-hidden ${
!open && 'hidden'
!open && "hidden"
}` }
>
<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
to="/logs/stations"
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 ' +
(isActive && '!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")
}
>
<FaBuildingFlag/>
{t('sidebar.stations')}
{ t("sidebar.stations") }
</NavLink>
</li>
<li>
<NavLink
to="/logs/trains"
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 ' +
(isActive && '!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")
}
>
<FaTrain/>
{t('sidebar.trains')}
{ t("sidebar.trains") }
</NavLink>
</li>
</ul>
@ -177,20 +192,22 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
<SidebarLinkGroup
activeCondition={
pathname === '/leaderboard' || pathname.includes('leaderboard')
pathname === "/leaderboard" || pathname.includes("leaderboard")
}
>
{(handleClick, open) => {
{ (handleClick, open) =>
{
return (
<React.Fragment>
<NavLink
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 ${
(pathname === '/leaderboard' ||
pathname.includes('leaderboard')) &&
'bg-graydark dark:bg-meta-4'
(pathname === "/leaderboard" ||
pathname.includes("leaderboard")) &&
"bg-graydark dark:bg-meta-4"
}` }
onClick={(e) => {
onClick={ (e) =>
{
e.preventDefault();
sidebarExpanded
? handleClick()
@ -198,12 +215,12 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
} }
>
<FaChartSimple/>
{t('sidebar.leaderboard')}
{ t("sidebar.leaderboard") }
<ArrowIcon rotated={ open }/>
</NavLink>
<div
className={ `translate transform overflow-hidden ${
!open && 'hidden'
!open && "hidden"
}` }
>
<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
to="/leaderboard/stations"
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 ' +
(isActive && '!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")
}
>
<FaBuildingFlag/>
{t('sidebar.stations')}
{ t("sidebar.stations") }
</NavLink>
</li>
<li>
<NavLink
to="/leaderboard/trains"
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 ' +
(isActive && '!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")
}
>
<FaTrain/>
{t('sidebar.trains')}
{ t("sidebar.trains") }
</NavLink>
</li>
</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;
activeCondition: boolean;
}
const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) => {
const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) =>
{
const [ open, setOpen ] = useState<boolean>(activeCondition);
const handleClick = () => {
const handleClick = () =>
{
setOpen(!open);
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import React from "react";
interface CardDataStatsProps {
interface CardDataStatsProps
{
title: string;
total: string;
rate?: string;
@ -13,8 +14,9 @@ export const CardDataStats: React.FC<CardDataStatsProps> = ({
total,
rate,
levelUp,
levelDown
}) => {
levelDown,
}) =>
{
return (
<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
className={ `flex items-center gap-1 text-sm font-medium ${
levelUp && 'text-meta-3'
} ${levelDown && 'text-meta-5'} `}
levelUp && "text-meta-3"
} ${ levelDown && "text-meta-5" } ` }
>
{ 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
export const PageMeta = ({ title = '', description = '', image = '', name = '' }) => {
export const PageMeta = ({ title = "", description = "", image = "", name = "" }) =>
{
return (
<Helmet>
{ /* Standard metadata tags */ }

View File

@ -1,10 +1,11 @@
import { ChangeEventHandler } from 'react';
import { useTranslation } from 'react-i18next';
import { ChangeEventHandler } from "react";
import { useTranslation } from "react-i18next";
export const Search = ({ searchItem, handleInputChange }: {
searchItem: string;
handleInputChange: ChangeEventHandler
}) => {
}) =>
{
const { t } = useTranslation();
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">
@ -14,7 +15,7 @@ export const Search = ({ searchItem, handleInputChange }: {
type="text"
onChange={ handleInputChange }
value={ searchItem }
placeholder={t('logs.search')}
placeholder={ t("logs.search") }
/>
</div>
</div>;

View File

@ -1,16 +1,19 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { TLeaderboardRecord } from '../../../types/leaderboard.ts';
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
import { ContentLoader } from "../../mini/loaders/ContentLoader.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();
return (
<>
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
description={t('content_loader.notfound.description')} />}
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
description={ t("content_loader.notfound.description") }/> }
{ error === 0 && <ContentLoader/> }
{ 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">
@ -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="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.user')}
{ t("leaderboard.user") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.time')}
{ t("leaderboard.time") }
</h5>
</div>
<div className="hidden p-2.5 text-center sm:block xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.actions')}
{ t("leaderboard.actions") }
</h5>
</div>
</div>
@ -37,28 +40,28 @@ export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord
{ stations.map((station, key) => (
<div
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 }
>
<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">
<Link to={'/profile/' + station.steam}
className="color-orchid">{station.steamName}</Link>
<Link to={ "/profile/" + station.steam }
className="color-orchid">{ station.steamName }</Link> { station.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
</p>
</div>
<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 className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
<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"
>
{t('leaderboard.profile')}
{ t("leaderboard.profile") }
</Link>
</div>
</div>

View File

@ -1,17 +1,20 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { TLeaderboardRecord } from '../../../types/leaderboard.ts';
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
import { ContentLoader } from "../../mini/loaders/ContentLoader.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();
return (
<>
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
description={t('content_loader.notfound.description')} />}
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
description={ t("content_loader.notfound.description") }/> }
{ error === 0 && <ContentLoader/> }
{ 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">
@ -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="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.user')}
{ t("leaderboard.user") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.points')}
{ t("leaderboard.points") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.distance')}
{ t("leaderboard.distance") }
</h5>
</div>
<div className="p-2.5 text-center sm:block xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.time')}
{ t("leaderboard.time") }
</h5>
</div>
<div className="hidden p-2.5 text-center sm:block xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('leaderboard.actions')}
{ t("leaderboard.actions") }
</h5>
</div>
</div>
@ -47,15 +50,15 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
{ trains.map((train, key) => (
<div
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 }
>
<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">
<Link to={'/profile/' + train.steam}
className="color-orchid">{train.steamName}</Link>
<Link to={ "/profile/" + train.steam }
className="color-orchid">{ train.steamName }</Link> { train.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
</p>
</div>
@ -68,15 +71,15 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
</div>
<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 className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
<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"
>
{t('leaderboard.profile')}
{ t("leaderboard.profile") }
</Link>
</div>
</div>

View File

@ -1,21 +1,26 @@
import { useTranslation } from 'react-i18next';
import { TLogStationData } from '../../../types/log.ts';
import dayjs from 'dayjs';
import { toast } from 'react-toastify';
import { useTranslation } from "react-i18next";
import { TLogStationData } from "../../../types/log.ts";
import dayjs from "dayjs";
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 copyLink = () => {
const copyLink = () =>
{
void navigator.clipboard.writeText(location.href);
toast.success(t('log.toasts.copied'));
toast.success(t("log.toasts.copied"));
};
const report = () => {
toast.info(t('log.toasts.report'), {
autoClose: 5000
const report = () =>
{
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
@ -29,7 +34,7 @@ export const StationLog = ({ data }: { data: TLogStationData }) => {
</div>
<div className="mt-4">
<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>
</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="flex flex-col sm:flex-row sm:flex-wrap sm:justify-end">
<div className="flex flex-col">
<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.station', { name: data.stationName, short: data.stationShort })}</p>
<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.station", { name: data.stationName, short: data.stationShort }) }</p>
{ data.joinedDate &&
<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.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>
{ 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 className="flex flex-col gap-5 mt-5 sm:mt-0 sm:ml-auto">
<a
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"
>
{t('log.buttons.report')}
{ t("log.buttons.report") }
</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"
>
{t('log.buttons.copy')}
{ t("log.buttons.copy") }
</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>

View File

@ -1,21 +1,27 @@
import { useTranslation } from 'react-i18next';
import { TLogTrainData } from '../../../types/log.ts';
import dayjs from 'dayjs';
import { toast } from 'react-toastify';
import { useTranslation } from "react-i18next";
import { TLogTrainData } from "../../../types/log.ts";
import dayjs from "dayjs";
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 copyLink = () => {
const copyLink = () =>
{
void navigator.clipboard.writeText(location.href);
toast.success(t('log.toasts.copied'));
toast.success(t("log.toasts.copied"));
};
const report = () => {
toast.info(t('log.toasts.report'), {
autoClose: 5000
const report = () =>
{
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
@ -29,7 +35,7 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) => {
</div>
<div className="mt-4">
<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>
</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="flex flex-col sm:flex-row sm:flex-wrap sm:justify-end">
<div className="flex flex-col">
<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.train', { name: data.trainName, number: data.trainNumber })}</p>
<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.train", { name: data.trainName, number: data.trainNumber }) }</p>
{ (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 &&
<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.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>
{ 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 className="flex flex-col gap-5 mt-5 sm:mt-0 sm:ml-auto">
<a
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"
>
{t('log.buttons.report')}
{ t("log.buttons.report") }
</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"
>
{t('log.buttons.copy')}
{ t("log.buttons.copy") }
</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>

View File

@ -1,21 +1,23 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import dayjs from 'dayjs';
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
import { TStationRecord } from '../../../types/station.ts';
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import dayjs from "dayjs";
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
import { TStationRecord } from "../../../types/station.ts";
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
import { FaCheck } from 'react-icons/fa6';
// setSearchItem: Dispatch<SetStateAction<string>>
export const StationTable = ({ stations, error }: {
stations: TStationRecord[], error: number
}) => {
}) =>
{
const { t } = useTranslation();
return (
<>
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
description={t('content_loader.notfound.description')} />}
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
description={ t("content_loader.notfound.description") }/> }
{ error === 0 && <ContentLoader/> }
{ 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="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.user')}
{ t("logs.user") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.station')}
{ t("logs.station") }
</h5>
</div>
<div className="hidden sm:block p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.time')}
{ t("logs.time") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.actions')}
{ t("logs.actions") }
</h5>
</div>
</div>
@ -47,39 +49,39 @@ export const StationTable = ({ stations, error }: {
{ stations.map((station, key) => (
<div
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 }
>
<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">
<Link to={'/profile/' + station.userSteamId}
className="color-orchid">{station.userUsername}</Link>
<Link to={ "/profile/" + station.userSteamId }
className="color-orchid">{ station.userUsername }</Link> { station.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
</p>
</div>
<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 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
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
<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"
>
{t('logs.profile')}
{ t("logs.profile") }
</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"
>
{t('logs.record')}
{ t("logs.record") }
</Link>
</div>
</div>

View File

@ -1,21 +1,22 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { TTrainRecord } from '../../../types/train.ts';
import dayjs from 'dayjs';
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TTrainRecord } from "../../../types/train.ts";
import dayjs from "dayjs";
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
import { FaCheck } from 'react-icons/fa6';
// setSearchItem: Dispatch<SetStateAction<string>>
export const TrainTable = ({ trains, error }: {
trains: TTrainRecord[], error: number
}) => {
}) =>
{
const { t } = useTranslation();
return (
<>
{error === 2 && <WarningAlert title={t('content_loader.notfound.header')}
description={t('content_loader.notfound.description')} />}
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
description={ t("content_loader.notfound.description") }/> }
{ error === 0 && <ContentLoader/> }
{ 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">
@ -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="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.user')}
{ t("logs.user") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.train')}
{ t("logs.train") }
</h5>
</div>
<div className="hidden sm:block p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.points')}
{ t("logs.points") }
</h5>
</div>
<div className="hidden sm:block p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.distance')}
{ t("logs.distance") }
</h5>
</div>
<div className="hidden sm:block p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.time')}
{ t("logs.time") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('logs.actions')}
{ t("logs.actions") }
</h5>
</div>
</div>
@ -56,47 +57,47 @@ export const TrainTable = ({ trains, error }: {
{ trains.map((train, key) => (
<div
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 }
>
<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">
<Link to={'/profile/' + train.userSteamId}
className="color-orchid">{train.userUsername}</Link>
<Link to={ "/profile/" + train.userSteamId }
className="color-orchid">{ train.userUsername }</Link> { train.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
</p>
</div>
<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 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 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 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
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
<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"
>
{t('logs.profile')}
{ t("logs.profile") }
</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"
>
{t('logs.record')}
{ t("logs.record") }
</Link>
</div>
</div>

View File

@ -1,9 +1,12 @@
import { useState } from 'react';
import { TProfileData } from '../../../types/profile.ts';
import { useTranslation } from 'react-i18next';
import { ArrowIcon } from '../../mini/icons/ArrowIcon.tsx';
import { useState } from "react";
import { TProfileData } from "../../../types/profile.ts";
import { useTranslation } from "react-i18next";
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 [ showStations, setShowStations ] = useState(false);
@ -20,7 +23,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
</div>
<div className="mt-4">
<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>
<div
@ -30,14 +33,14 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
<span className="font-semibold text-black dark:text-white">
{ Math.floor(data.player.trainDistance / 1000) }km
</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
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">
{Math.floor(data.player.dispatcherTime / 3600000)}h
{ formatTime(data.player.dispatcherTime) }
</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>
@ -45,7 +48,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
{ 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="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 }/>
</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="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('profile.trains.train')}
{ t("profile.trains.train") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('profile.trains.distance')}
{ t("profile.trains.distance") }
</h5>
</div>
<div className="hidden sm:block p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('profile.trains.points')}
{ t("profile.trains.points") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('profile.trains.time')}
{ t("profile.trains.time") }
</h5>
</div>
</div>
{Object.keys(data.player.trainStats).map(trainName => {
{ Object.keys(data.player.trainStats).map(trainName =>
{
const train = data.player.trainStats[ trainName ];
return <div
@ -96,7 +100,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
</div>
<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>;
}) }
@ -108,7 +112,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
{ 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="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 }/>
</div>
{ 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="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('profile.stations.station')}
{ t("profile.stations.station") }
</h5>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t('profile.stations.time')}
{ t("profile.stations.time") }
</h5>
</div>
</div>
{Object.keys(data.player.dispatcherStats).map(stationName => {
{ Object.keys(data.player.dispatcherStats).map(stationName =>
{
const station = data.player.dispatcherStats[ stationName ];
return <div
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 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>;
}) }

View File

@ -59,8 +59,8 @@
},
"profile": {
"stats": {
"distance": "Kilometers traveled",
"time": "Dispatcher hours"
"distance": "Driver experience",
"time": "Dispatcher experience"
},
"trains": {
"header": "Train Statistics",
@ -87,12 +87,14 @@
},
"log": {
"errors": {
"notfound": {
"title": "Record not found",
"description": "This record could not be found."
},
"blacklist": {
"title": "The record cannot be displayed",
"description": "The record has been blocked."
}
},
"station": {
"header": "Leaving the station",
@ -118,7 +120,8 @@
},
"buttons": {
"report": "Report",
"copy": "Copy link"
"copy": "Copy link",
"profile": "Profile"
}
},
"sidebar": {

View File

@ -59,8 +59,8 @@
},
"profile": {
"stats": {
"distance": "Przejechanych kilometrów",
"time": "Godzin dyżurów"
"distance": "Staż maszynisty",
"time": "Staż dyżurnego ruchu"
},
"trains": {
"header": "Statystyki pociągów",
@ -87,12 +87,14 @@
},
"log": {
"errors": {
"notfound": {
"title": "Nie znaleziono rekordu",
"description": "Ten rekord nie został znaleziony."
},
"blacklist": {
"title": "Nie można wyświetlić rekordu",
"description": "Rekord został zablokowany."
}
},
"station": {
"header": "Wyjście z posterunku",
@ -118,7 +120,8 @@
},
"buttons": {
"report": "Zgłoś",
"copy": "Kopiuj link"
"copy": "Kopiuj link",
"profile": "Profil"
}
},
"sidebar": {

View File

@ -1,8 +1,9 @@
import React, { useState, ReactNode } from 'react';
import { Header } from '../components/mini/header/Header';
import { Sidebar } from '../components/mini/sidebar/Sidebar';
import React, { useState, ReactNode } from "react";
import { Header } from "../components/mini/header/Header";
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);
return (

View File

@ -1,21 +1,24 @@
import React, { useEffect, useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom';
import { TStatsResponse } from '../types/stats.ts';
import { WarningAlert } from '../components/mini/alerts/Warning.tsx';
import { CardDataStats } from '../components/mini/util/CardDataStats.tsx';
import React, { useEffect, useState } from "react";
import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
import { TStatsResponse } from "../types/stats.ts";
import { WarningAlert } from "../components/mini/alerts/Warning.tsx";
import { CardDataStats } from "../components/mini/util/CardDataStats.tsx";
export const Home: React.FC = () => {
export const Home: React.FC = () =>
{
const { t } = useTranslation();
const [commit, setCommit] = useState('');
const [version, setVersion] = useState('');
const [ commit, setCommit ] = useState("");
const [ version, setVersion ] = useState("");
const [ trains, setTrains ] = useState(0);
const [ dispatchers, setDispatchers ] = useState(0);
const [ profiles, setProfiles ] = useState(0);
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/stats/`).then(x => x.json()).then((data: TStatsResponse) => {
useEffect(() =>
{
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.version && setVersion(data.data.git.version);
@ -29,14 +32,14 @@ export const Home: React.FC = () => {
return (
<>
<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 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">
<CardDataStats title={t('home.stats.trains')} total={trains.toString()} />
<CardDataStats title={t('home.stats.dispatchers')} total={dispatchers.toString()} />
<CardDataStats title={t('home.stats.profiles')} total={profiles.toString()} />
<CardDataStats title={ t("home.stats.trains") } total={ trains.toString() }/>
<CardDataStats title={ t("home.stats.dispatchers") } total={ dispatchers.toString() }/>
<CardDataStats title={ t("home.stats.profiles") } total={ profiles.toString() }/>
</div>
@ -45,22 +48,22 @@ export const Home: React.FC = () => {
<div className="px-4 pb-6 text-center">
<div className="mt-4">
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
{t('home.title')}
{ t("home.title") }
</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">
<Link
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"
>
{t('home.buttons.project')}
{ t("home.buttons.project") }
</Link>
<Link
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"
>
{t('home.buttons.forum')}
{ t("home.buttons.forum") }
</Link>
</div>
</div>
@ -73,28 +76,29 @@ export const Home: React.FC = () => {
<div className="mt-6.5">
<p><Trans
i18nKey={t('home.footer.author')}
values={{ author: 'alekswilc' }}
i18nKey={ t("home.footer.author") }
values={ { author: "alekswilc" } }
components={ {
anchor: <Link className="color-orchid" to={'https://www.alekswilc.dev'} />
anchor: <Link className="color-orchid" to={ "https://www.alekswilc.dev" }/>,
} }
/></p>
<p><Trans
i18nKey={t('home.footer.thanks')}
i18nKey={ t("home.footer.thanks") }
components={ {
bahu: <Link className="color-orchid" to={'https://bahu.pro/'} />,
bahu: <Link className="color-orchid" to={ "https://bahu.pro/" }/>,
simrailelite: <Link className="color-orchid"
to={'https://discord.gg/yDhy3pDrVr'} />
to={ "https://discord.gg/yDhy3pDrVr" }/>,
} }
/></p>
<p>{t('home.footer.license')} <Link className="color-orchid"
to={'https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/src/branch/main/LICENSE'}>GNU
<p>{ t("home.footer.license") } <Link className="color-orchid"
to={ "https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/src/branch/main/LICENSE" }>GNU
AGPL V3</Link></p>
<p>{t('home.footer.powered')} <Link className="color-orchid"
to={'https://tailadmin.com/'}>TailAdmin</Link></p>
<p>{ t("home.footer.powered") } <Link className="color-orchid"
to={ "https://tailadmin.com/" }>TailAdmin</Link>
</p>
<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"
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 { useDebounce } from "use-debounce";
import { Search } from "../../components/mini/util/Search.tsx";
import { useSearchParams } from "react-router-dom";
export const StationLeaderboard = () =>
{
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
useEffect(() =>
{
fetch(`${ import.meta.env.VITE_API_URL }leaderboard/station/`).then(x => x.json()).then(x =>
{
setData(x.data.records);
});
}, []);
const [ searchItem, setSearchItem ] = useState("");
const [ searchValue ] = useDebounce(searchItem, 500);
const [ error, setError ] = useState<0 | 1 | 2>(0);
useEffect(() =>
{
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
setSearchParams(searchParams);
setData([]);
setError(0);
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 ]);
useEffect(() =>
{
setSearchItem(searchParams.get("q") ?? "");
}, [ searchParams ]);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
{
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 { useDebounce } from "use-debounce";
import { Search } from "../../components/mini/util/Search.tsx";
import { useSearchParams } from "react-router-dom";
export const TrainLeaderboard = () =>
{
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
useEffect(() =>
{
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 [ error, setError ] = useState<0 | 1 | 2>(0);
useEffect(() =>
{
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
setSearchParams(searchParams);
setData([]);
setError(0);
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 ]);
useEffect(() =>
{
setSearchItem(searchParams.get("q") ?? "");
}, [ searchParams ]);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
{
setSearchItem(e.target.value);

View File

@ -1,23 +1,27 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ContentLoader } from '../../components/mini/loaders/ContentLoader.tsx';
import { WarningAlert } from '../../components/mini/alerts/Warning.tsx';
import { useTranslation } from 'react-i18next';
import { TLogResponse, TLogStationData, TLogTrainData } from '../../types/log.ts';
import { StationLog } from '../../components/pages/log/StationLog.tsx';
import { TrainLog } from '../../components/pages/log/TrainLog.tsx';
import { PageMeta } from '../../components/mini/util/PageMeta.tsx';
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx";
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
import { useTranslation } from "react-i18next";
import { TLogResponse, TLogStationData, TLogTrainData } from "../../types/log.ts";
import { StationLog } from "../../components/pages/log/StationLog.tsx";
import { TrainLog } from "../../components/pages/log/TrainLog.tsx";
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
export const Log = () => {
export const Log = () =>
{
const { id } = useParams();
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
const [ trainData, setTrainData ] = useState<TLogTrainData>(undefined!);
const [ stationData, setStationData ] = useState<TLogStationData>(undefined!);
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/log/${id}`).then(x => x.json()).then((data: TLogResponse) => {
switch (data.code) {
useEffect(() =>
{
fetch(`${ import.meta.env.VITE_API_URL }/log/${ id }`).then(x => x.json()).then((data: TLogResponse) =>
{
switch (data.code)
{
case 404:
setError(2);
break;
@ -28,7 +32,7 @@ export const Log = () => {
case 200:
setError(1);
'trainNumber' in data.data ? setTrainData(data.data) : setStationData(data.data);
"trainNumber" in data.data ? setTrainData(data.data) : setStationData(data.data);
break;
}
});
@ -43,13 +47,13 @@ export const Log = () => {
{/* NOT FOUND */ }
{ error === 2 && <PageMeta title="simrail.alekswilc.dev | Record not found"
description="This record could not be found."/> }
{error === 2 && <WarningAlert title={t('log.errors.notfound.title')}
description={t('log.errors.notfound.description')} />}
{ error === 2 && <WarningAlert title={ t("log.errors.notfound.title") }
description={ t("log.errors.notfound.description") }/> }
{/* BLACKLISTED LOG */ }
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted record"
description="The record has been blocked."/> }
{error === 3 && <WarningAlert title={t('log.errors.blacklist.title')}
description={t('log.errors.blacklist.description')} />}
{ error === 3 && <WarningAlert title={ t("log.errors.blacklist.title") }
description={ t("log.errors.blacklist.description") }/> }
{/* SUCCESS */ }
{ error === 1 && stationData && <PageMeta
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 { TStationRecord } from "../../types/station.ts";
import { Search } from "../../components/mini/util/Search.tsx";
import { useSearchParams } from "react-router-dom";
export const StationLogs = () =>
{
const [ data, setData ] = useState<TStationRecord[]>([]);
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
useEffect(() =>
{
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 [ searchItem, setSearchItem ] = useState("");
const [ searchValue ] = useDebounce(searchItem, 500);
useEffect(() =>
{
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
setSearchParams(searchParams);
setData([]);
setError(0);
fetch(`${ import.meta.env.VITE_API_URL }/stations/?q=${ searchValue }`).then(x => x.json()).then(x =>
@ -29,6 +34,11 @@ export const StationLogs = () =>
});
}, [ searchValue ]);
useEffect(() =>
{
setSearchItem(searchParams.get("q") ?? "");
}, [ searchParams ]);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
{
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 { useDebounce } from "use-debounce";
import { Search } from "../../components/mini/util/Search.tsx";
import { useSearchParams } from "react-router-dom";
export const TrainLogs = () =>
{
const [ data, setData ] = useState<TTrainRecord[]>([]);
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
useEffect(() =>
{
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);
});
}, []);
const [ error, setError ] = useState<0 | 1 | 2>(0);
const [ searchItem, setSearchItem ] = useState("");
const [ searchValue ] = useDebounce(searchItem, 500);
useEffect(() =>
{
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
setSearchParams(searchParams);
setData([]);
setError(0);
fetch(`${ import.meta.env.VITE_API_URL }/trains/?q=${ searchValue }`).then(x => x.json()).then(x =>
@ -29,6 +36,11 @@ export const TrainLogs = () =>
});
}, [ searchValue ]);
useEffect(() =>
{
setSearchItem(searchParams.get("q") ?? "");
}, [ searchParams ]);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
{
setSearchItem(e.target.value);

View File

@ -1,22 +1,25 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { TProfileData, TProfileResponse } from '../../types/profile.ts';
import { ContentLoader } from '../../components/mini/loaders/ContentLoader.tsx';
import { WarningAlert } from '../../components/mini/alerts/Warning.tsx';
import { ProfileCard } from '../../components/pages/profile/Profile.tsx';
import { useTranslation } from 'react-i18next';
import { PageMeta } from '../../components/mini/util/PageMeta.tsx';
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { TProfileData, TProfileResponse } from "../../types/profile.ts";
import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx";
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
import { ProfileCard } from "../../components/pages/profile/Profile.tsx";
import { useTranslation } from "react-i18next";
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
import { formatTime } from "../../util/time.ts";
export const Profile = () => {
export const Profile = () =>
{
const { id } = useParams();
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
const [ data, setData ] = useState<TProfileData>(undefined!);
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/profiles/${id}`).then(x => x.json()).then((data: TProfileResponse) => {
switch (data.code) {
useEffect(() =>
{
fetch(`${ import.meta.env.VITE_API_URL }/profiles/${ id }`).then(x => x.json()).then((data: TProfileResponse) =>
{
switch (data.code)
{
case 404:
setError(2);
break;
@ -40,18 +43,18 @@ export const 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."/> }
{error === 2 && <WarningAlert title={t('profile.errors.notfound.title')}
description={t('profile.errors.notfound.description')} />}
{ error === 2 && <WarningAlert title={ t("profile.errors.notfound.title") }
description={ t("profile.errors.notfound.description") }/> }
{/* BLACKLISTED PROFILE */ }
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted profile"
description="This player's profile has been blocked."/> }
{error === 3 && <WarningAlert title={t('profile.errors.blacklist.title')}
description={t('profile.errors.blacklist.description')} />}
{ error === 3 && <WarningAlert title={ t("profile.errors.blacklist.title") }
description={ t("profile.errors.blacklist.description") }/> }
{/* SUCCESS */ }
{ error === 1 && <PageMeta image={ data.steam.avatarfull }
title={ `simrail.alekswilc.dev | ${ data.steam.personname }'s profile` }
description={`${data.player.trainDistance ? 0 : ((data.player.trainDistance / 1000).toFixed(2))} kilometers travelled |
${data.player.dispatcherTime ? 0 : Math.floor(data.player.dispatcherTime / 3600000)}h dispatchers hours`} />}
description={ `${ data.player.trainDistance ? 0 : ((data.player.trainDistance / 1000).toFixed(2)) } driving experience |
${ data.player.dispatcherTime ? 0 : formatTime(data.player.dispatcherTime) } dispatcher experience` }/> }
{ error === 1 && <ProfileCard data={ data }/> }
</>
);

View File

@ -21,6 +21,7 @@ export interface TLeaderboardRecord
dispatcherTime: number;
dispatcherStats?: { [ key: string ]: TLeaderboardDispatcherStat };
trainStats?: { [ key: string ]: TLeaderboardTrainStat };
verified: boolean;
}
export interface TLeaderboardDispatcherStat

View File

@ -18,7 +18,7 @@ export interface TLogTrainData
server: string;
trainName: string;
joinedDate?: number;
verified: boolean;
}
export interface TLogStationData
@ -32,4 +32,6 @@ export interface TLogStationData
stationShort: string;
server: string;
joinedDate?: number;
verified: boolean;
}

View File

@ -32,6 +32,7 @@ export interface TProfilePlayer
trainStats: Record<string, TProfileTrainStatsRecord>;
trainDistance: number;
trainPoints: number;
verified: boolean;
}
export interface TProfileDispatcherStatsRecord

View File

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

View File

@ -23,5 +23,6 @@ export interface TTrainRecord
points: number;
server: 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';
}