v3 release #75
@ -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)
|
||||
|
@ -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(),
|
||||
});
|
||||
})
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
36
packages/backend/src/http/routes/stats.ts
Normal file
36
packages/backend/src/http/routes/stats.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
1
packages/frontend/.env.example
Normal file
1
packages/frontend/.env.example
Normal file
@ -0,0 +1 @@
|
||||
VITE_API_URL=API URL
|
22
packages/frontend/.gitignore
vendored
22
packages/frontend/.gitignore
vendored
@ -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
|
@ -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={
|
||||
|
21
packages/frontend/src/components/mini/alerts/Error.tsx
Normal file
21
packages/frontend/src/components/mini/alerts/Error.tsx
Normal 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>
|
19
packages/frontend/src/components/mini/alerts/Success.tsx
Normal file
19
packages/frontend/src/components/mini/alerts/Success.tsx
Normal 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>
|
@ -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>
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import useColorMode from '../../hooks/useColorMode';
|
||||
import useColorMode from '../../../hooks/useColorMode';
|
||||
|
||||
const DarkModeSwitcher = () => {
|
||||
const [colorMode, setColorMode] = useColorMode();
|
@ -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>}
|
||||
</>
|
||||
);
|
17
packages/frontend/src/components/mini/util/Search.tsx
Normal file
17
packages/frontend/src/components/mini/util/Search.tsx
Normal 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>
|
||||
}
|
@ -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>}
|
||||
</>
|
||||
|
||||
);
|
@ -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>}
|
||||
|
||||
</>
|
||||
);
|
88
packages/frontend/src/components/pages/logs/StationTable.tsx
Normal file
88
packages/frontend/src/components/pages/logs/StationTable.tsx
Normal 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>}
|
||||
</>
|
||||
);
|
||||
}
|
105
packages/frontend/src/components/pages/logs/TrainTable.tsx
Normal file
105
packages/frontend/src/components/pages/logs/TrainTable.tsx
Normal 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>}
|
||||
</>
|
||||
);
|
||||
}
|
177
packages/frontend/src/components/pages/profile/Profile.tsx
Normal file
177
packages/frontend/src/components/pages/profile/Profile.tsx
Normal 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>
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
|
38
packages/frontend/src/pages/logs/StationLogs.tsx
Normal file
38
packages/frontend/src/pages/logs/StationLogs.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
39
packages/frontend/src/pages/logs/TrainLogs.tsx
Normal file
39
packages/frontend/src/pages/logs/TrainLogs.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
49
packages/frontend/src/pages/profile/Profile.tsx
Normal file
49
packages/frontend/src/pages/profile/Profile.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
11
packages/frontend/src/react-app-env.d.ts
vendored
11
packages/frontend/src/react-app-env.d.ts
vendored
@ -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
|
||||
}
|
63
packages/frontend/src/types/profile.ts
Normal file
63
packages/frontend/src/types/profile.ts
Normal 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;
|
||||
}
|
21
packages/frontend/src/types/station.ts
Normal file
21
packages/frontend/src/types/station.ts
Normal 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;
|
||||
}
|
21
packages/frontend/src/types/stats.ts
Normal file
21
packages/frontend/src/types/stats.ts
Normal 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;
|
||||
}
|
@ -20,5 +20,4 @@
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "src/lib.d.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user