v3 release #75

Merged
alekswilc merged 63 commits from v3 into main 2024-12-13 20:29:17 +01:00
40 changed files with 894 additions and 187 deletions
Showing only changes of commit 6f7f2dad52 - Show all commits

View File

@ -1,10 +1,6 @@
import { Router } from 'express';
import dayjs from 'dayjs';
import { msToTime } from '../../util/time.js';
import { PipelineStage } from 'mongoose';
import { IProfile, MProfile, raw_schema } from '../../mongo/profile.js';
import { GitUtil } from '../../util/git.js';
import { SuccessResponseBuilder } from '../responseBuilder.js';
import { removeProperties } from '../../util/functions.js';
@ -26,7 +22,6 @@ export class LeaderboardRoute {
const filter: PipelineStage[] = [];
s && filter.push({
$match: {
$and: [
@ -35,12 +30,10 @@ export class LeaderboardRoute {
}
})
const records = await MProfile.aggregate(filter)
.sort({ trainPoints: -1 })
.limit(10)
res.json(
new SuccessResponseBuilder<{ records: Omit<IProfile, '_id' | '__v'>[] }>()
.setCode(200)
@ -54,8 +47,6 @@ export class LeaderboardRoute {
const s = req.query.q?.toString().split(',').map(x => new RegExp(x, "i"));
const filter: PipelineStage[] = [];
s && filter.push({
$match: {
$and: [
@ -64,12 +55,10 @@ export class LeaderboardRoute {
}
})
const records = await MProfile.aggregate(filter)
.sort({ dispatcherTime: -1 })
.limit(10)
res.json(
new SuccessResponseBuilder<{ records: Omit<IProfile, '_id' | '__v'>[] }>()
.setCode(200)

View File

@ -3,9 +3,7 @@ import { msToTime } from '../../util/time.js';
import { MProfile } from '../../mongo/profile.js';
import { MBlacklist } from '../../mongo/blacklist.js';
import { SteamUtil } from '../../util/SteamUtil.js';
import { GitUtil } from '../../util/git.js';
import { ErrorResponseBuilder, SuccessResponseBuilder } from '../responseBuilder.js';
export class ProfilesRoute {
static load() {
@ -15,16 +13,27 @@ export class ProfilesRoute {
app.get('/:id', async (req, res) => {
if (!req.params.id) return res.redirect('/');
const player = await MProfile.findOne({ steam: req.params.id });
if (!player) return res.render('profiles/private.ejs', GitUtil.getData());
if (!player) return res.status(404).json(new ErrorResponseBuilder()
.setCode(404).setData("Profile not found! (propably private)"));
const blacklist = await MBlacklist.findOne({ steam: req.params.id! });
if (blacklist && blacklist.status) return res.render('profiles/private.ejs', GitUtil.getData());
if (blacklist && blacklist.status) return res.status(403).json(new ErrorResponseBuilder()
.setCode(403).setData("Profile blacklisted!"));
const steam = await SteamUtil.getPlayer(player?.steam!);
const steamStats = await SteamUtil.getPlayerStats(player?.steam!);
res.json(
new SuccessResponseBuilder()
.setCode(200)
.setData({
player, steam, steamStats
})
.toJSON()
)
res.render('profiles/index.ejs', {
player, steam, steamStats: steamStats,
msToTime,
...GitUtil.getData(),
});
})

View File

@ -1,11 +1,13 @@
import { Router } from 'express';
import { MLog } from '../../mongo/logs.js';
import { ILog, MLog } from '../../mongo/logs.js';
import dayjs from 'dayjs';
import { msToTime } from '../../util/time.js';
import { PipelineStage } from 'mongoose';
import { MBlacklist } from '../../mongo/blacklist.js';
import { SteamUtil } from '../../util/SteamUtil.js';
import { GitUtil } from '../../util/git.js';
import { removeProperties } from '../../util/functions.js';
import { SuccessResponseBuilder } from '../responseBuilder.js';
const generateSearch = (regex: RegExp) => [
{
@ -46,13 +48,12 @@ export class StationsRoute {
const records = await MLog.aggregate(filter)
.sort({ leftDate: -1 })
.limit(30)
res.render('stations/index.ejs', {
records,
dayjs,
q: req.query.q,
msToTime,
...GitUtil.getData()
});
res.json(
new SuccessResponseBuilder<{ records: Omit<ILog, '_id' | '__v'>[] }>()
.setCode(200)
.setData({ records: records.map(x => removeProperties<Omit<ILog, '_id' | '__v'>>(x, ['_id', '__v'])) })
.toJSON()
);
})
app.get('/details/:id', async (req, res) => {
@ -69,28 +70,7 @@ export class StationsRoute {
msToTime,
...GitUtil.getData()
});
})
// API ENDPOINTS
// CREATE AN ISSUE IF YOU NEED API ACCESS: https://git.alekswilc.dev/alekswilc/simrail-logs/issues
/*
app.get('/api/last', async (req, res) => {
const records = await MLog.find()
.sort({ leftDate: -1 })
.limit(30)
res.json({ code: 200, records });
})
app.get('/api/search', async (req, res) => {
if (!req.query.q) return res.send('invalid');
const records = await MLog.find({ $text: { $search: req.query.q as string } })
.sort({ leftDate: -1 })
.limit(30)
res.json({ code: 200, records });
})*/
});
return app;
}

View File

@ -0,0 +1,36 @@
import { Router } from 'express';
import dayjs from 'dayjs';
import { msToTime } from '../../util/time.js';
import { PipelineStage } from 'mongoose';
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';
import { MLog } from '../../mongo/logs.js';
import { MProfile } from '../../mongo/profile.js';
export class StatsRoute {
static load() {
const app = Router();
app.get('/', async (req, res) => {
const { commit, version } = GitUtil.getData();
const trains = await MTrainLog.countDocuments();
const dispatchers = await MLog.countDocuments();
const profiles = await MProfile.countDocuments();
res.json(
new SuccessResponseBuilder<{ git: { commit?: string, version?: string }, stats: { trains: number, dispatchers: number, profiles: number } }>()
.setCode(200)
.setData({ git: { commit, version }, stats: { trains, dispatchers, profiles } })
.toJSON()
);
})
return app;
}
}

View File

@ -8,6 +8,7 @@ import { LeaderboardRoute } from './routes/leaderboard.js';
import { MProfile } from '../mongo/profile.js';
import { GitUtil } from '../util/git.js';
import cors from 'cors';
import { StatsRoute } from './routes/stats.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -21,12 +22,13 @@ export class ApiModule {
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));
app.get('/details/:id', (req, res) => res.redirect('/stations/details/' + req.params.id));
app.use('/stations/', StationsRoute.load());
app.use('/trains/', TrainsRoute.load());
app.use('/profiles/', ProfilesRoute.load());
app.use('/leaderboard/', LeaderboardRoute.load())
app.use('/leaderboard/', LeaderboardRoute.load());
app.use('/stats/', StatsRoute.load());
app.listen(2005);
}

View File

@ -1,14 +1,14 @@
import { execSync } from 'child_process';
export class GitUtil {
private static cache: { lastUpdated: number, version: string | null, commit: string | null } = undefined!;
private static cache: { lastUpdated: number, version?: string, commit?: string } = undefined!;
public static getLatestVersion() {
try {
const data = execSync('git describe --tags --exact-match').toString();
return data.replace('\n', '');
} catch {
return null;
return undefined;
}
}
@ -17,7 +17,7 @@ export class GitUtil {
const data = execSync('git rev-parse --short HEAD').toString();
return data.replace('\n', '');
} catch {
return null;
return undefined;
}
}

View File

@ -0,0 +1 @@
VITE_API_URL=API URL

View File

@ -1,24 +1,6 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.env
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.local

View File

@ -1,12 +1,11 @@
import { useEffect, useState } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { Loader } from './components/loaders/PageLoader.tsx';
import { PageTitle } from './components/common/PageTitle.tsx';
import { Loader } from './components/mini/loaders/PageLoader.tsx';
import { PageTitle } from './components/mini/util/PageTitle.tsx';
import Chart from './old/Chart.tsx';
import { Home } from './pages/Home';
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';
@ -14,7 +13,8 @@ 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';
function App() {
const [loading, setLoading] = useState<boolean>(true);
@ -52,7 +52,7 @@ function App() {
}
/>
<Route
<Route
path="/logs/trains"
element={
<>
@ -62,6 +62,16 @@ function App() {
}
/>
<Route
path="/logs/stations"
element={
<>
<PageTitle title="simrail.alekswilc.dev | Station logs" />
<StationLogs />
</>
}
/>
<Route
path="/leaderboard/stations"
element={
@ -71,15 +81,17 @@ function App() {
</>
}
/>
<Route
path="/tables"
path="/profile/:id"
element={
<>
<PageTitle title="Tables | TailAdmin - Tailwind CSS Admin Dashboard Template" />
<Tables />
<PageTitle title="simrail.alekswilc.dev | Stations Leaderboard" />
<Profile />
</>
}
/>
<Route
path="/settings"
element={

View File

@ -0,0 +1,21 @@
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]'>
<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]'>
{title}
</h5>
<ul>
<li className='leading-relaxed text-[#CD5D5D]'>
{description}
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,19 @@
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]'>
<svg width='16' height='12' viewBox='0 0 16 12' fill='none' xmlns='<http://www.w3.org/2000/svg>'>
<path
d='M15.2984 0.826822L15.2868 0.811827L15.2741 0.797751C14.9173 0.401867 14.3238 0.400754 13.9657 0.794406L5.91888 9.45376L2.05667 5.2868C1.69856 4.89287 1.10487 4.89389 0.747996 5.28987C0.417335 5.65675 0.417335 6.22337 0.747996 6.59026L0.747959 6.59029L0.752701 6.59541L4.86742 11.0348C5.14445 11.3405 5.52858 11.5 5.89581 11.5C6.29242 11.5 6.65178 11.3355 6.92401 11.035L15.2162 2.11161C15.5833 1.74452 15.576 1.18615 15.2984 0.826822Z'
fill='white' stroke='white'></path>
</svg>
</div>
<div className='w-full'>
<h5 className='mb-3 text-lg font-semibold text-black dark:text-[#34D399] '>
{title}
</h5>
<p className='text-base leading-relaxed text-body'>
{description}
</p>
</div>
</div>

View File

@ -1,9 +1,5 @@
import { useTranslation } from 'react-i18next';
export const NoRecordsFound = () => {
const { t } = useTranslation();
return <div
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'>
<svg width='19' height='16' viewBox='0 0 19 16' fill='none' xmlns='<http://www.w3.org/2000/svg>'>
@ -14,11 +10,10 @@ export const NoRecordsFound = () => {
</div>
<div className='w-full'>
<h5 className='mb-3 text-lg font-semibold text-[#9D5425]'>
Nie znaleziono danych
{title}
</h5>
<p className='leading-relaxed text-[#D0915C]'>
Twoje wyszukiwanie nie zwróciło wyników.
{description}
</p>
</div>
</div>
}

View File

@ -1,4 +1,4 @@
import useColorMode from '../../hooks/useColorMode';
import useColorMode from '../../../hooks/useColorMode';
const DarkModeSwitcher = () => {
const [colorMode, setColorMode] = useColorMode();

View File

@ -2,8 +2,8 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
const LoadError = () => {
export const LoadError = () => {
const { t } = useTranslation();
return <div
@ -25,8 +25,13 @@ const LoadError = () => {
</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>
{/* 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("contentloader.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("contentloader.error.refresh")}</Link>
</div>
</div>
</li>
@ -49,8 +54,6 @@ export const ContentLoader = () => {
<>
{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

@ -0,0 +1,17 @@
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">
<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={t("logs.search")}
/>
</div>
</div>
}

View File

@ -1,31 +1,20 @@
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';
import { TLeaderboardRecord } from '../../../types/leaderboard.ts';
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
export const StationTable = ({ stations, handleInputChange, searchItem, empty }: { stations: TLeaderboardRecord[], searchItem: string, handleInputChange: ChangeEventHandler, empty: boolean }) => {
export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord[], error: number }) => {
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">
{error === 2 && <WarningAlert title={t("contentloader.notfound.header")} description={t("contentloader.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">
@ -68,7 +57,7 @@ export const StationTable = ({ stations, handleInputChange, searchItem, empty }:
<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"
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")}
</Link>
@ -76,7 +65,7 @@ export const StationTable = ({ stations, handleInputChange, searchItem, empty }:
</div>
))}
</div>
</div> : <ContentLoader />}
</div>}
</>
);

View File

@ -1,33 +1,18 @@
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';
import { TLeaderboardRecord } from '../../../types/leaderboard.ts';
import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx';
import { WarningAlert } from '../../mini/alerts/Warning.tsx';
export const TrainTable = ({ trains, handleInputChange, searchItem, empty }: { trains: TLeaderboardRecord[], searchItem: string, handleInputChange: ChangeEventHandler, empty: boolean }) => {
export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], error: number }) => {
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">
{error === 2 && <WarningAlert title={t("contentloader.notfound.header")} description={t("contentloader.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">
@ -86,14 +71,14 @@ export const TrainTable = ({ trains, handleInputChange, searchItem, empty }: { t
<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"
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")}
</Link>
</div>
</div>
))}
</div> </div> : <ContentLoader />}
</div> </div>}
</>
);

View File

@ -0,0 +1,88 @@
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';
// setSearchItem: Dispatch<SetStateAction<string>>
export const StationTable = ({ stations, error }: {
stations: TStationRecord[], error: number
}) => {
const { t } = useTranslation();
return (
<>
{error === 2 && <WarningAlert title={t("contentloader.notfound.header")} description={t("contentloader.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-4">
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{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")}
</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")}
</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")}
</h5>
</div>
</div>
{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}
>
<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>
</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>
</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>
</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}
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")}
</Link>
<Link
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")}
</Link>
</div>
</div>
))}
</div>
</div>}
</>
);
}

View File

@ -0,0 +1,105 @@
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';
// setSearchItem: Dispatch<SetStateAction<string>>
export const TrainTable = ({ trains, error }: {
trains: TTrainRecord[], error: number
}) => {
const { t } = useTranslation();
return (
<>
{error === 2 && <WarningAlert title={t("contentloader.notfound.header")} description={t("contentloader.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")}
</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")}
</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")}
</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")}
</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")}
</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")}
</h5>
</div>
</div>
{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}
>
<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>
</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>
</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>
</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>
</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>
</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}
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")}
</Link>
<Link
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")}
</Link>
</div>
</div>
))}
</div>
</div>}
</>
);
}

View File

@ -0,0 +1,177 @@
import { useState } from 'react';
import { TProfileData } from '../../../types/profile.ts';
import { useTranslation } from 'react-i18next';
export const ProfileCard = ({ data }: { data: TProfileData }) => {
const [showTrains, setShowTrains] = useState(false);
const [showStations, setShowStations] = useState(false);
const { t } = useTranslation();
return <div className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
<div className="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
<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" />
</div>
</div>
<div className="mt-4">
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
{data.steam.personname}
</h3>
<div className="mx-auto mt-4.5 mb-5.5 grid max-w-94 grid-cols-2 rounded-md border border-stroke py-2.5 shadow-1 dark:border-strokedark dark:bg-[#37404F]">
<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
</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
</span>
<span className="text-sm text-wrap">{t("profile.stats.time")}</span>
</div>
</div>
</div>
</div>
{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>
<svg
className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${showTrains && '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>
</div>
{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")}
</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")}
</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")}
</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")}
</h5>
</div>
</div>
{Object.keys(data.player.trainStats).map(trainName => {
const train = data.player.trainStats[trainName];
return <div
className={`grid grid-cols-3 sm:grid-cols-4 ${false
? ''
: '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}
</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>
</div>
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
<p className="text-meta-3">{train.score}</p>
</div>
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
<p className="text-meta-3">{Math.floor(train.time / 3600000)}h</p>
</div>
</div>
})}
</div>}
</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>
<svg
className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${showStations && '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>
</div>
{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")}
</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")}
</h5>
</div>
</div>
{Object.keys(data.player.dispatcherStats).map(stationName => {
const station = data.player.dispatcherStats[stationName];
return <div
className={`grid grid-cols-2 ${false
? ''
: '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}
</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>
</div>
</div>;
})}
</div>}
</div>}
</div>
}

View File

@ -18,8 +18,8 @@
"footer": {
"license": "Licencja:",
"powered": "Oparte na:",
"version": "Wersja:",
"commit": "Commit:"
"thanks": "Specjalne podziękowania dla <bahu>serwerowni BAHU.PRO</bahu>, <simrailelite>discorda Simrail ELITE</simrailelite>, społeczności Simrail i mojej dziewczyny",
"author": "Stworzone przez <anchor>{{author}}</anchor> z ❤️ dla społeczności Simrail"
}
},
@ -31,7 +31,7 @@
"leaderboard": "Tablica wyników"
},
"leaderboard": {
"user": "Użytkownik",
"user": "Gracz",
"time": "Czas",
"distance": "Dystans",
"points": "Punkty",
@ -39,20 +39,55 @@
"actions": "Akcje"
},
"logs": {
"user": "Użytkownik",
"user": "Gracz",
"time": "Czas",
"distance": "Dystans",
"points": "Punkty",
"profile": "Profil",
"record": "Więcej",
"train": "Pociąg",
"actions": "Akcje"
"actions": "Akcje",
"search": "Wpisz, aby wyszukać",
"station": "Stacja"
},
"contentloader": {
"error": {
"header": "Błąd ładowania strony",
"description": "Sprawdź swoje połączenie z internetem i odśwież strone",
"report": "Zgłoś błąd"
"report": "Zgłoś błąd",
"refresh": "Odśwież"
},
"notfound": {
"header": "Nie znaleziono danych",
"description": "Twoje wyszukiwanie nie zwróciło wyników."
}
},
"profile": {
"stats": {
"distance": "Przejechanych kilometrów",
"time": "Godzin dyżurów"
},
"trains": {
"header": "Statystyki pociągów",
"train": "Pociąg",
"distance": "Dystans",
"points": "Punkty",
"time": "Czas"
},
"stations": {
"header": "Statystyki stacji",
"station": "Stacja",
"time": "Czas"
},
"errors": {
"notfound": {
"title": "Nie znaleziono profilu",
"description": "Profil gracza nie został znaleziony lub posiada on prywatny profil steam."
},
"blacklist": {
"title": "Nie można wyświetlic profilu",
"description": "Profil tego gracza został zablokowany."
}
}
}
}

View File

@ -1,6 +1,6 @@
import React, { useState, ReactNode } from 'react';
import Header from '../components/header/index';
import Sidebar from '../components/sidebar/index';
import Header from '../components/mini/header/index';
import Sidebar from '../components/mini/sidebar/index';
const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);

View File

@ -49,6 +49,7 @@ const TableThree = () => {
</tr>
</thead>
<tbody>
{packageData.map((packageItem, key) => (
<tr key={key}>
<td className="border-b border-[#eee] py-5 px-4 pl-9 dark:border-strokedark xl:pl-11">

View File

@ -1,18 +1,42 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import CardDataStats from '../old/CardDataStats';
import { useTranslation } from 'react-i18next';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom';
import { TStatsResponse } from '../types/stats.ts';
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);
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);
setTrains(data.data.stats.trains);
setDispatchers(data.data.stats.dispatchers);
setProfiles(data.data.stats.profiles);
});
}, [])
useEffect(() => { }, []);
return (
<>
<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="5,000" />
<CardDataStats title={t('home.stats.dispatchers')} total="3,000" />
<CardDataStats title={t('home.stats.profiles')} total="1,000" />
<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>
@ -46,10 +70,20 @@ export const Home: React.FC = () => {
<div className="px-4 pb-6 text-center">
<div className="mt-6.5">
<p>{t('home.footer.commit')} <Link className='color-orchid' to={"https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/commit/COMMIT"}>COMMIT</Link> | {t('home.footer.version')} <Link className='color-orchid' to={"https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/releases/tag/VERSION"}>VERSION</Link></p>
<p><Trans
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/"} />, simrailelite: <Link className='color-orchid' to={"https://bahu.pro/"} /> }}
/></p>
<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>{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>
</div>
</div>

View File

@ -1,27 +1,27 @@
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
import { ChangeEvent, useEffect, useState } from 'react';
import { StationTable } from '../../components/leaderboard/StationTable.tsx';
import { StationTable } from '../../components/pages/leaderboard/StationTable.tsx';
import { useDebounce } from 'use-debounce';
import { Search } from '../../components/mini/util/Search.tsx';
export const StationLeaderboard = () => {
const [data, setData] = useState<TLeaderboardRecord[]>([]);
useEffect(() => {
fetch('http://localhost:2005/leaderboard/station/').then(x => x.json()).then(x => {
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 [empty, setEmpty] = useState(false);
const [error, setError] = useState<0 | 1 | 2>(0);
useEffect(() => {
setData([]);
setEmpty(false);
fetch('http://localhost:2005/leaderboard/station/?q=' + searchValue).then(x => x.json()).then(x => {
setError(0);
fetch(`${import.meta.env.VITE_API_URL}/leaderboard/station/?q=${searchValue}`).then(x => x.json()).then(x => {
setData(x.data.records);
if (x.data.records.length === 0) setEmpty(true);
setError(x.data.records.length > 0 ? 1 : 2);
});
}, [searchValue])
@ -32,10 +32,8 @@ export const StationLeaderboard = () => {
return (
<>
<div className="flex flex-col gap-10">
{/* TODO: get data from API */}
<StationTable stations={data} handleInputChange={handleInputChange} searchItem={searchItem} empty={empty} />
<Search handleInputChange={handleInputChange} searchItem={searchItem} />
<StationTable stations={data} error={error} />
</div>
</>
);

View File

@ -1,27 +1,27 @@
import { TLeaderboardRecord } from '../../types/leaderboard.ts';
import { ChangeEvent, useEffect, useState } from 'react';
import { TrainTable } from '../../components/leaderboard/TrainTable.tsx';
import { TrainTable } from '../../components/pages/leaderboard/TrainTable.tsx';
import { useDebounce } from 'use-debounce';
import { Search } from '../../components/mini/util/Search.tsx';
export const TrainLeaderboard = () => {
const [data, setData] = useState<TLeaderboardRecord[]>([]);
useEffect(() => {
fetch('http://localhost:2005/leaderboard/train/').then(x => x.json()).then(x => {
fetch(`${import.meta.env.VITE_API_URL}/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);
const [error, setError] = useState<0 | 1 | 2>(0);
useEffect(() => {
setData([]);
setEmpty(false);
fetch('http://localhost:2005/leaderboard/train/?q=' + searchValue).then(x => x.json()).then(x => {
setError(0);
fetch(`${import.meta.env.VITE_API_URL}/leaderboard/train/?q=${searchValue}`).then(x => x.json()).then(x => {
setData(x.data.records);
if (x.data.records.length === 0) setEmpty(true);
setError(x.data.records.length > 0 ? 1 : 2);
});
}, [searchValue])
@ -29,13 +29,11 @@ export const TrainLeaderboard = () => {
setSearchItem(e.target.value);
};
return (
<>
<div className="flex flex-col gap-10">
{/* TODO: get data from API */}
<TrainTable trains={data} handleInputChange={handleInputChange} searchItem={searchItem} empty={empty} />
<Search handleInputChange={handleInputChange} searchItem={searchItem} />
<TrainTable trains={data} error={error} />
</div>
</>

View File

@ -0,0 +1,38 @@
import { ChangeEvent, useEffect, useState } from 'react';
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';
export const StationLogs = () => {
const [data, setData] = useState<TStationRecord[]>([]);
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/stations/`).then(x => x.json()).then(x => {
setData(x.data.records);
});
}, []);
const [error, setError] = useState<0 | 1 | 2>(0);
const [searchItem, setSearchItem] = useState('');
const [searchValue] = useDebounce(searchItem, 500);
useEffect(() => {
setData([]);
setError(0);
fetch(`${import.meta.env.VITE_API_URL}/stations/?q=${searchValue}`).then(x => x.json()).then(x => {
setData(x.data.records);
setError(x.data.records.length > 0 ? 1 : 2);
});
}, [searchValue])
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchItem(e.target.value);
};
return (
<>
<div className="flex flex-col gap-10">
<Search handleInputChange={handleInputChange} searchItem={searchItem} />
<StationTable stations={data} error={error} />
</div>
</>
);
};

View File

@ -0,0 +1,39 @@
import { ChangeEvent, useEffect, useState } from 'react';
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';
export const TrainLogs = () => {
const [data, setData] = useState<TTrainRecord[]>([]);
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/trains/`).then(x => x.json()).then(x => {
setData(x.data.records);
});
}, []);
const [error, setError] = useState<0 | 1 | 2>(0);
const [searchItem, setSearchItem] = useState('');
const [searchValue] = useDebounce(searchItem, 500);
useEffect(() => {
setData([]);
setError(0);
fetch(`${import.meta.env.VITE_API_URL}/trains/?q=${searchValue}`).then(x => x.json()).then(x => {
setData(x.data.records);
setError(x.data.records.length > 0 ? 1 : 2);
});
}, [searchValue])
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchItem(e.target.value);
};
return (
<>
<div className="flex flex-col gap-10">
<Search handleInputChange={handleInputChange} searchItem={searchItem} />
<TrainTable trains={data} error={error} />
</div>
</>
);
};

View File

@ -0,0 +1,49 @@
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';
export const Profile = () => {
const { id } = useParams();
const [error, setError] = useState<0 | 1 | 2 | 3>(0);
const [data, setData] = useState<TProfileData>(undefined!);
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/profiles/${id}`).then(x => x.json()).then((data: TProfileResponse) => {
switch (data.code) {
case 404:
setError(2);
break;
case 403:
setError(3);
break;
case 200:
setError(1);
setData(data.data);
console.log(data.data.steam);
break;
}
})
}, [])
const { t } = useTranslation();
return (
<>
{/* LOADING */}
{error === 0 && <ContentLoader />}
{/* NOT FOUND */}
{error === 2 && <WarningAlert title={t("profile.errors.notfound.title")} description={t("profile.errors.notfound.description")} />}
{/* BLACKLISTED PROFILE */}
{error === 3 && <WarningAlert title={t("profile.errors.blacklist.title")} description={t("profile.errors.blacklist.description")} />}
{/* SUCCESS */}
{error === 1 && <ProfileCard data={data} />}
</>
);
};

View File

@ -1,4 +1,15 @@
/// <reference types="vite/client" />
declare module '*.png';
declare module '*.svg';
declare module '*.jpeg';
declare module '*.jpg';
interface ImportMetaEnv {
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -0,0 +1,63 @@
export type TProfileResponse = TProfileErrorResponse | TProfileSuccessResponse;
export interface TProfileErrorResponse {
success: false;
data: string;
code: 404 | 403;
}
export interface TProfileSuccessResponse {
success: true;
data: TProfileData;
code: 200;
}
export interface TProfileData {
player: TProfilePlayer;
steam: TProfileSteam;
steamStats: TProfileSteamStats;
}
export interface TProfilePlayer {
id: string;
steam: string;
steamName: string;
trainTime: number;
dispatcherTime: number;
dispatcherStats: Record<string, TProfileDispatcherStatsRecord>;
trainStats: Record<string, TProfileTrainStatsRecord>;
trainDistance: number;
trainPoints: number;
}
export interface TProfileDispatcherStatsRecord {
time: number;
}
export interface TProfileTrainStatsRecord {
distance: number;
score: number;
time: number;
}
export interface TProfileSteam {
personname: string;
avatarfull: string;
}
export interface TProfileSteamStats {
steamID: string;
gameName: string;
stats: TProfileSteamStatsAchievementStat[];
achievements: TProfileSteamStatsAchievement[];
}
export interface TProfileSteamStatsAchievement {
name: string;
achieved: number;
}
export interface TProfileSteamStatsAchievementStat {
name: string;
value: number;
}

View File

@ -0,0 +1,21 @@
export interface TStationResponse {
success: boolean;
data: TStationData;
code: number;
}
export interface TStationData {
records: TStationRecord[];
}
export interface TStationRecord {
id: string;
userSteamId: string;
userUsername: string;
userAvatar: string;
leftDate: number;
stationName: string;
stationShort: string;
server: string;
joinedDate?: number;
}

View File

@ -0,0 +1,21 @@
export interface TStatsResponse {
success: boolean;
data: TStatsData;
code: number;
}
export interface TStatsData {
git: TStatsGit;
stats: TStatsStats;
}
export interface TStatsGit {
commit?: string;
version?: string;
}
export interface TStatsStats {
trains: number;
dispatchers: number;
profiles: number;
}

View File

@ -20,5 +20,4 @@
"noFallthroughCasesInSwitch": true
},
"include": ["src", "src/lib.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}