v3 release #75

Merged
alekswilc merged 63 commits from v3 into main 2024-12-13 20:29:17 +01:00
62 changed files with 462 additions and 228 deletions
Showing only changes of commit 3a21491e8b - Show all commits

View File

@ -11,6 +11,7 @@
"author": "Aleksander <alekswilc> Wilczyński",
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@types/uuid": "^10.0.0",
@ -21,6 +22,7 @@
"dependencies": {
"@simrail/types": "^0.0.4",
"@types/mongoose": "^5.11.97",
"cors": "^2.8.5",
"dayjs": "^1.11.12",
"ejs": "^3.1.10",
"express": "^4.19.2",

View File

@ -2,10 +2,12 @@ import { Router } from 'express';
import dayjs from 'dayjs';
import { msToTime } from '../../util/time.js';
import { PipelineStage } from 'mongoose';
import { MTrainLog, raw_schema } from '../../mongo/trainLogs.js';
import { ITrainLog, MTrainLog, raw_schema } from '../../mongo/trainLogs.js';
import { MBlacklist } from '../../mongo/blacklist.js';
import { SteamUtil } from '../../util/SteamUtil.js';
import { GitUtil } from '../../util/git.js';
import { SuccessResponseBuilder } from '../responseBuilder.js';
import { removeProperties } from '../../util/functions.js';
const generateSearch = (regex: RegExp) => [
{
@ -43,13 +45,22 @@ export class TrainsRoute {
const records = await MTrainLog.aggregate(filter)
.sort({ leftDate: -1 })
.limit(30)
res.render('trains/index.ejs', {
records,
dayjs,
q: req.query.q,
msToTime,
...GitUtil.getData()
});
res.json(
new SuccessResponseBuilder<{ records: Omit<ITrainLog, '_id' | '__v'>[] }>()
.setCode(200)
.setData({ records: records.map(x => removeProperties<Omit<ITrainLog, '_id' | '__v'>>(x, ['_id', '__v'])) })
.toJSON()
);
// res.render('trains/index.ejs', {
// records,
// dayjs,
// q: req.query.q,
// msToTime,
// ...GitUtil.getData()
// });
})
app.get('/details/:id', async (req, res) => {

View File

@ -7,7 +7,7 @@ import { ProfilesRoute } from './routes/profile.js';
import { LeaderboardRoute } from './routes/leaderboard.js';
import { MProfile } from '../mongo/profile.js';
import { GitUtil } from '../util/git.js';
import cors from 'cors';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -19,7 +19,7 @@ export class ApiModule {
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views')
app.get('/', (_, res) => res.render('home', GitUtil.getData()));
app.use(cors());
// backward compatible
app.get('/details/:id', (req, res) => res.redirect('/stations/details/'+req.params.id));

View File

@ -24,7 +24,8 @@
"react-icons": "^4.10.1",
"react-router-dom": "^6.14.2",
"react-toastify": "^9.1.3",
"sort-by": "^0.0.2"
"sort-by": "^0.0.2",
"use-debounce": "^10.0.4"
},
"devDependencies": {
"@types/react": "^18.2.17",

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,19 @@
import { useEffect, useState } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { Loader } from './common/Loader';
import { PageTitle } from './common/PageTitle';
import Chart from './pages/Chart';
import { Loader } from './components/loaders/PageLoader.tsx';
import { PageTitle } from './components/common/PageTitle.tsx';
import Chart from './old/Chart.tsx';
import { Home } from './pages/Home';
import Profile from './pages/Profile';
import Settings from './pages/Settings';
import Tables from './pages/Tables';
import Alerts from './pages/UiElements/Alerts';
import Buttons from './pages/UiElements/Buttons';
import Settings from './old/Settings.tsx';
import Tables from './old/Tables.tsx';
import Alerts from './old/UiElements/Alerts.tsx';
import Buttons from './old/UiElements/Buttons.tsx';
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';
function App() {
@ -25,7 +25,7 @@ function App() {
}, [pathname]);
useEffect(() => {
setTimeout(() => setLoading(false), 1000);
setTimeout(() => setLoading(false), 400);
}, []);
return loading ? (
@ -51,6 +51,17 @@ function App() {
</>
}
/>
<Route
path="/logs/trains"
element={
<>
<PageTitle title="simrail.alekswilc.dev | Train logs" />
<TrainLogs />
</>
}
/>
<Route
path="/leaderboard/stations"
element={

View File

@ -1,6 +1,4 @@
import { Link } from 'react-router-dom';
import LogoIcon from '../../images/logo/logo-icon.svg';
import DarkModeSwitcher from './DarkModeSwitcher';
import DarkModeSwitcher from './DarkModeSwitcher.tsx';
const Header = (props: {
sidebarOpen: string | boolean | undefined;

View File

@ -1,7 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import SidebarLinkGroup from './SidebarLinkGroup';
import Logo from '../../images/logo/logo.svg';
import SidebarLinkGroup from './SidebarLinkGroup.tsx';
import { useTranslation } from 'react-i18next';
interface SidebarProps {

View File

@ -0,0 +1,24 @@
import { useTranslation } from 'react-i18next';
export const NoRecordsFound = () => {
const { t } = useTranslation();
return <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'>
<svg width='19' height='16' viewBox='0 0 19 16' fill='none' xmlns='<http://www.w3.org/2000/svg>'>
<path
d='M1.50493 16H17.5023C18.6204 16 19.3413 14.9018 18.8354 13.9735L10.8367 0.770573C10.2852 -0.256858 8.70677 -0.256858 8.15528 0.770573L0.156617 13.9735C-0.334072 14.8998 0.386764 16 1.50493 16ZM10.7585 12.9298C10.7585 13.6155 10.2223 14.1433 9.45583 14.1433C8.6894 14.1433 8.15311 13.6155 8.15311 12.9298V12.9015C8.15311 12.2159 8.6894 11.688 9.45583 11.688C10.2223 11.688 10.7585 12.2159 10.7585 12.9015V12.9298ZM8.75236 4.01062H10.2548C10.6674 4.01062 10.9127 4.33826 10.8671 4.75288L10.2071 10.1186C10.1615 10.5049 9.88572 10.7455 9.50142 10.7455C9.11929 10.7455 8.84138 10.5028 8.79579 10.1186L8.13574 4.75288C8.09449 4.33826 8.33984 4.01062 8.75236 4.01062Z'
fill='#FBBF24'></path>
</svg>
</div>
<div className='w-full'>
<h5 className='mb-3 text-lg font-semibold text-[#9D5425]'>
Nie znaleziono danych
</h5>
<p className='leading-relaxed text-[#D0915C]'>
Twoje wyszukiwanie nie zwróciło wyników.
</p>
</div>
</div>
}

View File

@ -0,0 +1,83 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
import { ContentLoader } from '../loaders/ContentLoader.tsx';
import { ChangeEventHandler } from 'react';
import { NoRecordsFound } from '../common/NoRecordsFound.tsx';
export const StationTable = ({ stations, handleInputChange, searchItem, empty }: { stations: TLeaderboardRecord[], searchItem: string, handleInputChange: ChangeEventHandler, empty: boolean }) => {
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">
<div className="flex justify-center items-center">
<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='Type to search'
/>
</div>
</div>
{empty ? <NoRecordsFound /> : stations.length ? <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")}
</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")}
</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")}
</h5>
</div>
</div>
{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}
>
<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>
</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>
</div>
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
<Link
to={"/profile/" + station.steam}
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("leaderboard.profile")}
</Link>
</div>
</div>
))}
</div>
</div> : <ContentLoader />}
</>
);
}

View File

@ -0,0 +1,100 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
import { ContentLoader } from '../loaders/ContentLoader.tsx';
import { ChangeEventHandler } from 'react';
import { NoRecordsFound } from '../common/NoRecordsFound.tsx';
export const TrainTable = ({ trains, handleInputChange, searchItem, empty }: { trains: TLeaderboardRecord[], searchItem: string, handleInputChange: ChangeEventHandler, empty: boolean }) => {
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">
<div className="flex justify-center items-center">
<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='Type to search'
/>
</div>
</div>
{empty ? <NoRecordsFound /> : trains.length ? <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")}
</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")}
</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")}
</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")}
</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")}
</h5>
</div>
</div>
{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}
>
<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>
</p>
</div>
<div className="flex items-center justify-center p-2.5 lg:p-5">
<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>
</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>
</div>
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
<Link
to={"/profile/" + train.steam}
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("leaderboard.profile")}
</Link>
</div>
</div>
))}
</div> </div> : <ContentLoader />}
</>
);
}

View File

@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
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]'>
<svg width='13' height='13' viewBox='0 0 13 13' fill='none' xmlns='<http://www.w3.org/2000/svg>'>
<path
d='M6.4917 7.65579L11.106 12.2645C11.2545 12.4128 11.4715 12.5 11.6738 12.5C11.8762 12.5 12.0931 12.4128 12.2416 12.2645C12.5621 11.9445 12.5623 11.4317 12.2423 11.1114C12.2422 11.1113 12.2422 11.1113 12.2422 11.1113C12.242 11.1111 12.2418 11.1109 12.2416 11.1107L7.64539 6.50351L12.2589 1.91221L12.2595 1.91158C12.5802 1.59132 12.5802 1.07805 12.2595 0.757793C11.9393 0.437994 11.4268 0.437869 11.1064 0.757418C11.1063 0.757543 11.1062 0.757668 11.106 0.757793L6.49234 5.34931L1.89459 0.740581L1.89396 0.739942C1.57364 0.420019 1.0608 0.420019 0.740487 0.739944C0.42005 1.05999 0.419837 1.57279 0.73985 1.89309L6.4917 7.65579ZM6.4917 7.65579L1.89459 12.2639L1.89395 12.2645C1.74546 12.4128 1.52854 12.5 1.32616 12.5C1.12377 12.5 0.906853 12.4128 0.758361 12.2645L1.1117 11.9108L0.758358 12.2645C0.437984 11.9445 0.437708 11.4319 0.757539 11.1116C0.757812 11.1113 0.758086 11.111 0.75836 11.1107L5.33864 6.50287L0.740487 1.89373L6.4917 7.65579Z'
fill='#ffffff' stroke='#ffffff'></path>
</svg>
</div>
<div className='w-full'>
<h5 className='mb-3 font-semibold text-[#B45454]'>
{t("contentloader.error.header")}
</h5>
<ul>
<li className='leading-relaxed text-[#CD5D5D]'>
{t("contentloader.error.description")}
</li>
<li className='leading-relaxed text-[#CD5D5D]'>
<div className="pt-4">
{/* TODO: add params */}
<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("contentloader.error.report")}</Link>
</div>
</li>
</ul>
</div>
</div>;
}
export const ContentLoader = () => {
const [error, setError] = useState(false);
useEffect(() => {
new Promise(res => setTimeout(res, 5000)).then(() => {
setError(true);
});
}, []);
return (
<>
{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>}
</>
);
};

View File

@ -6,33 +6,24 @@ function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: SetValue<T>) => void] {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// useEffect to update local storage when the state changes
useEffect(() => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
typeof storedValue === 'function'
? storedValue(storedValue)
: storedValue;
// Save state
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
}, [key, storedValue]);

View File

@ -1,8 +1,8 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import translationsInEng from "../languages/en.json";
import translationsInPl from "../languages/pl.json";
import translationsInEng from "./languages/en.json";
import translationsInPl from "./languages/pl.json";
const resources = {
en: {

View File

@ -35,7 +35,24 @@
"time": "Czas",
"distance": "Dystans",
"points": "Punkty",
"profile": "Otwórz profil",
"profile": "Profil",
"actions": "Akcje"
},
"logs": {
"user": "Użytkownik",
"time": "Czas",
"distance": "Dystans",
"points": "Punkty",
"profile": "Profil",
"record": "Więcej",
"train": "Pociąg",
"actions": "Akcje"
},
"contentloader": {
"error": {
"header": "Błąd ładowania strony",
"description": "Sprawdź swoje połączenie z internetem i odśwież strone",
"report": "Zgłoś błąd"
}
}
}

View File

@ -1,35 +1,23 @@
import React, { useState, ReactNode } from 'react';
import Header from '../components/Header/index';
import Sidebar from '../components/Sidebar/index';
import Header from '../components/header/index';
import Sidebar from '../components/sidebar/index';
const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="dark:bg-boxdark-2 dark:text-bodydark">
{/* <!-- ===== Page Wrapper Start ===== --> */}
<div className="flex h-screen overflow-hidden">
{/* <!-- ===== Sidebar Start ===== --> */}
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
{/* <!-- ===== Sidebar End ===== --> */}
{/* <!-- ===== Content Area Start ===== --> */}
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
{/* <!-- ===== Header Start ===== --> */}
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
{/* <!-- ===== Header End ===== --> */}
{/* <!-- ===== Main Content Start ===== --> */}
<main>
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
{children}
</div>
</main>
{/* <!-- ===== Main Content End ===== --> */}
</div>
{/* <!-- ===== Content Area End ===== --> */}
</div>
{/* <!-- ===== Page Wrapper End ===== --> */}
</div>
);
};

View File

@ -1,8 +1,8 @@
import React from 'react';
import ChartOne from '../components/Charts/ChartOne';
import ChartThree from '../components/Charts/ChartThree';
import ChartTwo from '../components/Charts/ChartTwo';
import ChartOne from './Charts/ChartOne';
import ChartThree from './Charts/ChartThree';
import ChartTwo from './Charts/ChartTwo';
const Chart: React.FC = () => {
return (

View File

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom';
import { Chat } from '../../types/chat';
import { Chat } from '../../old/chat';
import UserOne from '../../images/user/user-01.png';
import UserTwo from '../../images/user/user-02.png';
import UserThree from '../../images/user/user-03.png';

View File

@ -1,4 +1,4 @@
import { Package } from '../../types/package';
import { Package } from '../package';
const packageData: Package[] = [
{

View File

@ -1,4 +1,4 @@
import { Product } from '../../types/product';
import { Product } from '../product';
import ProductOne from '../../images/product/product-01.png';
import ProductTwo from '../../images/product/product-02.png';
import ProductThree from '../../images/product/product-03.png';

View File

@ -0,0 +1 @@
this folder contains old unused files. remove before release!

View File

@ -1,5 +1,5 @@
import React from 'react';
import CardDataStats from '../components/CardDataStats';
import CardDataStats from '../old/CardDataStats';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';

View File

@ -1,78 +1,41 @@
import { Link } from 'react-router-dom';
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
import { useTranslation } from 'react-i18next';
const trains: TLeaderboardRecord[] = { "success": true, "data": { "records": [{ "id": "cad73c6c-53e6-43f7-bcbf-82b7cbe0f50b", "steam": "76561198278896700", "steamName": "xxx", "trainTime": 1428812, "trainPoints": 1662, "trainDistance": 20723, "dispatcherTime": 4841181851, "dispatcherStats": { "Dąbrowa Górnicza Ząbkowice": { "time": 4841181851 } }, "trainStats": { "EN71": { "distance": 18416, "score": 0, "time": 1180127 }, "EN96": { "distance": 2307, "score": 1662, "time": 248685 } } }, { "id": "c2310930-7a24-4984-bcd8-bf8ceeb52e3a", "steam": "76561199012181691", "steamName": "KifoHa WIN.SKINS", "trainTime": 0, "trainPoints": 0, "trainDistance": 0, "dispatcherTime": 4838576868, "dispatcherStats": { "Katowice": { "time": 4838576868 } } }, { "id": "54d71c47-0bb5-4436-a6f5-af74b578c1c8", "steam": "76561197990737195", "steamName": "sebger", "trainTime": 0, "trainPoints": 0, "trainDistance": 0, "dispatcherTime": 4414893359, "dispatcherStats": { "Łazy Ła": { "time": 4414893359 } } }, { "id": "b8ecaf9f-7401-4089-915e-6ce297e51be3", "steam": "76561198799185367", "steamName": "Tutakus", "trainTime": 2376567, "dispatcherTime": 4413227316, "trainStats": { "Traxx (E186)": { "distance": 40000, "score": 0, "time": 2376567 } }, "trainDistance": 40000, "trainPoints": 0, "dispatcherStats": { "Włoszczowa Północ": { "time": 4413227316 } } }, { "id": "b443ba00-ece0-4255-8d27-a863b92e1812", "steam": "76561198264136517", "steamName": "DrWorter", "trainTime": 0, "trainPoints": 0, "trainDistance": 0, "dispatcherTime": 4078328473, "dispatcherStats": { "Strzałki": { "time": 4078328473 } } }, { "id": "add8b491-27c9-4e61-a246-d17896493dc8", "steam": "76561198362204493", "steamName": "Kamil6065", "trainTime": 0, "trainPoints": 0, "trainDistance": 0, "dispatcherTime": 2227701379, "dispatcherStats": { "Biała Rawska": { "time": 2227701379 } } }, { "id": "94bd56da-7ceb-4bf1-b1bc-0d3736c63d73", "steam": "76561197968142289", "steamName": "Hubson", "trainTime": 1524538, "trainPoints": 872, "trainDistance": 29370, "dispatcherTime": 1852886798, "dispatcherStats": { "Dąbrowa Górnicza Wschodnia": { "time": 241818 }, "Sosnowiec Główny": { "time": 1852644980 } }, "trainStats": { "Traxx (E186)": { "distance": 29370, "score": 872, "time": 1524538 } } }, { "id": "952ff355-239d-40a1-86b7-fcb09414b363", "steam": "76561198053222412", "steamName": "t_reks", "trainTime": 0, "trainPoints": 0, "trainDistance": 0, "dispatcherTime": 1851283324, "dispatcherStats": { "Idzikowice": { "time": 1851283324 } } }, { "id": "4d181e15-c06a-484d-b61b-11c21d080647", "steam": "76561199100147616", "steamName": "Pagor", "trainTime": 0, "trainPoints": 0, "trainDistance": 0, "dispatcherTime": 1849854575, "dispatcherStats": { "Szeligi": { "time": 1849854575 } } }, { "id": "97799495-ed7b-493d-aea0-51b8c61f1a31", "steam": "76561198405571901", "steamName": "volvo", "trainTime": 0, "trainPoints": 0, "trainDistance": 0, "dispatcherTime": 429421112, "dispatcherStats": { "Tunel": { "time": 429421112 } } }] }, "code": 200 }.data.records;
const StationTable = ({ stations }: { stations: TLeaderboardRecord[] }) => {
const { t } = useTranslation();
return (
<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">
{/* <h4 className="mb-6 text-xl font-semibold text-black dark:text-white">
Top
</h4> */}
<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")}
</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")}
</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")}
</h5>
</div>
</div>
{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}
>
<div className="flex 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>
</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>
</div>
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
<Link
to={"/profile/" + station.steam}
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("leaderboard.profile")}
</Link>
</div>
</div>
))}
</div>
</div>
);
}
import { ChangeEvent, useEffect, useState } from 'react';
import { StationTable } from '../../components/leaderboard/StationTable.tsx';
import { useDebounce } from 'use-debounce';
export const StationLeaderboard = () => {
const [data, setData] = useState<TLeaderboardRecord[]>([]);
useEffect(() => {
fetch('http://localhost:2005/leaderboard/station/').then(x => x.json()).then(x => {
setData(x.data.records);
});
}, []);
const [searchItem, setSearchItem] = useState('');
const [searchValue] = useDebounce(searchItem, 500);
const [empty, setEmpty] = useState(false);
useEffect(() => {
setData([]);
setEmpty(false);
fetch('http://localhost:2005/leaderboard/station/?q=' + searchValue).then(x => x.json()).then(x => {
setData(x.data.records);
if (x.data.records.length === 0) setEmpty(true);
});
}, [searchValue])
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchItem(e.target.value);
};
return (
<>
<div className="flex flex-col gap-10">
{/* TODO: get data from API */}
<StationTable stations={trains} />
<StationTable stations={data} handleInputChange={handleInputChange} searchItem={searchItem} empty={empty} />
</div>
</>
);

View File

@ -1,97 +1,42 @@
import { useTranslation } from 'react-i18next';
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
import { Link } from 'react-router-dom';
const trains: TLeaderboardRecord[] = { "success": true, "data": { "records": [{ "id": "055d3e33-2623-4654-80dd-4d3516cfeccc", "steam": "76561198159312038", "steamName": "_JeNTeK_", "trainTime": 4416784286, "trainPoints": 978915, "trainDistance": 2467309, "dispatcherTime": 0, "trainStats": { "Pendolino (ED250)": { "distance": 2467309, "score": 978915, "time": 4416784286 } } }, { "id": "16b54f4a-0826-4005-b67d-2ab57d74ffeb", "steam": "76561199101984415", "steamName": "tomsobczak35", "trainTime": 4841000336, "trainPoints": 572048, "trainDistance": 8066546, "dispatcherTime": 0, "trainStats": { "Pendolino (ED250)": { "distance": 8066546, "score": 572048, "time": 4841000336 } } }, { "id": "a9050fd0-f3cd-45c9-8087-44948da79b00", "steam": "76561198258359953", "steamName": "Kashameister.", "trainTime": 4838762785, "trainPoints": 353232, "trainDistance": 10274248, "dispatcherTime": 0, "trainStats": { "EU07": { "distance": 10274248, "score": 353232, "time": 4838762785 } } }, { "id": "2a08cadb-bacb-494a-8d09-0ef1870f1057", "steam": "76561198200906855", "steamName": "Nesto Ash Leo", "trainTime": 4411743671, "trainPoints": 110438, "trainDistance": 4412798, "dispatcherTime": 0, "trainStats": { "EP08": { "distance": 4412798, "score": 110438, "time": 4411743671 } } }, { "id": "23de2c57-84da-4845-b901-a40b6b5e1261", "steam": "76561199065951587", "steamName": "Pablo", "trainTime": 3021172809, "trainPoints": 55277, "trainDistance": 429106, "dispatcherTime": 250586, "trainStats": { "EN96": { "distance": 429106, "score": 55277, "time": 3021172809 } }, "dispatcherStats": { "Korytów": { "time": 22064 }, "Olszamowice": { "time": 221137 }, "Dąbrowa Górnicza": { "time": 7385 } } }, { "id": "b44b2b4b-476a-4e52-b612-573d5c5eea78", "steam": "76561198356160006", "steamName": "Marcion", "trainTime": 9571588, "dispatcherTime": 0, "trainStats": { "Pendolino (ED250)": { "distance": 303450, "score": 9856, "time": 9571588 } }, "trainDistance": 303450, "trainPoints": 9856 }, { "id": "3ee1f655-8329-4c83-96ae-bcf162d31f78", "steam": "76561199465955782", "steamName": "Bolek", "trainTime": 14441779, "dispatcherTime": 0, "trainStats": { "Pendolino (ED250)": { "distance": 402970, "score": 9740, "time": 14441779 } }, "trainDistance": 402970, "trainPoints": 9740 }, { "id": "c3731119-72b0-47c4-a047-70315e595d01", "steam": "76561198048854814", "steamName": "Night King_UA", "trainTime": 9537157, "dispatcherTime": 0, "trainStats": { "Pendolino (ED250)": { "distance": 302954, "score": 9686, "time": 9537157 } }, "trainDistance": 302954, "trainPoints": 9686 }, { "id": "a88f7594-844b-47e1-b657-d6c0be2d021a", "steam": "76561198886710784", "steamName": "LIPTON2315", "trainTime": 10820008, "dispatcherTime": 0, "trainStats": { "EP08": { "distance": 240000, "score": 0, "time": 9860788 }, "Pendolino (ED250)": { "distance": 22693, "score": 9320, "time": 959220 } }, "trainDistance": 262693, "trainPoints": 9320 }, { "id": "42b5c7f0-3af4-4305-aca7-6462e5bf134b", "steam": "76561198067997310", "steamName": "Gladicek", "trainTime": 8042564, "dispatcherTime": 0, "trainStats": { "EU07": { "distance": 97165, "score": 0, "time": 4419762 }, "Traxx (E186)": { "distance": 5704, "score": 4483, "time": 873122 }, "EN57": { "distance": 27186, "score": 4696, "time": 2262371 }, "EN96": { "distance": 3506, "score": 0, "time": 487309 } }, "trainDistance": 133561, "trainPoints": 9179 }] }, "code": 200 }.data.records;
const TrainTable = ({ trains }: { trains: TLeaderboardRecord[] }) => {
const { t } = useTranslation();
return (
<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">
{/* <h4 className="mb-6 text-xl font-semibold text-black dark:text-white">
Top
</h4> */}
<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")}
</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")}
</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")}
</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")}
</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")}
</h5>
</div>
</div>
{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}
>
<div className="flex items-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>
</p>
</div>
<div className="flex items-center justify-center p-2.5 lg:p-5">
<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>
</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>
</div>
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
<Link
to={"/profile/" + train.steam}
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("leaderboard.profile")}
</Link>
</div>
</div>
))}
</div>
</div>
);
}
import { ChangeEvent, useEffect, useState } from 'react';
import { TrainTable } from '../../components/leaderboard/TrainTable.tsx';
import { useDebounce } from 'use-debounce';
export const TrainLeaderboard = () => {
const [data, setData] = useState<TLeaderboardRecord[]>([]);
useEffect(() => {
fetch('http://localhost:2005/leaderboard/train/').then(x => x.json()).then(x => {
setData(x.data.records);
});
}, []);
const [searchItem, setSearchItem] = useState('');
const [searchValue] = useDebounce(searchItem, 500);
const [empty, setEmpty] = useState(false);
useEffect(() => {
setData([]);
setEmpty(false);
fetch('http://localhost:2005/leaderboard/train/?q=' + searchValue).then(x => x.json()).then(x => {
setData(x.data.records);
if (x.data.records.length === 0) setEmpty(true);
});
}, [searchValue])
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchItem(e.target.value);
};
return (
<>
<div className="flex flex-col gap-10">
{/* TODO: get data from API */}
<TrainTable trains={trains} />
<TrainTable trains={data} handleInputChange={handleInputChange} searchItem={searchItem} empty={empty} />
</div>
</>
);

View File

@ -1,7 +1,7 @@
export interface TLeaderboardResponse {
success: boolean;
data: TLeaderboardData;
code: number;
data: TLeaderboardData;
code: number;
}
export interface TLeaderboardData {
@ -9,15 +9,15 @@ export interface TLeaderboardData {
}
export interface TLeaderboardRecord {
id: string;
steam: string;
steamName: string;
trainTime: number;
trainPoints: number;
trainDistance: number;
dispatcherTime: number;
id: string;
steam: string;
steamName: string;
trainTime: number;
trainPoints: number;
trainDistance: number;
dispatcherTime: number;
dispatcherStats?: { [key: string]: TLeaderboardDispatcherStat };
trainStats?: { [key: string]: TLeaderboardTrainStat };
trainStats?: { [key: string]: TLeaderboardTrainStat };
}
export interface TLeaderboardDispatcherStat {
@ -26,6 +26,6 @@ export interface TLeaderboardDispatcherStat {
export interface TLeaderboardTrainStat {
distance: number;
score: number;
time: number;
score: number;
time: number;
}

View File

@ -0,0 +1,24 @@
export interface TTrainResponse {
success: boolean;
data: TTrainData;
code: number;
}
export interface TTrainData {
records: TTrainRecord[];
}
export interface TTrainRecord {
id: string;
trainNumber: string;
userSteamId: string;
userUsername: string;
userAvatar: string;
joinedDate?: number;
leftDate: number;
distance: number;
points: number;
server: string;
trainName: string;
}

View File

@ -480,6 +480,13 @@
dependencies:
"@types/node" "*"
"@types/cors@^2.8.17":
version "2.8.17"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==
dependencies:
"@types/node" "*"
"@types/estree@^1.0.5":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
@ -1131,6 +1138,14 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
cross-spawn@^7.0.0:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -2038,7 +2053,7 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
object-assign@^4.0.1, object-assign@^4.1.1:
object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
@ -2879,6 +2894,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-debounce@^10.0.4:
version "10.0.4"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.4.tgz#2135be498ad855416c4495cfd8e0e130bd33bb24"
integrity sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==
util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@ -2894,7 +2914,7 @@ uuid@^10.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
vary@~1.1.2:
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==