v3 release #75
@ -11,6 +11,7 @@
|
|||||||
"author": "Aleksander <alekswilc> Wilczyński",
|
"author": "Aleksander <alekswilc> Wilczyński",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simrail/types": "^0.0.4",
|
"@simrail/types": "^0.0.4",
|
||||||
"@types/mongoose": "^5.11.97",
|
"@types/mongoose": "^5.11.97",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
@ -2,10 +2,12 @@ import { Router } from 'express';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { msToTime } from '../../util/time.js';
|
import { msToTime } from '../../util/time.js';
|
||||||
import { PipelineStage } from 'mongoose';
|
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 { MBlacklist } from '../../mongo/blacklist.js';
|
||||||
import { SteamUtil } from '../../util/SteamUtil.js';
|
import { SteamUtil } from '../../util/SteamUtil.js';
|
||||||
import { GitUtil } from '../../util/git.js';
|
import { GitUtil } from '../../util/git.js';
|
||||||
|
import { SuccessResponseBuilder } from '../responseBuilder.js';
|
||||||
|
import { removeProperties } from '../../util/functions.js';
|
||||||
|
|
||||||
const generateSearch = (regex: RegExp) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
{
|
{
|
||||||
@ -43,13 +45,22 @@ export class TrainsRoute {
|
|||||||
const records = await MTrainLog.aggregate(filter)
|
const records = await MTrainLog.aggregate(filter)
|
||||||
.sort({ leftDate: -1 })
|
.sort({ leftDate: -1 })
|
||||||
.limit(30)
|
.limit(30)
|
||||||
res.render('trains/index.ejs', {
|
|
||||||
records,
|
|
||||||
dayjs,
|
res.json(
|
||||||
q: req.query.q,
|
new SuccessResponseBuilder<{ records: Omit<ITrainLog, '_id' | '__v'>[] }>()
|
||||||
msToTime,
|
.setCode(200)
|
||||||
...GitUtil.getData()
|
.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) => {
|
app.get('/details/:id', async (req, res) => {
|
||||||
|
@ -7,7 +7,7 @@ import { ProfilesRoute } from './routes/profile.js';
|
|||||||
import { LeaderboardRoute } from './routes/leaderboard.js';
|
import { LeaderboardRoute } from './routes/leaderboard.js';
|
||||||
import { MProfile } from '../mongo/profile.js';
|
import { MProfile } from '../mongo/profile.js';
|
||||||
import { GitUtil } from '../util/git.js';
|
import { GitUtil } from '../util/git.js';
|
||||||
|
import cors from 'cors';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ export class ApiModule {
|
|||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
app.set('views', __dirname + '/views')
|
app.set('views', __dirname + '/views')
|
||||||
app.get('/', (_, res) => res.render('home', GitUtil.getData()));
|
app.get('/', (_, res) => res.render('home', GitUtil.getData()));
|
||||||
|
app.use(cors());
|
||||||
// backward compatible
|
// backward compatible
|
||||||
app.get('/details/:id', (req, res) => res.redirect('/stations/details/'+req.params.id));
|
app.get('/details/:id', (req, res) => res.redirect('/stations/details/'+req.params.id));
|
||||||
|
|
||||||
|
@ -24,7 +24,8 @@
|
|||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"sort-by": "^0.0.2"
|
"sort-by": "^0.0.2",
|
||||||
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.17",
|
"@types/react": "^18.2.17",
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,19 +1,19 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { Loader } from './common/Loader';
|
import { Loader } from './components/loaders/PageLoader.tsx';
|
||||||
import { PageTitle } from './common/PageTitle';
|
import { PageTitle } from './components/common/PageTitle.tsx';
|
||||||
import Chart from './pages/Chart';
|
import Chart from './old/Chart.tsx';
|
||||||
import { Home } from './pages/Home';
|
import { Home } from './pages/Home';
|
||||||
import Profile from './pages/Profile';
|
import Settings from './old/Settings.tsx';
|
||||||
import Settings from './pages/Settings';
|
import Tables from './old/Tables.tsx';
|
||||||
import Tables from './pages/Tables';
|
import Alerts from './old/UiElements/Alerts.tsx';
|
||||||
import Alerts from './pages/UiElements/Alerts';
|
import Buttons from './old/UiElements/Buttons.tsx';
|
||||||
import Buttons from './pages/UiElements/Buttons';
|
|
||||||
import DefaultLayout from './layout/DefaultLayout';
|
import DefaultLayout from './layout/DefaultLayout';
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
import { TrainLeaderboard } from './pages/leaderboard/TrainLeaderboard.tsx';
|
import { TrainLeaderboard } from './pages/leaderboard/TrainLeaderboard.tsx';
|
||||||
import { StationLeaderboard } from './pages/leaderboard/StationsLeaderboard.tsx';
|
import { StationLeaderboard } from './pages/leaderboard/StationsLeaderboard.tsx';
|
||||||
|
import { TrainLogs } from './pages/logs/TrainLogs.tsx';
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -25,7 +25,7 @@ function App() {
|
|||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => setLoading(false), 1000);
|
setTimeout(() => setLoading(false), 400);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return loading ? (
|
return loading ? (
|
||||||
@ -51,6 +51,17 @@ function App() {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/logs/trains"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<PageTitle title="simrail.alekswilc.dev | Train logs" />
|
||||||
|
<TrainLogs />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/leaderboard/stations"
|
path="/leaderboard/stations"
|
||||||
element={
|
element={
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import DarkModeSwitcher from './DarkModeSwitcher.tsx';
|
||||||
import LogoIcon from '../../images/logo/logo-icon.svg';
|
|
||||||
import DarkModeSwitcher from './DarkModeSwitcher';
|
|
||||||
|
|
||||||
const Header = (props: {
|
const Header = (props: {
|
||||||
sidebarOpen: string | boolean | undefined;
|
sidebarOpen: string | boolean | undefined;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
import SidebarLinkGroup from './SidebarLinkGroup';
|
import SidebarLinkGroup from './SidebarLinkGroup.tsx';
|
||||||
import Logo from '../../images/logo/logo.svg';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
|
24
packages/frontend/src/components/common/NoRecordsFound.tsx
Normal file
24
packages/frontend/src/components/common/NoRecordsFound.tsx
Normal 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>
|
||||||
|
}
|
@ -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 />}
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
100
packages/frontend/src/components/leaderboard/TrainTable.tsx
Normal file
100
packages/frontend/src/components/leaderboard/TrainTable.tsx
Normal 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 />}
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
57
packages/frontend/src/components/loaders/ContentLoader.tsx
Normal file
57
packages/frontend/src/components/loaders/ContentLoader.tsx
Normal 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>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -6,33 +6,24 @@ function useLocalStorage<T>(
|
|||||||
key: string,
|
key: string,
|
||||||
initialValue: T
|
initialValue: T
|
||||||
): [T, (value: SetValue<T>) => void] {
|
): [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(() => {
|
const [storedValue, setStoredValue] = useState(() => {
|
||||||
try {
|
try {
|
||||||
// Get from local storage by key
|
|
||||||
const item = window.localStorage.getItem(key);
|
const item = window.localStorage.getItem(key);
|
||||||
// Parse stored json or if none return initialValue
|
|
||||||
return item ? JSON.parse(item) : initialValue;
|
return item ? JSON.parse(item) : initialValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If error also return initialValue
|
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return initialValue;
|
return initialValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// useEffect to update local storage when the state changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
// Allow value to be a function so we have same API as useState
|
|
||||||
const valueToStore =
|
const valueToStore =
|
||||||
typeof storedValue === 'function'
|
typeof storedValue === 'function'
|
||||||
? storedValue(storedValue)
|
? storedValue(storedValue)
|
||||||
: storedValue;
|
: storedValue;
|
||||||
// Save state
|
|
||||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// A more advanced implementation would handle the error case
|
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}, [key, storedValue]);
|
}, [key, storedValue]);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import translationsInEng from "../languages/en.json";
|
import translationsInEng from "./languages/en.json";
|
||||||
import translationsInPl from "../languages/pl.json";
|
import translationsInPl from "./languages/pl.json";
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: {
|
en: {
|
||||||
|
@ -35,7 +35,24 @@
|
|||||||
"time": "Czas",
|
"time": "Czas",
|
||||||
"distance": "Dystans",
|
"distance": "Dystans",
|
||||||
"points": "Punkty",
|
"points": "Punkty",
|
||||||
"profile": "Otwórz profil",
|
"profile": "Profil",
|
||||||
"actions": "Akcje"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,35 +1,23 @@
|
|||||||
import React, { useState, ReactNode } from 'react';
|
import React, { useState, ReactNode } from 'react';
|
||||||
import Header from '../components/Header/index';
|
import Header from '../components/header/index';
|
||||||
import Sidebar from '../components/Sidebar/index';
|
import Sidebar from '../components/sidebar/index';
|
||||||
|
|
||||||
const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) => {
|
const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-boxdark-2 dark:text-bodydark">
|
<div className="dark:bg-boxdark-2 dark:text-bodydark">
|
||||||
{/* <!-- ===== Page Wrapper Start ===== --> */}
|
|
||||||
<div className="flex h-screen overflow-hidden">
|
<div className="flex h-screen overflow-hidden">
|
||||||
{/* <!-- ===== Sidebar Start ===== --> */}
|
|
||||||
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
||||||
{/* <!-- ===== Sidebar End ===== --> */}
|
|
||||||
|
|
||||||
{/* <!-- ===== Content Area Start ===== --> */}
|
|
||||||
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
|
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
|
||||||
{/* <!-- ===== Header Start ===== --> */}
|
|
||||||
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
||||||
{/* <!-- ===== Header End ===== --> */}
|
|
||||||
|
|
||||||
{/* <!-- ===== Main Content Start ===== --> */}
|
|
||||||
<main>
|
<main>
|
||||||
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{/* <!-- ===== Main Content End ===== --> */}
|
|
||||||
</div>
|
</div>
|
||||||
{/* <!-- ===== Content Area End ===== --> */}
|
|
||||||
</div>
|
</div>
|
||||||
{/* <!-- ===== Page Wrapper End ===== --> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ChartOne from '../components/Charts/ChartOne';
|
import ChartOne from './Charts/ChartOne';
|
||||||
import ChartThree from '../components/Charts/ChartThree';
|
import ChartThree from './Charts/ChartThree';
|
||||||
import ChartTwo from '../components/Charts/ChartTwo';
|
import ChartTwo from './Charts/ChartTwo';
|
||||||
|
|
||||||
const Chart: React.FC = () => {
|
const Chart: React.FC = () => {
|
||||||
return (
|
return (
|
@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom';
|
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 UserOne from '../../images/user/user-01.png';
|
||||||
import UserTwo from '../../images/user/user-02.png';
|
import UserTwo from '../../images/user/user-02.png';
|
||||||
import UserThree from '../../images/user/user-03.png';
|
import UserThree from '../../images/user/user-03.png';
|
@ -1,4 +1,4 @@
|
|||||||
import { Package } from '../../types/package';
|
import { Package } from '../package';
|
||||||
|
|
||||||
const packageData: Package[] = [
|
const packageData: Package[] = [
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
import { Product } from '../../types/product';
|
import { Product } from '../product';
|
||||||
import ProductOne from '../../images/product/product-01.png';
|
import ProductOne from '../../images/product/product-01.png';
|
||||||
import ProductTwo from '../../images/product/product-02.png';
|
import ProductTwo from '../../images/product/product-02.png';
|
||||||
import ProductThree from '../../images/product/product-03.png';
|
import ProductThree from '../../images/product/product-03.png';
|
1
packages/frontend/src/old/readme
Normal file
1
packages/frontend/src/old/readme
Normal file
@ -0,0 +1 @@
|
|||||||
|
this folder contains old unused files. remove before release!
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CardDataStats from '../components/CardDataStats';
|
import CardDataStats from '../old/CardDataStats';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -1,78 +1,41 @@
|
|||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
|
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { ChangeEvent, useEffect, useState } from 'react';
|
||||||
|
import { StationTable } from '../../components/leaderboard/StationTable.tsx';
|
||||||
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;
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StationLeaderboard = () => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
{/* TODO: get data from API */}
|
{/* TODO: get data from API */}
|
||||||
<StationTable stations={trains} />
|
<StationTable stations={data} handleInputChange={handleInputChange} searchItem={searchItem} empty={empty} />
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,97 +1,42 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
|
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
|
||||||
import { Link } from 'react-router-dom';
|
import { ChangeEvent, useEffect, useState } from 'react';
|
||||||
|
import { TrainTable } from '../../components/leaderboard/TrainTable.tsx';
|
||||||
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;
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TrainLeaderboard = () => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
{/* TODO: get data from API */}
|
{/* TODO: get data from API */}
|
||||||
<TrainTable trains={trains} />
|
|
||||||
|
<TrainTable trains={data} handleInputChange={handleInputChange} searchItem={searchItem} empty={empty} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
24
packages/frontend/src/types/train.ts
Normal file
24
packages/frontend/src/types/train.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
24
yarn.lock
24
yarn.lock
@ -480,6 +480,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@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":
|
"@types/estree@^1.0.5":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
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"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
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:
|
cross-spawn@^7.0.0:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
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"
|
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||||
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
|
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"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||||
@ -2879,6 +2894,11 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
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:
|
util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
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"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||||
|
|
||||||
vary@~1.1.2:
|
vary@^1, vary@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user