Merge pull request 'feat(): Add veritication mark' (#50) from v3 into preview
Reviewed-on: simrail/simrail.alekswilc.dev#50 Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
This commit is contained in:
commit
20c29173f5
@ -4,6 +4,7 @@ import { MBlacklist } from "../../mongo/blacklist.js";
|
||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { 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,51 +1,54 @@
|
||||
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() {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
function App()
|
||||
{
|
||||
const [ loading, setLoading ] = useState<boolean>(true);
|
||||
const { pathname } = useLocation();
|
||||
const [theme] = useColorMode();
|
||||
useEffect(() => {
|
||||
const [ theme ] = useColorMode();
|
||||
useEffect(() =>
|
||||
{
|
||||
window.scrollTo(0, 0);
|
||||
}, [pathname]);
|
||||
}, [ pathname ]);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() =>
|
||||
{
|
||||
setTimeout(() => setLoading(false), 400);
|
||||
}, []);
|
||||
|
||||
return <HelmetProvider>
|
||||
|
||||
|
||||
{loading ? (
|
||||
<Loader />
|
||||
{ loading ? (
|
||||
<Loader/>
|
||||
) : (
|
||||
<>
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={1500}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
autoClose={ 1500 }
|
||||
hideProgressBar={ false }
|
||||
newestOnTop={ false }
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
rtl={ false }
|
||||
pauseOnHover
|
||||
theme={theme as 'light' | 'dark'}
|
||||
theme={ theme as "light" | "dark" }
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<Routes>
|
||||
@ -54,8 +57,8 @@ function App() {
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.alekswilc.dev | Home"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!" />
|
||||
<Home />
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<Home/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -64,8 +67,8 @@ function App() {
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.alekswilc.dev | Train Leaderboard"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!" />
|
||||
<TrainLeaderboard />
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<TrainLeaderboard/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -75,8 +78,8 @@ function App() {
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.alekswilc.dev | Trains Logs"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!" />
|
||||
<TrainLogs />
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<TrainLogs/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -86,8 +89,8 @@ function App() {
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.alekswilc.dev | Stations Logs"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!" />
|
||||
<StationLogs />
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<StationLogs/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -97,8 +100,8 @@ function App() {
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.alekswilc.dev | Station Leaderboard"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!" />
|
||||
<StationLeaderboard />
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<StationLeaderboard/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -108,9 +111,9 @@ function App() {
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.alekswilc.dev | Profile"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!" />
|
||||
{/* page meta is modified in component! */}
|
||||
<Profile />
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
{/* page meta is modified in component! */ }
|
||||
<Profile/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -120,16 +123,16 @@ function App() {
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.alekswilc.dev | Log"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!" />
|
||||
{/* page title is modified after API response */}
|
||||
<Log />
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
{/* page title is modified after API response */ }
|
||||
<Log/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</DefaultLayout>
|
||||
</>
|
||||
)}
|
||||
) }
|
||||
</HelmetProvider>;
|
||||
}
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
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">
|
||||
<div className="mr-5 flex h-9 w-full max-w-[36px] items-center justify-center rounded-lg bg-[#F87171]">
|
||||
<ErrorAlertIcon />
|
||||
<ErrorAlertIcon/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<h5 className="mb-3 font-semibold text-[#B45454]">
|
||||
{title}
|
||||
{ title }
|
||||
</h5>
|
||||
<ul>
|
||||
<li className="leading-relaxed text-[#CD5D5D]">
|
||||
{description}
|
||||
{ description }
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { SuccessAlertIcon } from '../icons/AlertIcons.tsx';
|
||||
import { SuccessAlertIcon } from "../icons/AlertIcons.tsx";
|
||||
|
||||
export const SuccessAlert = ({ title, description }: { title: string, description: string }) =>
|
||||
<div
|
||||
className="flex w-full border-l-6 border-[#34D399] bg-[#34D399] bg-opacity-[15%] dark:bg-[#1B1B24] px-7 py-8 shadow-md dark:bg-opacity-30 md:p-9">
|
||||
<div className="mr-5 flex h-9 w-full max-w-[36px] items-center justify-center rounded-lg bg-[#34D399]">
|
||||
<SuccessAlertIcon />
|
||||
<SuccessAlertIcon/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<h5 className="mb-3 text-lg font-semibold text-black dark:text-[#34D399] ">
|
||||
{title}
|
||||
{ title }
|
||||
</h5>
|
||||
<p className="text-base leading-relaxed text-body">
|
||||
{description}
|
||||
{ description }
|
||||
</p>
|
||||
</div>
|
||||
</div>;
|
@ -1,17 +1,17 @@
|
||||
import { WarningAlertIcon } from '../icons/AlertIcons.tsx';
|
||||
import { WarningAlertIcon } from "../icons/AlertIcons.tsx";
|
||||
|
||||
export const WarningAlert = ({ title, description }: { title: string, description: string }) =>
|
||||
<div
|
||||
className="flex w-full border-l-6 border-warning bg-warning bg-opacity-[15%] dark:bg-[#1B1B24] px-7 py-8 shadow-md dark:bg-opacity-30 md:p-9">
|
||||
<div className="mr-5 flex h-9 w-9 items-center justify-center rounded-lg bg-warning bg-opacity-30">
|
||||
<WarningAlertIcon />
|
||||
<WarningAlertIcon/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<h5 className="mb-3 text-lg font-semibold text-[#9D5425]">
|
||||
{title}
|
||||
{ title }
|
||||
</h5>
|
||||
<p className="leading-relaxed text-[#D0915C]">
|
||||
{description}
|
||||
{ description }
|
||||
</p>
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -1,35 +1,38 @@
|
||||
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 [colorMode, setColorMode] = useColorMode();
|
||||
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'
|
||||
}`}
|
||||
className={ `relative m-0 block h-7.5 w-14 rounded-full ${
|
||||
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'
|
||||
}`}
|
||||
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"
|
||||
}` }
|
||||
>
|
||||
<span className="dark:hidden">
|
||||
<LightIcon />
|
||||
<LightIcon/>
|
||||
</span>
|
||||
<span className="hidden dark:inline-block">
|
||||
<DarkIcon />
|
||||
<DarkIcon/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
@ -1,51 +1,53 @@
|
||||
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);
|
||||
}}
|
||||
} }
|
||||
className="z-99999 block rounded-sm border border-stroke bg-white p-1.5 shadow-sm dark:border-strokedark dark:bg-boxdark lg:hidden"
|
||||
>
|
||||
<span className="relative block h-5.5 w-5.5 cursor-pointer">
|
||||
<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'
|
||||
}`}
|
||||
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"
|
||||
}` }
|
||||
></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'
|
||||
}`}
|
||||
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"
|
||||
}` }
|
||||
></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'
|
||||
}`}
|
||||
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"
|
||||
}` }
|
||||
></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]'
|
||||
}`}
|
||||
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]"
|
||||
}` }
|
||||
></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'
|
||||
}`}
|
||||
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"
|
||||
}` }
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
@ -57,15 +59,15 @@ 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">
|
||||
<DarkModeSwitcher />
|
||||
<DarkModeSwitcher/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
export const ArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
||||
<svg
|
||||
className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${
|
||||
rotated && 'rotate-180'
|
||||
}`}
|
||||
className={ `absolute right-4 top-1/2 -translate-y-1/2 fill-current ${
|
||||
rotated && "rotate-180"
|
||||
}` }
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -1,37 +1,38 @@
|
||||
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
|
||||
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">
|
||||
<div className="mr-5 flex h-9 w-full max-w-[36px] items-center justify-center rounded-lg bg-[#F87171]">
|
||||
<ErrorAlertIcon />
|
||||
<ErrorAlertIcon/>
|
||||
</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">
|
||||
{/* TODO: add git issue params */}
|
||||
{/* TODO: add git issue params */ }
|
||||
<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,20 +43,23 @@ export const LoadError = () => {
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const ContentLoader = () => {
|
||||
const [error, setError] = useState(false);
|
||||
useEffect(() => {
|
||||
new Promise(res => setTimeout(res, 5000)).then(() => {
|
||||
export const ContentLoader = () =>
|
||||
{
|
||||
const [ error, setError ] = useState(false);
|
||||
useEffect(() =>
|
||||
{
|
||||
new Promise(res => setTimeout(res, 5000)).then(() =>
|
||||
{
|
||||
setError(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{error ? <LoadError /> : <div
|
||||
{ error ? <LoadError/> : <div
|
||||
className="flex h-screen items-center justify-center shadow-default bg-white dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent" />
|
||||
</div>}
|
||||
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"/>
|
||||
</div> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
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" />
|
||||
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,91 +1,104 @@
|
||||
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 [sidebarExpanded, setSidebarExpanded] = useState(
|
||||
storedSidebarExpanded === null ? false : storedSidebarExpanded === 'true'
|
||||
const storedSidebarExpanded = localStorage.getItem("sidebar-expanded");
|
||||
const [ sidebarExpanded, setSidebarExpanded ] = useState(
|
||||
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");
|
||||
}
|
||||
}, [sidebarExpanded]);
|
||||
else
|
||||
{
|
||||
document.querySelector("body")?.classList.remove("sidebar-expanded");
|
||||
}
|
||||
}, [ sidebarExpanded ]);
|
||||
|
||||
return (
|
||||
<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'
|
||||
}`}
|
||||
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"
|
||||
}` }
|
||||
>
|
||||
{/* <!-- SIDEBAR HEADER --> */}
|
||||
{/* <!-- SIDEBAR HEADER --> */ }
|
||||
<div className="flex items-center justify-between gap-2 px-6 py-5.5 lg:py-6.5">
|
||||
<button
|
||||
ref={trigger}
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
ref={ trigger }
|
||||
onClick={ () => setSidebarOpen(!sidebarOpen) }
|
||||
aria-controls="sidebar"
|
||||
aria-expanded={sidebarOpen}
|
||||
aria-expanded={ sidebarOpen }
|
||||
className="block lg:hidden"
|
||||
>
|
||||
<HamburgerGoBackIcon />
|
||||
<HamburgerGoBackIcon/>
|
||||
</button>
|
||||
</div>
|
||||
{/* <!-- SIDEBAR HEADER --> */}
|
||||
{/* <!-- SIDEBAR HEADER --> */ }
|
||||
|
||||
<div className="no-scrollbar flex flex-col overflow-y-auto duration-300 ease-linear">
|
||||
<nav className="mt-5 py-4 px-4 lg:mt-9 lg:px-6">
|
||||
@ -94,13 +107,13 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
<li>
|
||||
<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'
|
||||
}`}
|
||||
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"
|
||||
}` }
|
||||
>
|
||||
<FaHome />
|
||||
{t('sidebar.home')}
|
||||
<FaHome/>
|
||||
{ t("sidebar.home") }
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
@ -108,409 +121,413 @@ 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'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
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"
|
||||
}` }
|
||||
onClick={ (e) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
sidebarExpanded
|
||||
? handleClick()
|
||||
: setSidebarExpanded(true);
|
||||
}}
|
||||
} }
|
||||
>
|
||||
<FaClipboardList />
|
||||
{t('sidebar.logs')}
|
||||
<ArrowIcon rotated={open} />
|
||||
<FaClipboardList/>
|
||||
{ t("sidebar.logs") }
|
||||
<ArrowIcon rotated={ open }/>
|
||||
</NavLink>
|
||||
|
||||
<div
|
||||
className={`translate transform overflow-hidden ${
|
||||
!open && 'hidden'
|
||||
}`}
|
||||
className={ `translate transform overflow-hidden ${
|
||||
!open && "hidden"
|
||||
}` }
|
||||
>
|
||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||
<li>
|
||||
<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')
|
||||
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")
|
||||
}
|
||||
>
|
||||
<FaBuildingFlag />
|
||||
{t('sidebar.stations')}
|
||||
<FaBuildingFlag/>
|
||||
{ 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')
|
||||
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")
|
||||
}
|
||||
>
|
||||
<FaTrain />
|
||||
{t('sidebar.trains')}
|
||||
<FaTrain/>
|
||||
{ t("sidebar.trains") }
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
} }
|
||||
</SidebarLinkGroup>
|
||||
|
||||
<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'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
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"
|
||||
}` }
|
||||
onClick={ (e) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
sidebarExpanded
|
||||
? handleClick()
|
||||
: setSidebarExpanded(true);
|
||||
}}
|
||||
} }
|
||||
>
|
||||
<FaChartSimple />
|
||||
{t('sidebar.leaderboard')}
|
||||
<ArrowIcon rotated={open} />
|
||||
<FaChartSimple/>
|
||||
{ t("sidebar.leaderboard") }
|
||||
<ArrowIcon rotated={ open }/>
|
||||
</NavLink>
|
||||
<div
|
||||
className={`translate transform overflow-hidden ${
|
||||
!open && 'hidden'
|
||||
}`}
|
||||
className={ `translate transform overflow-hidden ${
|
||||
!open && "hidden"
|
||||
}` }
|
||||
>
|
||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||
<li>
|
||||
<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')
|
||||
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")
|
||||
}
|
||||
>
|
||||
<FaBuildingFlag />
|
||||
{t('sidebar.stations')}
|
||||
<FaBuildingFlag/>
|
||||
{ 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')
|
||||
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")
|
||||
}
|
||||
>
|
||||
<FaTrain />
|
||||
{t('sidebar.trains')}
|
||||
<FaTrain/>
|
||||
{ t("sidebar.trains") }
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
} }
|
||||
</SidebarLinkGroup>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* TODO: add admin panel with simple auth */}
|
||||
{/*{false && <div>*/}
|
||||
{/* <h3 className="mb-4 ml-4 text-sm font-semibold text-bodydark2">*/}
|
||||
{/* {t('sidebar.admin')}*/}
|
||||
{/* </h3>*/}
|
||||
{/* TODO: add admin panel with simple auth */ }
|
||||
{/*{false && <div>*/ }
|
||||
{/* <h3 className="mb-4 ml-4 text-sm font-semibold text-bodydark2">*/ }
|
||||
{/* {t('sidebar.admin')}*/ }
|
||||
{/* </h3>*/ }
|
||||
|
||||
{/* <ul className="mb-6 flex flex-col gap-1.5">*/}
|
||||
{/* */}{/*{/* <!-- Menu Item Chart --> */}
|
||||
{/* <li>*/}
|
||||
{/* <NavLink*/}
|
||||
{/* to="/chart"*/}
|
||||
{/* 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.includes('chart') && 'bg-graydark dark:bg-meta-4'*/}
|
||||
{/* }`}*/}
|
||||
{/* >*/}
|
||||
{/* <svg*/}
|
||||
{/* className="fill-current"*/}
|
||||
{/* width="18"*/}
|
||||
{/* height="19"*/}
|
||||
{/* viewBox="0 0 18 19"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* >*/}
|
||||
{/* <g clipPath="url(#clip0_130_9801)">*/}
|
||||
{/* <path*/}
|
||||
{/* d="M10.8563 0.55835C10.5188 0.55835 10.2095 0.8396 10.2095 1.20522V6.83022C10.2095 7.16773 10.4907 7.4771 10.8563 7.4771H16.8751C17.0438 7.4771 17.2126 7.39272 17.3251 7.28022C17.4376 7.1396 17.4938 6.97085 17.4938 6.8021C17.2688 3.28647 14.3438 0.55835 10.8563 0.55835ZM11.4751 6.15522V1.8521C13.8095 2.13335 15.6938 3.8771 16.1438 6.18335H11.4751V6.15522Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* <path*/}
|
||||
{/* d="M15.3845 8.7427H9.1126V2.69582C9.1126 2.35832 8.83135 2.07707 8.49385 2.07707C8.40947 2.07707 8.3251 2.07707 8.24072 2.07707C3.96572 2.04895 0.506348 5.53645 0.506348 9.81145C0.506348 14.0864 3.99385 17.5739 8.26885 17.5739C12.5438 17.5739 16.0313 14.0864 16.0313 9.81145C16.0313 9.6427 16.0313 9.47395 16.0032 9.33332C16.0032 8.99582 15.722 8.7427 15.3845 8.7427ZM8.26885 16.3083C4.66885 16.3083 1.77197 13.4114 1.77197 9.81145C1.77197 6.3802 4.47197 3.53957 7.8751 3.3427V9.36145C7.8751 9.69895 8.15635 10.0083 8.52197 10.0083H14.7938C14.6813 13.4958 11.7845 16.3083 8.26885 16.3083Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* </g>*/}
|
||||
{/* <defs>*/}
|
||||
{/* <clipPath id="clip0_130_9801">*/}
|
||||
{/* <rect*/}
|
||||
{/* width="18"*/}
|
||||
{/* height="18"*/}
|
||||
{/* fill="white"*/}
|
||||
{/* transform="translate(0 0.052124)"*/}
|
||||
{/* />*/}
|
||||
{/* </clipPath>*/}
|
||||
{/* </defs>*/}
|
||||
{/* </svg>*/}
|
||||
{/* Chart*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* </li>*/}
|
||||
{/* */}{/*{/* <!-- Menu Item Chart --> */}
|
||||
{/* <ul className="mb-6 flex flex-col gap-1.5">*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Chart --> */ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/chart"*/ }
|
||||
{/* 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.includes('chart') && 'bg-graydark dark:bg-meta-4'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* >*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className="fill-current"*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="19"*/ }
|
||||
{/* viewBox="0 0 18 19"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <g clipPath="url(#clip0_130_9801)">*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M10.8563 0.55835C10.5188 0.55835 10.2095 0.8396 10.2095 1.20522V6.83022C10.2095 7.16773 10.4907 7.4771 10.8563 7.4771H16.8751C17.0438 7.4771 17.2126 7.39272 17.3251 7.28022C17.4376 7.1396 17.4938 6.97085 17.4938 6.8021C17.2688 3.28647 14.3438 0.55835 10.8563 0.55835ZM11.4751 6.15522V1.8521C13.8095 2.13335 15.6938 3.8771 16.1438 6.18335H11.4751V6.15522Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M15.3845 8.7427H9.1126V2.69582C9.1126 2.35832 8.83135 2.07707 8.49385 2.07707C8.40947 2.07707 8.3251 2.07707 8.24072 2.07707C3.96572 2.04895 0.506348 5.53645 0.506348 9.81145C0.506348 14.0864 3.99385 17.5739 8.26885 17.5739C12.5438 17.5739 16.0313 14.0864 16.0313 9.81145C16.0313 9.6427 16.0313 9.47395 16.0032 9.33332C16.0032 8.99582 15.722 8.7427 15.3845 8.7427ZM8.26885 16.3083C4.66885 16.3083 1.77197 13.4114 1.77197 9.81145C1.77197 6.3802 4.47197 3.53957 7.8751 3.3427V9.36145C7.8751 9.69895 8.15635 10.0083 8.52197 10.0083H14.7938C14.6813 13.4958 11.7845 16.3083 8.26885 16.3083Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </g>*/ }
|
||||
{/* <defs>*/ }
|
||||
{/* <clipPath id="clip0_130_9801">*/ }
|
||||
{/* <rect*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="18"*/ }
|
||||
{/* fill="white"*/ }
|
||||
{/* transform="translate(0 0.052124)"*/ }
|
||||
{/* />*/ }
|
||||
{/* </clipPath>*/ }
|
||||
{/* </defs>*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* Chart*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Chart --> */ }
|
||||
|
||||
{/* */}{/*{/* <!-- Menu Item Ui Elements --> */}
|
||||
{/* <SidebarLinkGroup*/}
|
||||
{/* activeCondition={pathname === '/ui' || pathname.includes('ui')}*/}
|
||||
{/* >*/}
|
||||
{/* {(handleClick, open) => {*/}
|
||||
{/* return (*/}
|
||||
{/* <React.Fragment>*/}
|
||||
{/* <NavLink*/}
|
||||
{/* to="#"*/}
|
||||
{/* className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${*/}
|
||||
{/* (pathname === '/ui' || pathname.includes('ui')) &&*/}
|
||||
{/* 'bg-graydark dark:bg-meta-4'*/}
|
||||
{/* }`}*/}
|
||||
{/* onClick={(e) => {*/}
|
||||
{/* e.preventDefault();*/}
|
||||
{/* sidebarExpanded*/}
|
||||
{/* ? handleClick()*/}
|
||||
{/* : setSidebarExpanded(true);*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* <svg*/}
|
||||
{/* className="fill-current"*/}
|
||||
{/* width="18"*/}
|
||||
{/* height="19"*/}
|
||||
{/* viewBox="0 0 18 19"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* >*/}
|
||||
{/* <g clipPath="url(#clip0_130_9807)">*/}
|
||||
{/* <path*/}
|
||||
{/* d="M15.7501 0.55835H2.2501C1.29385 0.55835 0.506348 1.34585 0.506348 2.3021V7.53335C0.506348 8.4896 1.29385 9.2771 2.2501 9.2771H15.7501C16.7063 9.2771 17.4938 8.4896 17.4938 7.53335V2.3021C17.4938 1.34585 16.7063 0.55835 15.7501 0.55835ZM16.2563 7.53335C16.2563 7.8146 16.0313 8.0396 15.7501 8.0396H2.2501C1.96885 8.0396 1.74385 7.8146 1.74385 7.53335V2.3021C1.74385 2.02085 1.96885 1.79585 2.2501 1.79585H15.7501C16.0313 1.79585 16.2563 2.02085 16.2563 2.3021V7.53335Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* <path*/}
|
||||
{/* d="M6.13135 10.9646H2.2501C1.29385 10.9646 0.506348 11.7521 0.506348 12.7083V15.8021C0.506348 16.7583 1.29385 17.5458 2.2501 17.5458H6.13135C7.0876 17.5458 7.8751 16.7583 7.8751 15.8021V12.7083C7.90322 11.7521 7.11572 10.9646 6.13135 10.9646ZM6.6376 15.8021C6.6376 16.0833 6.4126 16.3083 6.13135 16.3083H2.2501C1.96885 16.3083 1.74385 16.0833 1.74385 15.8021V12.7083C1.74385 12.4271 1.96885 12.2021 2.2501 12.2021H6.13135C6.4126 12.2021 6.6376 12.4271 6.6376 12.7083V15.8021Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* <path*/}
|
||||
{/* d="M15.75 10.9646H11.8688C10.9125 10.9646 10.125 11.7521 10.125 12.7083V15.8021C10.125 16.7583 10.9125 17.5458 11.8688 17.5458H15.75C16.7063 17.5458 17.4938 16.7583 17.4938 15.8021V12.7083C17.4938 11.7521 16.7063 10.9646 15.75 10.9646ZM16.2562 15.8021C16.2562 16.0833 16.0312 16.3083 15.75 16.3083H11.8688C11.5875 16.3083 11.3625 16.0833 11.3625 15.8021V12.7083C11.3625 12.4271 11.5875 12.2021 11.8688 12.2021H15.75C16.0312 12.2021 16.2562 12.4271 16.2562 12.7083V15.8021Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* </g>*/}
|
||||
{/* <defs>*/}
|
||||
{/* <clipPath id="clip0_130_9807">*/}
|
||||
{/* <rect*/}
|
||||
{/* width="18"*/}
|
||||
{/* height="18"*/}
|
||||
{/* fill="white"*/}
|
||||
{/* transform="translate(0 0.052124)"*/}
|
||||
{/* />*/}
|
||||
{/* </clipPath>*/}
|
||||
{/* </defs>*/}
|
||||
{/* </svg>*/}
|
||||
{/* UI Elements*/}
|
||||
{/* <svg*/}
|
||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/}
|
||||
{/* open && 'rotate-180'*/}
|
||||
{/* }`}*/}
|
||||
{/* width="20"*/}
|
||||
{/* height="20"*/}
|
||||
{/* viewBox="0 0 20 20"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* >*/}
|
||||
{/* <path*/}
|
||||
{/* fillRule="evenodd"*/}
|
||||
{/* clipRule="evenodd"*/}
|
||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* </svg>*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* */}{/*{/* <!-- Dropdown Menu Start --> */}
|
||||
{/* <div*/}
|
||||
{/* className={`translate transform overflow-hidden ${*/}
|
||||
{/* !open && 'hidden'*/}
|
||||
{/* }`}*/}
|
||||
{/* >*/}
|
||||
{/* <ul className="mb-5.5 mt-4 flex flex-col gap-2.5 pl-6">*/}
|
||||
{/* <li>*/}
|
||||
{/* <NavLink*/}
|
||||
{/* to="/ui/alerts"*/}
|
||||
{/* 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')*/}
|
||||
{/* }*/}
|
||||
{/* >*/}
|
||||
{/* Alerts*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* </li>*/}
|
||||
{/* <li>*/}
|
||||
{/* <NavLink*/}
|
||||
{/* to="/ui/buttons"*/}
|
||||
{/* 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')*/}
|
||||
{/* }*/}
|
||||
{/* >*/}
|
||||
{/* Buttons*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* </li>*/}
|
||||
{/* </ul>*/}
|
||||
{/* </div>*/}
|
||||
{/* */}{/*{/* <!-- Dropdown Menu End --> */}
|
||||
{/* </React.Fragment>*/}
|
||||
{/* );*/}
|
||||
{/* }}*/}
|
||||
{/* </SidebarLinkGroup>*/}
|
||||
{/* */}{/*{/* <!-- Menu Item Ui Elements --> */}
|
||||
{/* */ }{/*{/* <!-- Menu Item Ui Elements --> */ }
|
||||
{/* <SidebarLinkGroup*/ }
|
||||
{/* activeCondition={pathname === '/ui' || pathname.includes('ui')}*/ }
|
||||
{/* >*/ }
|
||||
{/* {(handleClick, open) => {*/ }
|
||||
{/* return (*/ }
|
||||
{/* <React.Fragment>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="#"*/ }
|
||||
{/* className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${*/ }
|
||||
{/* (pathname === '/ui' || pathname.includes('ui')) &&*/ }
|
||||
{/* 'bg-graydark dark:bg-meta-4'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* onClick={(e) => {*/ }
|
||||
{/* e.preventDefault();*/ }
|
||||
{/* sidebarExpanded*/ }
|
||||
{/* ? handleClick()*/ }
|
||||
{/* : setSidebarExpanded(true);*/ }
|
||||
{/* }}*/ }
|
||||
{/* >*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className="fill-current"*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="19"*/ }
|
||||
{/* viewBox="0 0 18 19"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <g clipPath="url(#clip0_130_9807)">*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M15.7501 0.55835H2.2501C1.29385 0.55835 0.506348 1.34585 0.506348 2.3021V7.53335C0.506348 8.4896 1.29385 9.2771 2.2501 9.2771H15.7501C16.7063 9.2771 17.4938 8.4896 17.4938 7.53335V2.3021C17.4938 1.34585 16.7063 0.55835 15.7501 0.55835ZM16.2563 7.53335C16.2563 7.8146 16.0313 8.0396 15.7501 8.0396H2.2501C1.96885 8.0396 1.74385 7.8146 1.74385 7.53335V2.3021C1.74385 2.02085 1.96885 1.79585 2.2501 1.79585H15.7501C16.0313 1.79585 16.2563 2.02085 16.2563 2.3021V7.53335Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M6.13135 10.9646H2.2501C1.29385 10.9646 0.506348 11.7521 0.506348 12.7083V15.8021C0.506348 16.7583 1.29385 17.5458 2.2501 17.5458H6.13135C7.0876 17.5458 7.8751 16.7583 7.8751 15.8021V12.7083C7.90322 11.7521 7.11572 10.9646 6.13135 10.9646ZM6.6376 15.8021C6.6376 16.0833 6.4126 16.3083 6.13135 16.3083H2.2501C1.96885 16.3083 1.74385 16.0833 1.74385 15.8021V12.7083C1.74385 12.4271 1.96885 12.2021 2.2501 12.2021H6.13135C6.4126 12.2021 6.6376 12.4271 6.6376 12.7083V15.8021Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M15.75 10.9646H11.8688C10.9125 10.9646 10.125 11.7521 10.125 12.7083V15.8021C10.125 16.7583 10.9125 17.5458 11.8688 17.5458H15.75C16.7063 17.5458 17.4938 16.7583 17.4938 15.8021V12.7083C17.4938 11.7521 16.7063 10.9646 15.75 10.9646ZM16.2562 15.8021C16.2562 16.0833 16.0312 16.3083 15.75 16.3083H11.8688C11.5875 16.3083 11.3625 16.0833 11.3625 15.8021V12.7083C11.3625 12.4271 11.5875 12.2021 11.8688 12.2021H15.75C16.0312 12.2021 16.2562 12.4271 16.2562 12.7083V15.8021Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </g>*/ }
|
||||
{/* <defs>*/ }
|
||||
{/* <clipPath id="clip0_130_9807">*/ }
|
||||
{/* <rect*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="18"*/ }
|
||||
{/* fill="white"*/ }
|
||||
{/* transform="translate(0 0.052124)"*/ }
|
||||
{/* />*/ }
|
||||
{/* </clipPath>*/ }
|
||||
{/* </defs>*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* UI Elements*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/ }
|
||||
{/* open && 'rotate-180'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* width="20"*/ }
|
||||
{/* height="20"*/ }
|
||||
{/* viewBox="0 0 20 20"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <path*/ }
|
||||
{/* fillRule="evenodd"*/ }
|
||||
{/* clipRule="evenodd"*/ }
|
||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu Start --> */ }
|
||||
{/* <div*/ }
|
||||
{/* className={`translate transform overflow-hidden ${*/ }
|
||||
{/* !open && 'hidden'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* >*/ }
|
||||
{/* <ul className="mb-5.5 mt-4 flex flex-col gap-2.5 pl-6">*/ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/ui/alerts"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Alerts*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/ui/buttons"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Buttons*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* </ul>*/ }
|
||||
{/* </div>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu End --> */ }
|
||||
{/* </React.Fragment>*/ }
|
||||
{/* );*/ }
|
||||
{/* }}*/ }
|
||||
{/* </SidebarLinkGroup>*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Ui Elements --> */ }
|
||||
|
||||
{/* */}{/*{/* <!-- Menu Item Auth Pages --> */}
|
||||
{/* <SidebarLinkGroup*/}
|
||||
{/* activeCondition={*/}
|
||||
{/* pathname === '/auth' || pathname.includes('auth')*/}
|
||||
{/* }*/}
|
||||
{/* >*/}
|
||||
{/* {(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 === '/auth' || pathname.includes('auth')) &&*/}
|
||||
{/* 'bg-graydark dark:bg-meta-4'*/}
|
||||
{/* }`}*/}
|
||||
{/* onClick={(e) => {*/}
|
||||
{/* e.preventDefault();*/}
|
||||
{/* sidebarExpanded*/}
|
||||
{/* ? handleClick()*/}
|
||||
{/* : setSidebarExpanded(true);*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* <svg*/}
|
||||
{/* className="fill-current"*/}
|
||||
{/* width="18"*/}
|
||||
{/* height="19"*/}
|
||||
{/* viewBox="0 0 18 19"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* >*/}
|
||||
{/* <g clipPath="url(#clip0_130_9814)">*/}
|
||||
{/* <path*/}
|
||||
{/* d="M12.7127 0.55835H9.53457C8.80332 0.55835 8.18457 1.1771 8.18457 1.90835V3.84897C8.18457 4.18647 8.46582 4.46772 8.80332 4.46772C9.14082 4.46772 9.45019 4.18647 9.45019 3.84897V1.88022C9.45019 1.82397 9.47832 1.79585 9.53457 1.79585H12.7127C13.3877 1.79585 13.9221 2.33022 13.9221 3.00522V15.0709C13.9221 15.7459 13.3877 16.2802 12.7127 16.2802H9.53457C9.47832 16.2802 9.45019 16.2521 9.45019 16.1959V14.2552C9.45019 13.9177 9.16894 13.6365 8.80332 13.6365C8.43769 13.6365 8.18457 13.9177 8.18457 14.2552V16.1959C8.18457 16.9271 8.80332 17.5459 9.53457 17.5459H12.7127C14.0908 17.5459 15.1877 16.4209 15.1877 15.0709V3.03335C15.1877 1.65522 14.0627 0.55835 12.7127 0.55835Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* <path*/}
|
||||
{/* d="M10.4346 8.60205L7.62207 5.7333C7.36895 5.48018 6.97519 5.48018 6.72207 5.7333C6.46895 5.98643 6.46895 6.38018 6.72207 6.6333L8.46582 8.40518H3.45957C3.12207 8.40518 2.84082 8.68643 2.84082 9.02393C2.84082 9.36143 3.12207 9.64268 3.45957 9.64268H8.49395L6.72207 11.4427C6.46895 11.6958 6.46895 12.0896 6.72207 12.3427C6.83457 12.4552 7.00332 12.5114 7.17207 12.5114C7.34082 12.5114 7.50957 12.4552 7.62207 12.3145L10.4346 9.4458C10.6877 9.24893 10.6877 8.85518 10.4346 8.60205Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* </g>*/}
|
||||
{/* <defs>*/}
|
||||
{/* <clipPath id="clip0_130_9814">*/}
|
||||
{/* <rect*/}
|
||||
{/* width="18"*/}
|
||||
{/* height="18"*/}
|
||||
{/* fill="white"*/}
|
||||
{/* transform="translate(0 0.052124)"*/}
|
||||
{/* />*/}
|
||||
{/* </clipPath>*/}
|
||||
{/* </defs>*/}
|
||||
{/* </svg>*/}
|
||||
{/* Authentication*/}
|
||||
{/* <svg*/}
|
||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/}
|
||||
{/* open && 'rotate-180'*/}
|
||||
{/* }`}*/}
|
||||
{/* width="20"*/}
|
||||
{/* height="20"*/}
|
||||
{/* viewBox="0 0 20 20"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* >*/}
|
||||
{/* <path*/}
|
||||
{/* fillRule="evenodd"*/}
|
||||
{/* clipRule="evenodd"*/}
|
||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/}
|
||||
{/* fill=""*/}
|
||||
{/* />*/}
|
||||
{/* </svg>*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* */}{/*{/* <!-- Dropdown Menu Start --> */}
|
||||
{/* <div*/}
|
||||
{/* className={`translate transform overflow-hidden ${*/}
|
||||
{/* !open && 'hidden'*/}
|
||||
{/* }`}*/}
|
||||
{/* >*/}
|
||||
{/* <ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">*/}
|
||||
{/* <li>*/}
|
||||
{/* <NavLink*/}
|
||||
{/* to="/auth/signin"*/}
|
||||
{/* 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')*/}
|
||||
{/* }*/}
|
||||
{/* >*/}
|
||||
{/* Sign In*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* </li>*/}
|
||||
{/* <li>*/}
|
||||
{/* <NavLink*/}
|
||||
{/* to="/auth/signup"*/}
|
||||
{/* 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')*/}
|
||||
{/* }*/}
|
||||
{/* >*/}
|
||||
{/* Sign Up*/}
|
||||
{/* </NavLink>*/}
|
||||
{/* </li>*/}
|
||||
{/* </ul>*/}
|
||||
{/* </div>*/}
|
||||
{/* */}{/*{/* <!-- Dropdown Menu End --> */}
|
||||
{/* </React.Fragment>*/}
|
||||
{/* );*/}
|
||||
{/* }}*/}
|
||||
{/* </SidebarLinkGroup>*/}
|
||||
{/* */}{/*{/* <!-- Menu Item Auth Pages --> */}
|
||||
{/* </ul>*/}
|
||||
{/*</div>}*/}
|
||||
{/* */ }{/*{/* <!-- Menu Item Auth Pages --> */ }
|
||||
{/* <SidebarLinkGroup*/ }
|
||||
{/* activeCondition={*/ }
|
||||
{/* pathname === '/auth' || pathname.includes('auth')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* {(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 === '/auth' || pathname.includes('auth')) &&*/ }
|
||||
{/* 'bg-graydark dark:bg-meta-4'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* onClick={(e) => {*/ }
|
||||
{/* e.preventDefault();*/ }
|
||||
{/* sidebarExpanded*/ }
|
||||
{/* ? handleClick()*/ }
|
||||
{/* : setSidebarExpanded(true);*/ }
|
||||
{/* }}*/ }
|
||||
{/* >*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className="fill-current"*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="19"*/ }
|
||||
{/* viewBox="0 0 18 19"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <g clipPath="url(#clip0_130_9814)">*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M12.7127 0.55835H9.53457C8.80332 0.55835 8.18457 1.1771 8.18457 1.90835V3.84897C8.18457 4.18647 8.46582 4.46772 8.80332 4.46772C9.14082 4.46772 9.45019 4.18647 9.45019 3.84897V1.88022C9.45019 1.82397 9.47832 1.79585 9.53457 1.79585H12.7127C13.3877 1.79585 13.9221 2.33022 13.9221 3.00522V15.0709C13.9221 15.7459 13.3877 16.2802 12.7127 16.2802H9.53457C9.47832 16.2802 9.45019 16.2521 9.45019 16.1959V14.2552C9.45019 13.9177 9.16894 13.6365 8.80332 13.6365C8.43769 13.6365 8.18457 13.9177 8.18457 14.2552V16.1959C8.18457 16.9271 8.80332 17.5459 9.53457 17.5459H12.7127C14.0908 17.5459 15.1877 16.4209 15.1877 15.0709V3.03335C15.1877 1.65522 14.0627 0.55835 12.7127 0.55835Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M10.4346 8.60205L7.62207 5.7333C7.36895 5.48018 6.97519 5.48018 6.72207 5.7333C6.46895 5.98643 6.46895 6.38018 6.72207 6.6333L8.46582 8.40518H3.45957C3.12207 8.40518 2.84082 8.68643 2.84082 9.02393C2.84082 9.36143 3.12207 9.64268 3.45957 9.64268H8.49395L6.72207 11.4427C6.46895 11.6958 6.46895 12.0896 6.72207 12.3427C6.83457 12.4552 7.00332 12.5114 7.17207 12.5114C7.34082 12.5114 7.50957 12.4552 7.62207 12.3145L10.4346 9.4458C10.6877 9.24893 10.6877 8.85518 10.4346 8.60205Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </g>*/ }
|
||||
{/* <defs>*/ }
|
||||
{/* <clipPath id="clip0_130_9814">*/ }
|
||||
{/* <rect*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="18"*/ }
|
||||
{/* fill="white"*/ }
|
||||
{/* transform="translate(0 0.052124)"*/ }
|
||||
{/* />*/ }
|
||||
{/* </clipPath>*/ }
|
||||
{/* </defs>*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* Authentication*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/ }
|
||||
{/* open && 'rotate-180'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* width="20"*/ }
|
||||
{/* height="20"*/ }
|
||||
{/* viewBox="0 0 20 20"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <path*/ }
|
||||
{/* fillRule="evenodd"*/ }
|
||||
{/* clipRule="evenodd"*/ }
|
||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu Start --> */ }
|
||||
{/* <div*/ }
|
||||
{/* className={`translate transform overflow-hidden ${*/ }
|
||||
{/* !open && 'hidden'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* >*/ }
|
||||
{/* <ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">*/ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/auth/signin"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Sign In*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/auth/signup"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Sign Up*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* </ul>*/ }
|
||||
{/* </div>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu End --> */ }
|
||||
{/* </React.Fragment>*/ }
|
||||
{/* );*/ }
|
||||
{/* }}*/ }
|
||||
{/* </SidebarLinkGroup>*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Auth Pages --> */ }
|
||||
{/* </ul>*/ }
|
||||
{/*</div>}*/ }
|
||||
</nav>
|
||||
{/* <!-- Sidebar Menu --> */}
|
||||
{/* <!-- Sidebar Menu --> */ }
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
@ -1,18 +1,21 @@
|
||||
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 [open, setOpen] = useState<boolean>(activeCondition);
|
||||
const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) =>
|
||||
{
|
||||
const [ open, setOpen ] = useState<boolean>(activeCondition);
|
||||
|
||||
const handleClick = () => {
|
||||
const handleClick = () =>
|
||||
{
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return <li>{children(handleClick, open)}</li>;
|
||||
return <li>{ children(handleClick, open) }</li>;
|
||||
};
|
||||
|
||||
export default SidebarLinkGroup;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface CardDataStatsProps {
|
||||
interface CardDataStatsProps
|
||||
{
|
||||
title: string;
|
||||
total: string;
|
||||
rate?: string;
|
||||
@ -13,27 +14,28 @@ 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">
|
||||
|
||||
<div className="mt-4 flex items-end justify-between">
|
||||
<div>
|
||||
<h4 className="text-title-md font-bold text-black dark:text-white">
|
||||
{total}
|
||||
{ total }
|
||||
</h4>
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
<span className="text-sm font-medium">{ title }</span>
|
||||
</div>
|
||||
|
||||
{rate && <span
|
||||
className={`flex items-center gap-1 text-sm font-medium ${
|
||||
levelUp && 'text-meta-3'
|
||||
} ${levelDown && 'text-meta-5'} `}
|
||||
{ rate && <span
|
||||
className={ `flex items-center gap-1 text-sm font-medium ${
|
||||
levelUp && "text-meta-3"
|
||||
} ${ levelDown && "text-meta-5" } ` }
|
||||
>
|
||||
{rate}
|
||||
{ rate }
|
||||
|
||||
{levelUp && (
|
||||
{ levelUp && (
|
||||
<svg
|
||||
className="fill-meta-3"
|
||||
width="10"
|
||||
@ -47,8 +49,8 @@ export const CardDataStats: React.FC<CardDataStatsProps> = ({
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{levelDown && (
|
||||
) }
|
||||
{ levelDown && (
|
||||
<svg
|
||||
className="fill-meta-5"
|
||||
width="10"
|
||||
@ -62,8 +64,8 @@ export const CardDataStats: React.FC<CardDataStatsProps> = ({
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</span>}
|
||||
) }
|
||||
</span> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,29 +1,30 @@
|
||||
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 */}
|
||||
<title>{title}</title>
|
||||
<link rel="canonical" href={window.location.href} />
|
||||
<meta name="description" content={description} />
|
||||
{ /* Open Graph tags (OG) */}
|
||||
<meta property="og:url" content={window.location.href} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
{/* OG image tags */}
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:image:secure_url" content={image} />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:width" content="200" />
|
||||
<meta property="og:image:alt" content={`Image of ${title} site`} />
|
||||
{ /* Twitter tags */}
|
||||
{name && <meta name="twitter:creator" content={name} />}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
{ /* Standard metadata tags */ }
|
||||
<title>{ title }</title>
|
||||
<link rel="canonical" href={ window.location.href }/>
|
||||
<meta name="description" content={ description }/>
|
||||
{ /* Open Graph tags (OG) */ }
|
||||
<meta property="og:url" content={ window.location.href }/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:title" content={ title }/>
|
||||
<meta property="og:description" content={ description }/>
|
||||
{/* OG image tags */ }
|
||||
<meta property="og:image" content={ image }/>
|
||||
<meta property="og:image:secure_url" content={ image }/>
|
||||
<meta property="og:image:type" content="image/jpeg"/>
|
||||
<meta property="og:image:width" content="200"/>
|
||||
<meta property="og:image:alt" content={ `Image of ${ title } site` }/>
|
||||
{ /* Twitter tags */ }
|
||||
{ name && <meta name="twitter:creator" content={ name }/> }
|
||||
<meta name="twitter:card" content="summary"/>
|
||||
<meta name="twitter:title" content={ title }/>
|
||||
<meta name="twitter:description" content={ description }/>
|
||||
</Helmet>
|
||||
);
|
||||
};
|
@ -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">
|
||||
@ -12,9 +13,9 @@ export const Search = ({ searchItem, handleInputChange }: {
|
||||
<input
|
||||
className="w-full rounded border border-stroke bg-gray py-3 pl-5 pr-5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary"
|
||||
type="text"
|
||||
onChange={handleInputChange}
|
||||
value={searchItem}
|
||||
placeholder={t('logs.search')}
|
||||
onChange={ handleInputChange }
|
||||
value={ searchItem }
|
||||
placeholder={ t("logs.search") }
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -1,70 +1,73 @@
|
||||
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 === 0 && <ContentLoader />}
|
||||
{error === 1 && <div
|
||||
{ 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">
|
||||
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
|
||||
{stations.map((station, key) => (
|
||||
{ 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'
|
||||
}`}
|
||||
key={station.id}
|
||||
className={ `grid grid-cols-2 sm:grid-cols-3 ${ stations.length === (key + 1) // todo: ...
|
||||
? ""
|
||||
: "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>
|
||||
))}
|
||||
)) }
|
||||
</div>
|
||||
</div>}
|
||||
</div> }
|
||||
</>
|
||||
|
||||
);
|
||||
|
@ -1,88 +1,91 @@
|
||||
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 === 0 && <ContentLoader />}
|
||||
{error === 1 && <div
|
||||
{ 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">
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
|
||||
{trains.map((train, key) => (
|
||||
{ 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'
|
||||
}`}
|
||||
key={train.id}
|
||||
className={ `grid grid-cols-4 sm:grid-cols-5 ${ trains.length === (key + 1)
|
||||
? ""
|
||||
: "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>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6">{train.trainPoints}</p>
|
||||
<p className="text-meta-6">{ train.trainPoints }</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-5">{(train.trainDistance / 1000).toFixed(2)}km</p>
|
||||
<p className="text-meta-5">{ (train.trainDistance / 1000).toFixed(2) }km</p>
|
||||
</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>
|
||||
))}
|
||||
)) }
|
||||
</div>
|
||||
</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
|
||||
@ -24,12 +29,12 @@ export const StationLog = ({ data }: { data: TLogStationData }) => {
|
||||
<div
|
||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
||||
<div className="relative drop-shadow-2">
|
||||
<img className="rounded-full" src={data.userAvatar} alt="profile" />
|
||||
<img className="rounded-full" src={ data.userAvatar } alt="profile"/>
|
||||
</div>
|
||||
</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,31 +42,38 @@ 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>
|
||||
{data.joinedDate &&
|
||||
<p>{t('log.station.spent', { date: dayjs.duration(data.leftDate - data.joinedDate).format('H[h] m[m]') })}</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>
|
||||
{ data.joinedDate &&
|
||||
<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}
|
||||
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
|
||||
onClick={copyLink}
|
||||
onClick={ copyLink }
|
||||
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
|
||||
@ -24,12 +30,12 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) => {
|
||||
<div
|
||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
||||
<div className="relative drop-shadow-2">
|
||||
<img className="rounded-full" src={data.userAvatar} alt="profile" />
|
||||
<img className="rounded-full" src={ data.userAvatar } alt="profile"/>
|
||||
</div>
|
||||
</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,35 +43,42 @@ 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>
|
||||
{(data.distance || data.distance === 0) &&
|
||||
<p>{t('log.train.distance', { distance: (data.distance / 1000).toFixed(2) })}</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> }
|
||||
|
||||
{(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>
|
||||
{data.joinedDate &&
|
||||
<p>{t('log.train.spent', { date: dayjs.duration(data.leftDate - data.joinedDate).format('H[h] m[m]') })}</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>
|
||||
{ data.joinedDate &&
|
||||
<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}
|
||||
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
|
||||
onClick={copyLink}
|
||||
onClick={ copyLink }
|
||||
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,91 +1,93 @@
|
||||
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 === 0 && <ContentLoader />}
|
||||
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||
description={ t("content_loader.notfound.description") }/> }
|
||||
{ error === 0 && <ContentLoader/> }
|
||||
|
||||
{error === 1 && <div
|
||||
{ error === 1 && <div
|
||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
|
||||
{stations.map((station, key) => (
|
||||
{ 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'
|
||||
}`}
|
||||
key={station.id}
|
||||
className={ `grid grid-cols-3 sm:grid-cols-4 ${ stations.length === (key + 1)
|
||||
? ""
|
||||
: "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>
|
||||
))}
|
||||
)) }
|
||||
</div>
|
||||
</div>}
|
||||
</div> }
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,108 +1,109 @@
|
||||
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 === 0 && <ContentLoader />}
|
||||
{error === 1 && <div
|
||||
{ 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">
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
|
||||
{trains.map((train, key) => (
|
||||
{ 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'
|
||||
}`}
|
||||
key={train.id}
|
||||
className={ `grid grid-cols-3 sm:grid-cols-6 ${ trains.length === (key + 1)
|
||||
? ""
|
||||
: "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>
|
||||
))}
|
||||
)) }
|
||||
</div>
|
||||
</div>}
|
||||
</div> }
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,12 +1,15 @@
|
||||
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);
|
||||
const [ showTrains, setShowTrains ] = useState(false);
|
||||
const [ showStations, setShowStations ] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
return <div
|
||||
@ -15,12 +18,12 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
||||
<div
|
||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
||||
<div className="relative drop-shadow-2">
|
||||
<img className="rounded-full" src={data.steam.avatarfull} alt="profile" />
|
||||
<img className="rounded-full" src={ data.steam.avatarfull } alt="profile"/>
|
||||
</div>
|
||||
</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
|
||||
@ -28,124 +31,126 @@ export const ProfileCard = ({ data }: { data: TProfileData }) => {
|
||||
<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.trainDistance / 1000)}km
|
||||
{ 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>
|
||||
</div>
|
||||
{Object.keys(data.player.trainStats || {}).length > 0 &&
|
||||
{ Object.keys(data.player.trainStats || {}).length > 0 &&
|
||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||
<div className="group relative cursor-pointer" onClick={() => setShowTrains(val => !val)}>
|
||||
<h1 className="text-xl text-black dark:text-white pb-5">{t('profile.trains.header')}</h1>
|
||||
<ArrowIcon rotated={showTrains} />
|
||||
<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>
|
||||
<ArrowIcon rotated={ showTrains }/>
|
||||
</div>
|
||||
|
||||
{showTrains &&
|
||||
{ showTrains &&
|
||||
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
||||
<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 => {
|
||||
const train = data.player.trainStats[trainName];
|
||||
{ Object.keys(data.player.trainStats).map(trainName =>
|
||||
{
|
||||
const train = data.player.trainStats[ trainName ];
|
||||
|
||||
return <div
|
||||
className={`grid grid-cols-3 sm:grid-cols-4 border-t border-t-stroke dark:border-t-strokedark`}
|
||||
key={1}
|
||||
className={ `grid grid-cols-3 sm:grid-cols-4 border-t border-t-stroke dark:border-t-strokedark` }
|
||||
key={ 1 }
|
||||
>
|
||||
<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">
|
||||
{trainName}
|
||||
{ trainName }
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6 sm:block break-all">{Math.floor(train.distance / 1000)}km</p>
|
||||
<p className="text-meta-6 sm:block break-all">{ Math.floor(train.distance / 1000) }km</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-3">{train.score}</p>
|
||||
<p className="text-meta-3">{ train.score }</p>
|
||||
</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>;
|
||||
})}
|
||||
}) }
|
||||
|
||||
|
||||
</div>}
|
||||
</div> }
|
||||
|
||||
</div>}
|
||||
{Object.keys(data.player.dispatcherStats || {}).length > 0 &&
|
||||
</div> }
|
||||
{ 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>
|
||||
<ArrowIcon rotated={showTrains} />
|
||||
<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>
|
||||
<ArrowIcon rotated={ showTrains }/>
|
||||
</div>
|
||||
{showStations &&
|
||||
{ showStations &&
|
||||
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
||||
<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 => {
|
||||
const station = data.player.dispatcherStats[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`}
|
||||
key={1}
|
||||
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
|
||||
key={ 1 }
|
||||
>
|
||||
<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">
|
||||
{stationName}
|
||||
{ stationName }
|
||||
</p>
|
||||
</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>;
|
||||
})}
|
||||
}) }
|
||||
|
||||
</div>}
|
||||
</div> }
|
||||
|
||||
</div>}
|
||||
</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,19 +1,20 @@
|
||||
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 }) => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
export const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
{
|
||||
const [ sidebarOpen, setSidebarOpen ] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="dark:bg-boxdark-2 dark:text-bodydark">
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
||||
<Sidebar sidebarOpen={ sidebarOpen } setSidebarOpen={ setSidebarOpen }/>
|
||||
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
|
||||
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
||||
<Header sidebarOpen={ sidebarOpen } setSidebarOpen={ setSidebarOpen }/>
|
||||
<main>
|
||||
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
||||
{children}
|
||||
{ children }
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
@ -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 [trains, setTrains] = useState(0);
|
||||
const [dispatchers, setDispatchers] = useState(0);
|
||||
const [profiles, setProfiles] = useState(0);
|
||||
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,30 +76,31 @@ export const Home: React.FC = () => {
|
||||
|
||||
<div className="mt-6.5">
|
||||
<p><Trans
|
||||
i18nKey={t('home.footer.author')}
|
||||
values={{ author: 'alekswilc' }}
|
||||
components={{
|
||||
anchor: <Link className="color-orchid" to={'https://www.alekswilc.dev'} />
|
||||
}}
|
||||
i18nKey={ t("home.footer.author") }
|
||||
values={ { author: "alekswilc" } }
|
||||
components={ {
|
||||
anchor: <Link className="color-orchid" to={ "https://www.alekswilc.dev" }/>,
|
||||
} }
|
||||
/></p>
|
||||
<p><Trans
|
||||
i18nKey={t('home.footer.thanks')}
|
||||
components={{
|
||||
bahu: <Link className="color-orchid" to={'https://bahu.pro/'} />,
|
||||
i18nKey={ t("home.footer.thanks") }
|
||||
components={ {
|
||||
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 &&
|
||||
<p>{ version && <Link className="color-orchid"
|
||||
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>
|
||||
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/commit/${ commit }` }>{ commit }</Link> }</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -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!);
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -38,30 +42,30 @@ export const Log = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* LOADING */}
|
||||
{error === 0 && <ContentLoader />}
|
||||
{/* 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')} />}
|
||||
{/* 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')} />}
|
||||
{/* SUCCESS */}
|
||||
{error === 1 && stationData && <PageMeta
|
||||
title={`simrail.alekswilc.dev | ${stationData.userUsername}`}
|
||||
image={stationData.userAvatar}
|
||||
description={`${stationData.stationName} - ${stationData.stationShort}`} />}
|
||||
{error === 1 && stationData && < StationLog data={stationData} />}
|
||||
{/* LOADING */ }
|
||||
{ error === 0 && <ContentLoader/> }
|
||||
{/* 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") }/> }
|
||||
{/* 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") }/> }
|
||||
{/* SUCCESS */ }
|
||||
{ error === 1 && stationData && <PageMeta
|
||||
title={ `simrail.alekswilc.dev | ${ stationData.userUsername }` }
|
||||
image={ stationData.userAvatar }
|
||||
description={ `${ stationData.stationName } - ${ stationData.stationShort }` }/> }
|
||||
{ error === 1 && stationData && < StationLog data={ stationData }/> }
|
||||
|
||||
{error === 1 && trainData && <PageMeta
|
||||
title={`simrail.alekswilc.dev | ${trainData.userUsername}`}
|
||||
image={trainData.userAvatar}
|
||||
description={`${trainData.trainName} - ${trainData.trainNumber}`} />}
|
||||
{error === 1 && trainData && < TrainLog data={trainData} />}
|
||||
{ error === 1 && trainData && <PageMeta
|
||||
title={ `simrail.alekswilc.dev | ${ trainData.userUsername }` }
|
||||
image={ trainData.userAvatar }
|
||||
description={ `${ trainData.trainName } - ${ trainData.trainNumber }` }/> }
|
||||
{ error === 1 && trainData && < TrainLog data={ trainData }/> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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!);
|
||||
|
||||
|
||||
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;
|
||||
@ -35,24 +38,24 @@ export const Profile = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* LOADING */}
|
||||
{error === 0 && <ContentLoader />}
|
||||
{/* 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')} />}
|
||||
{/* 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')} />}
|
||||
{/* 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`} />}
|
||||
{error === 1 && <ProfileCard data={data} />}
|
||||
{/* LOADING */ }
|
||||
{ error === 0 && <ContentLoader/> }
|
||||
{/* 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") }/> }
|
||||
{/* 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") }/> }
|
||||
{/* 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)) } 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