feat(): Add veritication mark #50
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
);
|
||||
});
|
||||
|
@ -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(),
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
});
|
||||
}, []);
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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 */ }
|
||||
|
@ -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>;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>;
|
||||
}) }
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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 }` }
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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 }/> }
|
||||
</>
|
||||
);
|
||||
|
@ -21,6 +21,7 @@ export interface TLeaderboardRecord
|
||||
dispatcherTime: number;
|
||||
dispatcherStats?: { [ key: string ]: TLeaderboardDispatcherStat };
|
||||
trainStats?: { [ key: string ]: TLeaderboardTrainStat };
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export interface TLeaderboardDispatcherStat
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ export interface TProfilePlayer
|
||||
trainStats: Record<string, TProfileTrainStatsRecord>;
|
||||
trainDistance: number;
|
||||
trainPoints: number;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export interface TProfileDispatcherStatsRecord
|
||||
|
@ -21,4 +21,5 @@ export interface TStationRecord
|
||||
stationShort: string;
|
||||
server: string;
|
||||
joinedDate?: number;
|
||||
verified: boolean;
|
||||
}
|
@ -23,5 +23,6 @@ export interface TTrainRecord
|
||||
points: number;
|
||||
server: string;
|
||||
trainName: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
|
14
packages/frontend/src/util/time.ts
Normal file
14
packages/frontend/src/util/time.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const formatTime = (time: number) =>
|
||||
{
|
||||
if (Math.floor(time / 3600000) > 0)
|
||||
{
|
||||
return `${ Math.floor(time / 3600000) }h`;
|
||||
}
|
||||
|
||||
if (Math.floor(time / 60000) > 0)
|
||||
{
|
||||
return `${ Math.floor(time / 60000) }m`
|
||||
}
|
||||
|
||||
return '0h';
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user