forked from simrail/simrail.pro
feat(*): use SWR, add steam leaderboard, rewrite SimrailClient.ts, update DB structure.
This commit is contained in:
parent
0a3c70b108
commit
4ec78b998d
3
packages/backend/.gitignore
vendored
Normal file
3
packages/backend/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
@ -12,7 +12,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.10.1",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { PipelineStage } from "mongoose";
|
import { PipelineStage } from "mongoose";
|
||||||
import { IProfile, MProfile, raw_schema } from "../../mongo/profile.js";
|
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
||||||
|
|
||||||
@ -54,6 +54,7 @@ export class LeaderboardRoute
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortBy = sortyByMap[req.query.s?.toString() ?? 'distance'] ?? sortyByMap.distance;
|
const sortBy = sortyByMap[req.query.s?.toString() ?? 'distance'] ?? sortyByMap.distance;
|
||||||
|
|
||||||
const records = await MProfile.aggregate(filter)
|
const records = await MProfile.aggregate(filter)
|
||||||
|
@ -15,12 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { ITrainLog, MTrainLog } from "../../mongo/trainLogs.js";
|
import { MTrainLog } from "../../mongo/trainLog.js";
|
||||||
import { MBlacklist } from "../../mongo/blacklist.js";
|
|
||||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { removeProperties } from "../../util/functions.js";
|
import { MStationLog } from "../../mongo/stationLog.js";
|
||||||
import { ILog, MLog } from "../../mongo/logs.js";
|
import { IProfile } from "../../mongo/profile.js";
|
||||||
import { MProfile } from "../../mongo/profile.js";
|
|
||||||
|
|
||||||
|
|
||||||
export class LogRoute
|
export class LogRoute
|
||||||
@ -40,7 +38,7 @@ export class LogRoute
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const log = await MLog.findOne({ id }) || await MTrainLog.findOne({ id });
|
const log = await MStationLog.findOne({ id }).populate<{ player: IProfile }>('player').orFail().catch(() => null) || await MTrainLog.findOne({ id }).populate<{ player: IProfile }>('player').orFail().catch(() => null);
|
||||||
|
|
||||||
if (!log)
|
if (!log)
|
||||||
{
|
{
|
||||||
@ -49,12 +47,9 @@ export class LogRoute
|
|||||||
.setData("Invalid Id parameter").toJSON());
|
.setData("Invalid Id parameter").toJSON());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const profile = await MProfile.findOne({ steam: log.userSteamId });
|
|
||||||
|
|
||||||
|
|
||||||
res.status(200).json(new SuccessResponseBuilder().setCode(200).setData({
|
res.status(200).json(new SuccessResponseBuilder().setCode(200).setData({
|
||||||
verified: profile?.verified,
|
...log.toJSON()
|
||||||
...removeProperties<Omit<(ILog | ITrainLog), "_id" | "__v">>(log.toJSON(), [ "_id", "__v" ])
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,12 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
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 { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { removeProperties } from "../../util/functions.js";
|
import { PlayerUtil } from "../../util/PlayerUtil.js";
|
||||||
|
|
||||||
export class ProfilesRoute
|
export class ProfilesRoute
|
||||||
{
|
{
|
||||||
@ -36,30 +32,33 @@ export class ProfilesRoute
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = await MProfile.findOne({ steam: req.params.id });
|
const player = await PlayerUtil.getPlayer(req.params.id);
|
||||||
if (!player)
|
if (!player)
|
||||||
{
|
{
|
||||||
res.status(404).json(new ErrorResponseBuilder()
|
res.status(404).json(new ErrorResponseBuilder()
|
||||||
.setCode(404).setData("Profile not found! (probably private)"));
|
.setCode(404).setData("Profile not found!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blacklist = await MBlacklist.findOne({ steam: req.params.id! });
|
if (player.flags.includes('blacklist'))
|
||||||
if (blacklist && blacklist.status)
|
|
||||||
{
|
{
|
||||||
res.status(403).json(new ErrorResponseBuilder()
|
res.status(403).json(new ErrorResponseBuilder()
|
||||||
.setCode(403).setData("Profile blacklisted!"));
|
.setCode(403).setData("Profile blacklisted!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const steam = await SteamUtil.getPlayer(player?.steam!);
|
if (player.flags.includes('private'))
|
||||||
const steamStats = await SteamUtil.getPlayerStats(player?.steam!);
|
{
|
||||||
|
res.status(404).json(new ErrorResponseBuilder()
|
||||||
|
.setCode(404).setData("Profile is private!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({
|
.setData({
|
||||||
player: removeProperties(player, ['_id', '__v']), steam, steamStats,
|
player
|
||||||
})
|
})
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
|
@ -15,14 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { ILog, MLog } from "../../mongo/logs.js";
|
import { IStationLog, MStationLog } from "../../mongo/stationLog.js";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { msToTime } from "../../util/time.js";
|
|
||||||
import { PipelineStage } from "mongoose";
|
import { PipelineStage } from "mongoose";
|
||||||
import { MBlacklist } from "../../mongo/blacklist.js";
|
import { escapeRegexString } from "../../util/functions.js";
|
||||||
import { SteamUtil } from "../../util/SteamUtil.js";
|
|
||||||
import { GitUtil } from "../../util/git.js";
|
|
||||||
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
|
||||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { MProfile } from "../../mongo/profile.js";
|
import { MProfile } from "../../mongo/profile.js";
|
||||||
|
|
||||||
@ -31,13 +26,13 @@ const generateSearch = (regex: RegExp) => [
|
|||||||
stationName: { $regex: regex },
|
stationName: { $regex: regex },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
userUsername: { $regex: regex },
|
username: { $regex: regex },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stationShort: { $regex: regex },
|
stationShort: { $regex: regex },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
userSteamId: { $regex: regex },
|
steam: { $regex: regex },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
server: { $regex: regex },
|
server: { $regex: regex },
|
||||||
@ -53,10 +48,8 @@ export class StationsRoute
|
|||||||
app.get("/", async (req, res) =>
|
app.get("/", async (req, res) =>
|
||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
const profiles = await MProfile.find({ verified: true });
|
|
||||||
const filter: PipelineStage[] = [];
|
const filter: PipelineStage[] = [];
|
||||||
|
|
||||||
|
|
||||||
s && filter.push({
|
s && filter.push({
|
||||||
$match: {
|
$match: {
|
||||||
$and: [
|
$and: [
|
||||||
@ -65,20 +58,16 @@ export class StationsRoute
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const records = await MStationLog.aggregate(filter)
|
||||||
const records = await MLog.aggregate(filter)
|
|
||||||
.sort({ leftDate: -1 })
|
.sort({ leftDate: -1 })
|
||||||
.limit(30);
|
.limit(30);
|
||||||
|
|
||||||
|
await MProfile.populate(records, { path: "player" });
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<ILog, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder<{ records: IStationLog[] }>()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({ records: records.map(x => {
|
.setData({ records })
|
||||||
return {
|
|
||||||
...removeProperties<Omit<ILog, "_id" | "__v">>(x, [ "_id", "__v" ]),
|
|
||||||
verified: profiles.find(xx => xx.steam === x.userSteamId)
|
|
||||||
}
|
|
||||||
}) })
|
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -15,16 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import dayjs from "dayjs";
|
import { MTrainLog } from "../../mongo/trainLog.js";
|
||||||
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 { GitUtil } from "../../util/git.js";
|
||||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { removeProperties } from "../../util/functions.js";
|
import { MStationLog } from "../../mongo/stationLog.js";
|
||||||
import { MLog } from "../../mongo/logs.js";
|
|
||||||
import { MProfile } from "../../mongo/profile.js";
|
import { MProfile } from "../../mongo/profile.js";
|
||||||
|
|
||||||
export class StatsRoute
|
export class StatsRoute
|
||||||
@ -38,7 +32,7 @@ export class StatsRoute
|
|||||||
const { commit, version } = GitUtil.getData();
|
const { commit, version } = GitUtil.getData();
|
||||||
|
|
||||||
const trains = await MTrainLog.countDocuments();
|
const trains = await MTrainLog.countDocuments();
|
||||||
const dispatchers = await MLog.countDocuments();
|
const dispatchers = await MStationLog.countDocuments();
|
||||||
const profiles = await MProfile.countDocuments();
|
const profiles = await MProfile.countDocuments();
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
|
100
packages/backend/src/http/routes/steamLeaderboard.ts
Normal file
100
packages/backend/src/http/routes/steamLeaderboard.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router } from "express";
|
||||||
|
import { PipelineStage } from "mongoose";
|
||||||
|
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||||
|
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
|
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
||||||
|
|
||||||
|
const generateSearch = (regex: RegExp) => [
|
||||||
|
{
|
||||||
|
id: { $regex: regex },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: { $regex: regex },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortyByMap: Record<string, any> = {
|
||||||
|
points: { steamTrainDistance: -1 },
|
||||||
|
distance: { steamTrainScore: -1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SteamLeaderboardRoute
|
||||||
|
{
|
||||||
|
static load()
|
||||||
|
{
|
||||||
|
const app = Router();
|
||||||
|
|
||||||
|
app.get("/train", async (req, res) =>
|
||||||
|
{
|
||||||
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
|
|
||||||
|
const filter: PipelineStage[] = [];
|
||||||
|
|
||||||
|
s && filter.push({
|
||||||
|
$match: {
|
||||||
|
$and: [
|
||||||
|
...s.map(x => ({ $or: generateSearch(x) })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortBy = sortyByMap[req.query.s?.toString() ?? 'distance'] ?? sortyByMap.distance;
|
||||||
|
|
||||||
|
const records = await MProfile.aggregate(filter)
|
||||||
|
.sort(sortBy)
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
res.json(
|
||||||
|
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
||||||
|
.setCode(200)
|
||||||
|
.setData({ records: records.map(x => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
||||||
|
.toJSON(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.get("/station", async (req, res) =>
|
||||||
|
{
|
||||||
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
|
|
||||||
|
const filter: PipelineStage[] = [];
|
||||||
|
|
||||||
|
s && filter.push({
|
||||||
|
$match: {
|
||||||
|
$and: [
|
||||||
|
...s.map(x => ({ $or: generateSearch(x) })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const records = await MProfile.aggregate(filter)
|
||||||
|
.sort({ steamDispatcherTime: -1 })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
res.json(
|
||||||
|
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
||||||
|
.setCode(200)
|
||||||
|
.setData({ records: records.map(x => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
||||||
|
.toJSON(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
@ -15,15 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { msToTime } from "../../util/time.js";
|
|
||||||
import { PipelineStage } from "mongoose";
|
import { PipelineStage } from "mongoose";
|
||||||
import { ITrainLog, MTrainLog, raw_schema } from "../../mongo/trainLogs.js";
|
import { ITrainLog, MTrainLog } from "../../mongo/trainLog.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 { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
import { escapeRegexString } from "../../util/functions.js";
|
||||||
import { MProfile } from "../../mongo/profile.js";
|
import { MProfile } from "../../mongo/profile.js";
|
||||||
|
|
||||||
const generateSearch = (regex: RegExp) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
@ -31,13 +26,13 @@ const generateSearch = (regex: RegExp) => [
|
|||||||
trainNumber: { $regex: regex },
|
trainNumber: { $regex: regex },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
userSteamId: { $regex: regex },
|
username: { $regex: regex },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
server: { $regex: regex },
|
server: { $regex: regex },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
userUsername: { $regex: regex },
|
steam: { $regex: regex },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -50,7 +45,6 @@ export class TrainsRoute
|
|||||||
app.get("/", async (req, res) =>
|
app.get("/", async (req, res) =>
|
||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
const profiles = await MProfile.find({ verified: true });
|
|
||||||
|
|
||||||
const filter: PipelineStage[] = [];
|
const filter: PipelineStage[] = [];
|
||||||
|
|
||||||
@ -67,18 +61,13 @@ export class TrainsRoute
|
|||||||
.sort({ leftDate: -1 })
|
.sort({ leftDate: -1 })
|
||||||
.limit(30);
|
.limit(30);
|
||||||
|
|
||||||
|
await MProfile.populate(records, { path: "player" });
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<ITrainLog, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder<{ records: ITrainLog[] }>()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({
|
.setData({
|
||||||
records: records.map(x =>
|
records
|
||||||
{
|
|
||||||
return {
|
|
||||||
...removeProperties<Omit<ITrainLog, "_id" | "__v">>(x, [ "_id", "__v" ]),
|
|
||||||
verified: profiles.find(xx => xx.steam === x.userSteamId)
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
|
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import express, { Router } from "express";
|
import express, { Router } from "express";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import path from "node:path";
|
|
||||||
import { StationsRoute } from "./routes/stations.js";
|
import { StationsRoute } from "./routes/stations.js";
|
||||||
import { TrainsRoute } from "./routes/trains.js";
|
import { TrainsRoute } from "./routes/trains.js";
|
||||||
import { ProfilesRoute } from "./routes/profile.js";
|
import { ProfilesRoute } from "./routes/profile.js";
|
||||||
@ -24,6 +22,7 @@ import { LeaderboardRoute } from "./routes/leaderboard.js";
|
|||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import { StatsRoute } from "./routes/stats.js";
|
import { StatsRoute } from "./routes/stats.js";
|
||||||
import { LogRoute } from "./routes/log.js";
|
import { LogRoute } from "./routes/log.js";
|
||||||
|
import { SteamLeaderboardRoute } from "./routes/steamLeaderboard.js";
|
||||||
|
|
||||||
export class ApiModule
|
export class ApiModule
|
||||||
{
|
{
|
||||||
@ -37,6 +36,8 @@ export class ApiModule
|
|||||||
router.use("/trains/", TrainsRoute.load());
|
router.use("/trains/", TrainsRoute.load());
|
||||||
router.use("/profiles/", ProfilesRoute.load());
|
router.use("/profiles/", ProfilesRoute.load());
|
||||||
router.use("/leaderboard/", LeaderboardRoute.load());
|
router.use("/leaderboard/", LeaderboardRoute.load());
|
||||||
|
router.use("/steam/leaderboard/", SteamLeaderboardRoute.load());
|
||||||
|
|
||||||
router.use("/stats/", StatsRoute.load());
|
router.use("/stats/", StatsRoute.load());
|
||||||
router.use("/log/", LogRoute.load());
|
router.use("/log/", LogRoute.load());
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ import { ApiModule } from "./http/server.js";
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import { TrainsModule } from "./modules/trains.js";
|
import { TrainsModule } from "./modules/trains.js";
|
||||||
import { Server, Station, Train } from "@simrail/types";
|
import { Server, Station, Train } from "@simrail/types";
|
||||||
import { IPlayer } from "./types/player.js";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { TMProfile } from "./mongo/profile.js";
|
||||||
|
|
||||||
;(async () =>
|
;(async () =>
|
||||||
{
|
{
|
||||||
@ -50,25 +50,25 @@ import dayjs from "dayjs";
|
|||||||
|
|
||||||
if (process.env.NODE_ENV === "development")
|
if (process.env.NODE_ENV === "development")
|
||||||
{
|
{
|
||||||
client.on(SimrailClientEvents.StationJoined, (server: Server, station: Station, player: IPlayer) =>
|
client.on(SimrailClientEvents.StationJoined, (server: Server, station: Station, player: TMProfile) =>
|
||||||
{
|
{
|
||||||
console.log(`${ server.ServerCode } | ${ station.Name } | ${ player.personaname } joined`);
|
console.log(`${ server.ServerCode } | ${ station.Name } | ${ player.username } joined`);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on(SimrailClientEvents.StationLeft, (server: Server, station: Station, player: IPlayer, joinedAt: number) =>
|
client.on(SimrailClientEvents.StationLeft, (server: Server, station: Station, player: TMProfile, joinedAt: number) =>
|
||||||
{
|
{
|
||||||
console.log(`${ server.ServerCode } | ${ station.Name } | ${ player.personaname } left. | ${ joinedAt ? dayjs(joinedAt).fromNow() : "no time data." }`);
|
console.log(`${ server.ServerCode } | ${ station.Name } | ${ player.username } left. | ${ joinedAt ? dayjs(joinedAt).fromNow() : "no time data." }`);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on(SimrailClientEvents.TrainLeft, (server: Server, train: Train, player: IPlayer, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) =>
|
client.on(SimrailClientEvents.TrainLeft, (server: Server, train: Train, player: TMProfile, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) =>
|
||||||
{
|
{
|
||||||
console.log(`${ server.ServerCode } | ${ train.TrainName } | ${ player.personaname } left. | ${ joinedAt ? dayjs(joinedAt).fromNow() : "no time data." } |
|
console.log(`${ server.ServerCode } | ${ train.TrainName } | ${ player.username } left. | ${ joinedAt ? dayjs(joinedAt).fromNow() : "no time data." } |
|
||||||
${ vehicle } | ${ distance / 1000 } | ${ points }`);
|
${ vehicle } | ${ distance / 1000 } | ${ points }`);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on(SimrailClientEvents.TrainJoined, (server: Server, train: Train, player: IPlayer, start: number) =>
|
client.on(SimrailClientEvents.TrainJoined, (server: Server, train: Train, player: TMProfile, start: number) =>
|
||||||
{
|
{
|
||||||
console.log(`${ server.ServerCode } | ${ train.TrainName } | ${ player.personaname } joined | ${ start }`);
|
console.log(`${ server.ServerCode } | ${ train.TrainName } | ${ player.username } joined | ${ start }`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,68 +15,74 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Server, Station } from "@simrail/types";
|
import { Server, Station } from "@simrail/types";
|
||||||
import { MLog } from "../mongo/logs.js";
|
import { MStationLog } from "../mongo/stationLog.js";
|
||||||
import { IPlayer } from "../types/player.js";
|
|
||||||
import { SimrailClientEvents } from "../util/SimrailClient.js";
|
import { SimrailClientEvents } from "../util/SimrailClient.js";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { MProfile } from "../mongo/profile.js";
|
import { MProfile, TMProfile } from "../mongo/profile.js";
|
||||||
import { SteamUtil } from "../util/SteamUtil.js";
|
import { PlayerUtil } from "../util/PlayerUtil.js";
|
||||||
|
import { isTruthyAndGreaterThanZero } from "../util/functions.js";
|
||||||
|
|
||||||
export class StationsModule
|
export class StationsModule
|
||||||
{
|
{
|
||||||
public static load()
|
public static load()
|
||||||
{
|
{
|
||||||
|
|
||||||
client.on(SimrailClientEvents.StationLeft, async (server: Server, station: Station, player: IPlayer, joinedAt: number) =>
|
client.on(SimrailClientEvents.StationLeft, async (server: Server, station: Station, player: TMProfile, joinedAt: number) =>
|
||||||
{
|
{
|
||||||
const stats = await SteamUtil.getPlayerStats(player.steamid);
|
const stats = await PlayerUtil.getPlayerStats(player.id);
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
if (stats)
|
|
||||||
|
if (stats && (date.getTime() - joinedAt) && (date.getTime() - joinedAt) > 0)
|
||||||
{
|
{
|
||||||
const time = (date.getTime() - joinedAt) || 0;
|
const time = (date.getTime() - joinedAt) || 0;
|
||||||
|
|
||||||
const userProfile = await MProfile.findOne({ steam: player.steamid }) ?? await MProfile.create({ steam: player.steamid, id: v4(), steamName: player.personaname });
|
if (!player.dispatcherStats) player.dispatcherStats = {};
|
||||||
if (!userProfile.dispatcherStats)
|
|
||||||
{
|
|
||||||
userProfile.dispatcherStats = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userProfile.dispatcherStats[ station.Name ])
|
if (player.dispatcherStats[ station.Name ] && isTruthyAndGreaterThanZero(player.dispatcherStats[ station.Name ].time + time))
|
||||||
{
|
{
|
||||||
userProfile.dispatcherStats[ station.Name ].time = userProfile.dispatcherStats[ station.Name ].time + time;
|
player.dispatcherStats[ station.Name ].time = player.dispatcherStats[ station.Name ].time + time;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
userProfile.dispatcherStats[ station.Name ] = {
|
player.dispatcherStats[ station.Name ] = {
|
||||||
time,
|
time,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number.isNaN(userProfile.dispatcherStats[ station.Name ].time))
|
if (isTruthyAndGreaterThanZero(player.dispatcherTime + time)) player.dispatcherTime = player.dispatcherTime + time;
|
||||||
{
|
|
||||||
userProfile.dispatcherStats[ station.Name ].time = 0;
|
player.steamTrainDistance = stats?.stats?.find(x => x.name === "DISTANCE_M")?.value ?? 0;
|
||||||
|
player.steamDispatcherTime = stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0;
|
||||||
|
player.steamTrainScore = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
|
||||||
|
|
||||||
|
player.flags = player.flags.filter(x => x !== "private");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userProfile.dispatcherTime)
|
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
||||||
{
|
|
||||||
userProfile.dispatcherTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
userProfile.dispatcherTime = userProfile.dispatcherTime + time;
|
!stats && !player.flags.includes('private') && player.flags.push("private");
|
||||||
|
|
||||||
await MProfile.findOneAndUpdate({ id: userProfile.id }, { dispatcherStats: userProfile.dispatcherStats, dispatcherTime: userProfile.dispatcherTime });
|
player.flags = [...new Set(player.flags)];
|
||||||
}
|
|
||||||
|
|
||||||
await MLog.create({
|
player.username = playerData?.personaname ?? player.username;
|
||||||
|
player.avatar = playerData?.avatarfull ?? player.avatar;
|
||||||
|
|
||||||
|
await MProfile.updateOne({ id: player.id }, player);
|
||||||
|
|
||||||
|
await MStationLog.create({
|
||||||
id: v4(),
|
id: v4(),
|
||||||
userSteamId: player.steamid,
|
|
||||||
userAvatar: player.avatarfull,
|
steam: player.id,
|
||||||
userUsername: player.personaname,
|
username: player.username,
|
||||||
|
|
||||||
joinedDate: joinedAt,
|
joinedDate: joinedAt,
|
||||||
leftDate: date.getTime(),
|
leftDate: date.getTime(),
|
||||||
stationName: station.Name,
|
stationName: station.Name,
|
||||||
stationShort: station.Prefix,
|
stationShort: station.Prefix,
|
||||||
server: server.ServerCode,
|
server: server.ServerCode,
|
||||||
|
|
||||||
|
player: player._id,
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -15,80 +15,111 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Server, Train } from "@simrail/types";
|
import { Server, Train } from "@simrail/types";
|
||||||
import { IPlayer } from "../types/player.js";
|
|
||||||
import { SimrailClientEvents } from "../util/SimrailClient.js";
|
import { SimrailClientEvents } from "../util/SimrailClient.js";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { getVehicle } from "../util/contants.js";
|
import { getVehicle } from "../util/contants.js";
|
||||||
import { MProfile } from "../mongo/profile.js";
|
import { MProfile, TMProfile } from "../mongo/profile.js";
|
||||||
import { MTrainLog } from "../mongo/trainLogs.js";
|
import { MTrainLog } from "../mongo/trainLog.js";
|
||||||
|
import { PlayerUtil } from "../util/PlayerUtil.js";
|
||||||
|
import { isTruthyAndGreaterThanZero } from "../util/functions.js";
|
||||||
|
|
||||||
export class TrainsModule
|
export class TrainsModule
|
||||||
{
|
{
|
||||||
public static load()
|
public static load()
|
||||||
{
|
{
|
||||||
|
|
||||||
client.on(SimrailClientEvents.TrainLeft, async (server: Server, train: Train, player: IPlayer, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) =>
|
client.on(SimrailClientEvents.TrainLeft, async (server: Server, train: Train, player: TMProfile, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) =>
|
||||||
{
|
{
|
||||||
if (distance)
|
const stats = await PlayerUtil.getPlayerStats(player.id);
|
||||||
|
|
||||||
|
if (stats && (leftAt - joinedAt) && (leftAt - joinedAt) > 0)
|
||||||
{
|
{
|
||||||
const time = (leftAt - joinedAt) || 0;
|
const time = (leftAt - joinedAt) || 0;
|
||||||
const userProfile = await MProfile.findOne({ steam: player.steamid }) ?? await MProfile.create({ steam: player.steamid, id: v4(), steamName: player.personaname });
|
|
||||||
|
|
||||||
const vehicleName = getVehicle(vehicle) ?? vehicle;
|
const vehicleName = getVehicle(vehicle) ?? vehicle;
|
||||||
|
|
||||||
if (!userProfile.trainStats)
|
if (!isTruthyAndGreaterThanZero(distance))
|
||||||
|
distance = 0;
|
||||||
|
|
||||||
|
if (!isTruthyAndGreaterThanZero(points))
|
||||||
|
points = 0;
|
||||||
|
|
||||||
|
|
||||||
|
if (!player.trainStats)
|
||||||
{
|
{
|
||||||
userProfile.trainStats = {};
|
player.trainStats = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userProfile.trainStats[ vehicleName ])
|
if (player.trainStats[ vehicleName ])
|
||||||
{
|
{
|
||||||
userProfile.trainStats[ vehicleName ].distance = userProfile.trainStats[ vehicleName ].distance + distance;
|
if (isTruthyAndGreaterThanZero(player.trainStats[ vehicleName ].distance + distance))
|
||||||
userProfile.trainStats[ vehicleName ].score = userProfile.trainStats[ vehicleName ].score + points;
|
{
|
||||||
userProfile.trainStats[ vehicleName ].time = userProfile.trainStats[ vehicleName ].time + time;
|
player.trainStats[ vehicleName ].distance = player.trainStats[ vehicleName ].distance + distance;
|
||||||
|
}
|
||||||
|
if (isTruthyAndGreaterThanZero(player.trainStats[ vehicleName ].score + points))
|
||||||
|
{
|
||||||
|
player.trainStats[ vehicleName ].score = player.trainStats[ vehicleName ].score + points;
|
||||||
|
}
|
||||||
|
if (isTruthyAndGreaterThanZero(player.trainStats[ vehicleName ].time + time))
|
||||||
|
{
|
||||||
|
player.trainStats[ vehicleName ].time = player.trainStats[ vehicleName ].time + time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
userProfile.trainStats[ vehicleName ] = {
|
player.trainStats[ vehicleName ] = {
|
||||||
distance, score: points, time,
|
distance, score: points, time,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userProfile.trainTime)
|
|
||||||
|
if (isTruthyAndGreaterThanZero(player.trainTime + time))
|
||||||
{
|
{
|
||||||
userProfile.trainTime = 0;
|
player.trainTime = player.trainTime + time;
|
||||||
}
|
}
|
||||||
|
|
||||||
userProfile.trainTime = userProfile.trainTime + time;
|
if (isTruthyAndGreaterThanZero(player.trainPoints + points))
|
||||||
|
|
||||||
if (!userProfile.trainPoints)
|
|
||||||
{
|
{
|
||||||
userProfile.trainPoints = 0;
|
player.trainPoints = player.trainPoints + points;
|
||||||
}
|
}
|
||||||
|
|
||||||
userProfile.trainPoints = userProfile.trainPoints + points;
|
if (isTruthyAndGreaterThanZero(player.trainDistance + distance))
|
||||||
|
|
||||||
if (!userProfile.trainDistance)
|
|
||||||
{
|
{
|
||||||
userProfile.trainDistance = 0;
|
player.trainDistance = player.trainDistance + distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
userProfile.trainDistance = userProfile.trainDistance + distance;
|
player.steamTrainDistance = stats?.stats?.find(x => x.name === "DISTANCE_M")?.value ?? 0;
|
||||||
|
player.steamDispatcherTime = stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0;
|
||||||
|
player.steamTrainScore = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
|
||||||
|
|
||||||
await MProfile.findOneAndUpdate({ id: userProfile.id }, { trainStats: userProfile.trainStats, trainTime: userProfile.trainTime, trainPoints: userProfile.trainPoints, trainDistance: userProfile.trainDistance });
|
player.flags = player.flags.filter(x => x !== "private");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
||||||
|
|
||||||
|
!stats && !player.flags.includes('private') && player.flags.push("private");
|
||||||
|
|
||||||
|
player.flags = [...new Set(player.flags)];
|
||||||
|
|
||||||
|
player.username = playerData?.personaname ?? player.username;
|
||||||
|
player.avatar = playerData?.avatarfull ?? player.avatar;
|
||||||
|
|
||||||
|
await MProfile.updateOne({ id: player.id }, player);
|
||||||
|
|
||||||
await MTrainLog.create({
|
await MTrainLog.create({
|
||||||
id: v4(),
|
id: v4(),
|
||||||
userSteamId: player.steamid,
|
|
||||||
userAvatar: player.avatarfull,
|
steam: player.id,
|
||||||
userUsername: player.personaname,
|
username: player.username,
|
||||||
|
|
||||||
joinedDate: joinedAt,
|
joinedDate: joinedAt,
|
||||||
leftDate: leftAt,
|
leftDate: leftAt,
|
||||||
trainNumber: train.TrainNoLocal,
|
trainNumber: train.TrainNoLocal,
|
||||||
server: server.ServerCode,
|
server: server.ServerCode,
|
||||||
distance, points,
|
distance, points,
|
||||||
trainName: train.TrainName,
|
trainName: train.TrainName,
|
||||||
|
|
||||||
|
player: player._id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -14,71 +14,100 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Model, model, Schema } from "mongoose";
|
import { HydratedDocument, model, Schema } from "mongoose";
|
||||||
|
|
||||||
|
|
||||||
export const raw_schema = {
|
export const raw_schema = {
|
||||||
|
// STEAM HEX
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
unique: true
|
||||||
},
|
},
|
||||||
steam: {
|
// USERNAME FROM STEAM
|
||||||
type: String,
|
username: {
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
steamName: {
|
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// AVATAR FROM STEAM
|
||||||
|
avatar: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// OBJECT WITH TRAIN STATS
|
||||||
trainStats: {
|
trainStats: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
|
// OBJECT WITH DISPATCHER STATS
|
||||||
dispatcherStats: {
|
dispatcherStats: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
|
// FULL TRAIN-TIME for easy access
|
||||||
trainTime: {
|
trainTime: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
// FULL TRAIN-SCORE for easy access
|
||||||
trainPoints: {
|
trainPoints: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
// FULL TRAIN-DISTANCE for easy access
|
||||||
trainDistance: {
|
trainDistance: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
// FULL DISPATCHER-TIME for easy access
|
||||||
dispatcherTime: {
|
dispatcherTime: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
verified: {
|
steamDispatcherTime: {
|
||||||
type: Boolean,
|
type: Number,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: 0,
|
||||||
|
},
|
||||||
|
steamTrainDistance: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
steamTrainScore: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
flags: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: []
|
||||||
}
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = new Schema<IProfile>(raw_schema);
|
const schema = new Schema<IProfile>(raw_schema);
|
||||||
|
|
||||||
|
export type TMProfile = HydratedDocument<IProfile>;
|
||||||
export type TMProfile = Model<IProfile>
|
|
||||||
|
|
||||||
export const MProfile = model<IProfile>("profile", schema);
|
export const MProfile = model<IProfile>("profile", schema);
|
||||||
|
|
||||||
export interface IProfile
|
export interface IProfile
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
steam: string;
|
username: string;
|
||||||
|
avatar: string;
|
||||||
|
|
||||||
trainStats: {
|
trainStats: {
|
||||||
[ trainName: string ]: {
|
[ trainName: string ]: {
|
||||||
score: number,
|
score: number,
|
||||||
@ -92,10 +121,16 @@ export interface IProfile
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatcherTime: number;
|
|
||||||
trainTime: number;
|
trainTime: number;
|
||||||
trainPoints: number;
|
trainPoints: number;
|
||||||
steamName: string;
|
|
||||||
trainDistance: number;
|
trainDistance: number;
|
||||||
verified: boolean;
|
|
||||||
|
dispatcherTime: number;
|
||||||
|
|
||||||
|
steamDispatcherTime: number;
|
||||||
|
steamTrainDistance: number;
|
||||||
|
steamTrainScore: number;
|
||||||
|
|
||||||
|
|
||||||
|
flags: string[]
|
||||||
}
|
}
|
@ -22,18 +22,6 @@ export const raw_schema = {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
userSteamId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
userUsername: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
userAvatar: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
joinedDate: {
|
joinedDate: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
@ -55,24 +43,40 @@ export const raw_schema = {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
steam: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
player: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "profile"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = new Schema<ILog>(raw_schema);
|
const schema = new Schema<IStationLog>(raw_schema);
|
||||||
schema.index({ stationName: "text", userUsername: "text", stationShort: "text", userSteamId: "text", server: "text" });
|
|
||||||
|
|
||||||
export type TMLog = Model<ILog>
|
export type TMStationLog = Model<IStationLog>
|
||||||
|
|
||||||
export const MLog = model<ILog>("logs", schema);
|
export const MStationLog = model<IStationLog>("stations", schema);
|
||||||
|
|
||||||
export interface ILog
|
export interface IStationLog
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
userSteamId: string;
|
|
||||||
userUsername: string;
|
|
||||||
userAvatar: string;
|
|
||||||
joinedDate?: number;
|
joinedDate?: number;
|
||||||
leftDate: number;
|
leftDate: number;
|
||||||
stationName: string;
|
stationName: string;
|
||||||
stationShort: string;
|
stationShort: string;
|
||||||
server: string;
|
server: string;
|
||||||
|
|
||||||
|
player: Schema.Types.ObjectId;
|
||||||
|
|
||||||
|
username: string;
|
||||||
|
steam: string;
|
||||||
|
|
||||||
}
|
}
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* See LICENSE for more.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Model, model, Schema } from "mongoose";
|
|
||||||
|
|
||||||
|
|
||||||
export const raw_schema = {
|
|
||||||
steam: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
personaname: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
avatarfull: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const schema = new Schema<ISteam>(raw_schema);
|
|
||||||
|
|
||||||
|
|
||||||
export type TMSteam = Model<ISteam>
|
|
||||||
|
|
||||||
export const MSteam = model<ISteam>("steam", schema);
|
|
||||||
|
|
||||||
export interface ISteam
|
|
||||||
{
|
|
||||||
steam: string;
|
|
||||||
stats: object;
|
|
||||||
personaname: string;
|
|
||||||
avatarfull: string;
|
|
||||||
lastUpdated: number;
|
|
||||||
}
|
|
@ -26,18 +26,6 @@ export const raw_schema = {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
userSteamId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
userUsername: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
userAvatar: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
joinedDate: {
|
joinedDate: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
@ -65,20 +53,29 @@ export const raw_schema = {
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
steam: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "profile"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = new Schema<ITrainLog>(raw_schema);
|
const schema = new Schema<ITrainLog>(raw_schema);
|
||||||
|
|
||||||
export type TMTrainLog = Model<ITrainLog>
|
export type TMTrainLog = Model<ITrainLog>
|
||||||
|
|
||||||
export const MTrainLog = model<ITrainLog>("train_logs", schema);
|
export const MTrainLog = model<ITrainLog>("trains", schema);
|
||||||
|
|
||||||
export interface ITrainLog
|
export interface ITrainLog
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
userSteamId: string;
|
|
||||||
userUsername: string;
|
|
||||||
userAvatar: string;
|
|
||||||
joinedDate?: number;
|
joinedDate?: number;
|
||||||
leftDate: number;
|
leftDate: number;
|
||||||
trainNumber: string;
|
trainNumber: string;
|
||||||
@ -86,4 +83,10 @@ export interface ITrainLog
|
|||||||
distance: number;
|
distance: number;
|
||||||
points: number;
|
points: number;
|
||||||
server: string;
|
server: string;
|
||||||
|
|
||||||
|
player: Schema.Types.ObjectId;
|
||||||
|
|
||||||
|
username: string;
|
||||||
|
steam: string;
|
||||||
|
|
||||||
}
|
}
|
@ -15,18 +15,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IPlayerPayload, IPlayerStatsPayload } from "../types/player.js";
|
import { IPlayerPayload, IPlayerStatsPayload } from "../types/player.js";
|
||||||
|
import { MProfile } from "../mongo/profile.js";
|
||||||
|
import { assert } from "node:console";
|
||||||
|
|
||||||
const STEAM_API_KEY = process.env.STEAM_APIKEY;
|
const STEAM_API_KEY = process.env.STEAM_APIKEY;
|
||||||
|
|
||||||
const fetchFuckingSteamApi = (url: string) =>
|
const steamFetch = (url: string, maxRetries: number = 5) =>
|
||||||
{
|
{
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
|
|
||||||
return new Promise((res, rej) =>
|
return new Promise((res, _rej) =>
|
||||||
{
|
{
|
||||||
const req = () =>
|
const req = () =>
|
||||||
{
|
{
|
||||||
if (retries > 5)
|
if (retries > maxRetries)
|
||||||
{
|
{
|
||||||
throw new Error("request failed to api steam");
|
throw new Error("request failed to api steam");
|
||||||
}
|
}
|
||||||
@ -47,25 +49,78 @@ const fetchFuckingSteamApi = (url: string) =>
|
|||||||
|
|
||||||
export class PlayerUtil
|
export class PlayerUtil
|
||||||
{
|
{
|
||||||
public static async getPlayer(steamId: number | string)
|
public static async ensurePlayer(steamId: number | string)
|
||||||
{
|
{
|
||||||
const data = await fetchFuckingSteamApi(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${ STEAM_API_KEY }&format=json&steamids=${ steamId }`) as IPlayerPayload;
|
assert(steamId, "expected steamId to be a string or a number");
|
||||||
|
steamId = steamId.toString();
|
||||||
|
let player = await MProfile.findOne({ id: steamId });
|
||||||
|
|
||||||
if (!data.response.players)
|
if (!player)
|
||||||
{
|
{
|
||||||
return;
|
const data = await steamFetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${ STEAM_API_KEY }&format=json&steamids=${ steamId }`, 20) as IPlayerPayload;
|
||||||
|
|
||||||
|
assert(data.response.players, "Expected data.response.players to be truthy")
|
||||||
|
|
||||||
|
const stats = await this.getPlayerStats(steamId);
|
||||||
|
|
||||||
|
player = await MProfile.findOne({ id: steamId });
|
||||||
|
if (player) return player;
|
||||||
|
|
||||||
|
player = await MProfile.create({
|
||||||
|
id: steamId,
|
||||||
|
username: data.response.players[ 0 ].personaname,
|
||||||
|
avatar: data.response.players[ 0 ].avatarfull,
|
||||||
|
|
||||||
|
steamDispatcherTime: stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0,
|
||||||
|
steamTrainScore: stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0,
|
||||||
|
steamTrainDistance: stats?.stats?.find(x => x.name === "DISTANCE_M")?.value ?? 0,
|
||||||
|
|
||||||
|
trainStats: {},
|
||||||
|
dispatcherStats: {},
|
||||||
|
|
||||||
|
trainPoints: 0,
|
||||||
|
trainDistance: 0,
|
||||||
|
trainTime: 0,
|
||||||
|
|
||||||
|
dispatcherTime: 0,
|
||||||
|
|
||||||
|
flags: !stats ? [ 'private' ] : []
|
||||||
|
}).catch(e => e);
|
||||||
|
|
||||||
|
if (player instanceof Error)
|
||||||
|
player = await MProfile.findOne({ id: steamId });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(player, "expected player to be truthy");
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getPlayer(steamId: string) {
|
||||||
|
const player = await MProfile.findOne({ id: steamId });
|
||||||
|
|
||||||
|
if (!player) return undefined;
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getPlayerSteamData(steamId: string) {
|
||||||
|
const data = await steamFetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${ STEAM_API_KEY }&format=json&steamids=${ steamId }`, 5) as IPlayerPayload;
|
||||||
|
|
||||||
|
if (!data?.response?.players?.length)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
return data.response.players[0];
|
return data.response.players[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getPlayerStats(steamId: string)
|
public static async getPlayerStats(steamId: string)
|
||||||
{
|
{
|
||||||
const data = await fetchFuckingSteamApi(`http://api.steampowered.com/ISteamUserStats/GetUserStatsForGame/v0002/?appid=1422130&key=${ STEAM_API_KEY }&steamid=${ steamId }`) as IPlayerStatsPayload;
|
const data = await steamFetch(`https://api.steampowered.com/ISteamUserStats/GetUserStatsForGame/v0002/?appid=1422130&key=${ STEAM_API_KEY }&steamid=${ steamId }`) as IPlayerStatsPayload;
|
||||||
|
|
||||||
|
|
||||||
if (!data.playerstats?.stats)
|
if (!data.playerstats?.stats)
|
||||||
{
|
{
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
return data.playerstats;
|
return data.playerstats;
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
|
|
||||||
import { IPlayer } from "../types/player.js";
|
|
||||||
import { PlayerUtil } from "./PlayerUtil.js";
|
import { PlayerUtil } from "./PlayerUtil.js";
|
||||||
import { Station, ApiResponse, Server, Train } from "@simrail/types";
|
import { Station, ApiResponse, Server, Train } from "@simrail/types";
|
||||||
|
import { TMProfile } from "../mongo/profile.js";
|
||||||
|
|
||||||
export enum SimrailClientEvents
|
export enum SimrailClientEvents
|
||||||
{
|
{
|
||||||
@ -31,14 +30,14 @@ export enum SimrailClientEvents
|
|||||||
|
|
||||||
export declare interface SimrailClient
|
export declare interface SimrailClient
|
||||||
{
|
{
|
||||||
on(event: SimrailClientEvents.StationJoined, listener: (server: Server, station: Station, player: IPlayer) => void): this;
|
on(event: SimrailClientEvents.StationJoined, listener: (server: Server, station: Station, player: TMProfile) => void): this;
|
||||||
|
|
||||||
on(event: SimrailClientEvents.StationLeft, listener: (server: Server, station: Station, player: IPlayer, joinedAt: number) => void): this;
|
on(event: SimrailClientEvents.StationLeft, listener: (server: Server, station: Station, player: TMProfile, joinedAt: number) => void): this;
|
||||||
|
|
||||||
|
|
||||||
on(event: SimrailClientEvents.TrainJoined, listener: (server: Server, train: Train, player: IPlayer, startDistance: number) => void): this;
|
on(event: SimrailClientEvents.TrainJoined, listener: (server: Server, train: Train, player: TMProfile, startDistance: number) => void): this;
|
||||||
|
|
||||||
on(event: SimrailClientEvents.TrainLeft, listener: (server: Server, train: Train, player: IPlayer, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) => void): this;
|
on(event: SimrailClientEvents.TrainLeft, listener: (server: Server, train: Train, player: TMProfile, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) => void): this;
|
||||||
|
|
||||||
//on(event: string, listener: Function): this;
|
//on(event: string, listener: Function): this;
|
||||||
}
|
}
|
||||||
@ -66,30 +65,14 @@ export class SimrailClient extends EventEmitter
|
|||||||
public constructor()
|
public constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.setup();
|
this.setup().then(() => {
|
||||||
setTimeout(() => setInterval(() => this.update(), 500), 1000);
|
void this.update();
|
||||||
}
|
});
|
||||||
|
|
||||||
public getStation(server: Server["ServerCode"], name: string)
|
|
||||||
{
|
|
||||||
if (!this.stationsOccupied[ server ] || !this.stationsOccupied[ server ][ name ])
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const player = PlayerUtil.getPlayer(this.stationsOccupied[ server ][ name ]?.SteamId!);
|
|
||||||
return { player, joinedAt: this.stationsOccupied[ name ].joinedAt };
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTrain(server: Server["ServerCode"], name: string)
|
|
||||||
{
|
|
||||||
if (!this.trainsOccupied[ server ] || !this.trainsOccupied[ server ][ name ])
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const player = PlayerUtil.getPlayer(this.trainsOccupied[ server ][ name ]?.SteamId!);
|
|
||||||
return { player, joinedAt: this.trainsOccupied[ server ][ name ]?.JoinedAt, startPlayerDistance: this.trainsOccupied[ server ][ name ]?.StartPlayerDistance };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: full rewrite, rewrite db structure with option to join log to user profile, check for negative values in user profile
|
||||||
|
// todo: wipe database 13.12.2024
|
||||||
|
|
||||||
private async setup()
|
private async setup()
|
||||||
{
|
{
|
||||||
@ -161,7 +144,8 @@ export class SimrailClient extends EventEmitter
|
|||||||
{
|
{
|
||||||
// join
|
// join
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const player = await PlayerUtil.getPlayer(x.DispatchedBy[ 0 ]?.SteamId);
|
|
||||||
|
const player = await PlayerUtil.ensurePlayer(x.DispatchedBy[ 0 ]?.SteamId);
|
||||||
|
|
||||||
this.emit(SimrailClientEvents.StationJoined, server, x, player);
|
this.emit(SimrailClientEvents.StationJoined, server, x, player);
|
||||||
this.stationsOccupied[ server.ServerCode ][ data.Prefix ] = {
|
this.stationsOccupied[ server.ServerCode ][ data.Prefix ] = {
|
||||||
@ -172,7 +156,7 @@ export class SimrailClient extends EventEmitter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// leave
|
// leave
|
||||||
const player = await PlayerUtil.getPlayer(data.DispatchedBy[ 0 ]?.SteamId);
|
const player = await PlayerUtil.ensurePlayer(data.DispatchedBy[ 0 ]?.SteamId);
|
||||||
|
|
||||||
this.emit(SimrailClientEvents.StationLeft, server, x, player, this.stationsOccupied[ server.ServerCode ][ data.Prefix ]?.JoinedAt);
|
this.emit(SimrailClientEvents.StationLeft, server, x, player, this.stationsOccupied[ server.ServerCode ][ data.Prefix ]?.JoinedAt);
|
||||||
delete this.stationsOccupied[ server.ServerCode ][ data.Prefix ];
|
delete this.stationsOccupied[ server.ServerCode ][ data.Prefix ];
|
||||||
@ -228,7 +212,7 @@ export class SimrailClient extends EventEmitter
|
|||||||
// join
|
// join
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|
||||||
const player = await PlayerUtil.getPlayer(x.TrainData.ControlledBySteamID!);
|
const player = await PlayerUtil.ensurePlayer(x.TrainData.ControlledBySteamID!);
|
||||||
|
|
||||||
const playerStats = await PlayerUtil.getPlayerStats(x.TrainData.ControlledBySteamID!);
|
const playerStats = await PlayerUtil.getPlayerStats(x.TrainData.ControlledBySteamID!);
|
||||||
|
|
||||||
@ -250,35 +234,42 @@ export class SimrailClient extends EventEmitter
|
|||||||
}
|
}
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|
||||||
const player = await PlayerUtil.getPlayer(data.TrainData.ControlledBySteamID!);
|
const player = await PlayerUtil.ensurePlayer(data.TrainData.ControlledBySteamID!);
|
||||||
|
|
||||||
const playerId = data.TrainData.ControlledBySteamID!;
|
const playerId = data.TrainData.ControlledBySteamID!;
|
||||||
const trainOccupied = this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ] && JSON.parse(JSON.stringify(this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ])) || null;
|
const trainOccupied = this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ] && JSON.parse(JSON.stringify(this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ])) || null;
|
||||||
|
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
PlayerUtil.getPlayerStats(playerId).then(playerStats =>
|
PlayerUtil.getPlayerStats(playerId).then(playerStats =>
|
||||||
{
|
{
|
||||||
const oldKm = trainOccupied?.StartPlayerDistance ?? 0;
|
const oldKm = trainOccupied?.StartPlayerDistance ?? 0;
|
||||||
|
|
||||||
const distance = oldKm ? (playerStats?.stats.find(x => x.name === "DISTANCE_M")?.value ?? 0) - oldKm : 0;
|
let distance = oldKm ? (playerStats?.stats.find(x => x.name === "DISTANCE_M")?.value ?? 0) - oldKm : 0;
|
||||||
|
|
||||||
const oldPoints = trainOccupied?.StartPlayerPoints ?? 0;
|
const oldPoints = trainOccupied?.StartPlayerPoints ?? 0;
|
||||||
|
|
||||||
const points = oldPoints ? (playerStats?.stats.find(x => x.name === "SCORE")?.value ?? 0) - oldPoints : 0;
|
let points = oldPoints ? (playerStats?.stats.find(x => x.name === "SCORE")?.value ?? 0) - oldPoints : 0;
|
||||||
|
|
||||||
|
|
||||||
|
if (distance < 0) {
|
||||||
|
console.warn(`Player ${playerId}, Train ${data.TrainNoLocal} - distance < 0`);
|
||||||
|
distance = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points < 0) {
|
||||||
|
console.warn(`Player ${playerId}, Train ${data.TrainNoLocal} - distance < 0`);
|
||||||
|
points = 0;
|
||||||
|
}
|
||||||
|
|
||||||
this.emit(SimrailClientEvents.TrainLeft, server, data, player, trainOccupied?.JoinedAt, date.getTime(), points, distance, x.Vehicles[ 0 ]);
|
this.emit(SimrailClientEvents.TrainLeft, server, data, player, trainOccupied?.JoinedAt, date.getTime(), points, distance, x.Vehicles[ 0 ]);
|
||||||
});
|
});
|
||||||
}, 80_000);
|
}, 30_000);
|
||||||
delete this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ];
|
delete this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ];
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.trains[ server.ServerCode ] = trains.data;
|
this.trains[ server.ServerCode ] = trains.data;
|
||||||
redis.json.set("trains", "$", this.trains);
|
redis.json.set("trains", "$", this.trains);
|
||||||
redis.json.set("trains_occupied", "$", this.trainsOccupied);
|
redis.json.set("trains_occupied", "$", this.trainsOccupied);
|
||||||
@ -289,11 +280,12 @@ export class SimrailClient extends EventEmitter
|
|||||||
private async update()
|
private async update()
|
||||||
{
|
{
|
||||||
const servers = (await fetch("https://panel.simrail.eu:8084/servers-open").then(x => x.json()).catch(() => ({ data: [], result: false })) as ApiResponse<Server>)
|
const servers = (await fetch("https://panel.simrail.eu:8084/servers-open").then(x => x.json()).catch(() => ({ data: [], result: false })) as ApiResponse<Server>)
|
||||||
.data?.filter(x => x.ServerName.includes("Polski")) ?? []; // TODO: remove this in v3
|
.data ?? [] //?.filter(x => x.ServerName.includes("Polski")) ?? []; // TODO: remove this in v3
|
||||||
|
|
||||||
if (!servers.length)
|
if (!servers.length)
|
||||||
{
|
{
|
||||||
console.log("SimrailAPI is down");
|
console.log("SimrailAPI is down");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: maybe node:worker_threads?
|
// TODO: maybe node:worker_threads?
|
||||||
@ -303,10 +295,10 @@ export class SimrailClient extends EventEmitter
|
|||||||
const stations = (await fetch('https://panel.simrail.eu:8084/stations-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Station>;
|
const stations = (await fetch('https://panel.simrail.eu:8084/stations-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Station>;
|
||||||
const trains = (await fetch('https://panel.simrail.eu:8084/trains-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Train>;
|
const trains = (await fetch('https://panel.simrail.eu:8084/trains-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Train>;
|
||||||
|
|
||||||
void this.processStation(server, stations);
|
await this.processStation(server, stations);
|
||||||
void this.processTrain(server, trains);
|
await this.processTrain(server, trains);
|
||||||
|
}
|
||||||
|
await new Promise(res => setTimeout(res, 1000));
|
||||||
}
|
await this.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* See LICENSE for more.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MSteam } from "../mongo/steam.js";
|
|
||||||
import { PlayerUtil } from "./PlayerUtil.js";
|
|
||||||
|
|
||||||
export class SteamUtil
|
|
||||||
{
|
|
||||||
public static async updatePlayerDataInDatabase(steam: string)
|
|
||||||
{
|
|
||||||
const data = await PlayerUtil.getPlayer(steam);
|
|
||||||
const stats = await PlayerUtil.getPlayerStats(steam);
|
|
||||||
const steamApi = await MSteam.findOne({ steam });
|
|
||||||
|
|
||||||
if (steamApi)
|
|
||||||
{
|
|
||||||
await MSteam.findOneAndUpdate({ steam }, { stats, personaname: data?.personaname, avatarfull: data?.avatarfull });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await MSteam.create({ steam, stats, personaname: data?.personaname, avatarfull: data?.avatarfull });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static async getPlayer(steam: string)
|
|
||||||
{
|
|
||||||
const steamApi = await MSteam.findOne({ steam });
|
|
||||||
void this.updatePlayerDataInDatabase(steam);
|
|
||||||
|
|
||||||
if (steamApi)
|
|
||||||
{
|
|
||||||
return { personname: steamApi.personaname, avatarfull: steamApi.avatarfull };
|
|
||||||
}
|
|
||||||
|
|
||||||
return await PlayerUtil.getPlayer(steam);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getPlayerStats(steam: string)
|
|
||||||
{
|
|
||||||
const steamApi = await MSteam.findOne({ steam });
|
|
||||||
void this.updatePlayerDataInDatabase(steam);
|
|
||||||
|
|
||||||
if (steamApi)
|
|
||||||
{
|
|
||||||
return steamApi.stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await PlayerUtil.getPlayerStats(steam);
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,3 +29,8 @@ export const removeProperties = <T>(data: any, names: string[]) =>
|
|||||||
export const escapeRegexString = (str: string) => {
|
export const escapeRegexString = (str: string) => {
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isTruthyAndGreaterThanZero = (data: number) => {
|
||||||
|
if (!data) return false;
|
||||||
|
return data > 0;
|
||||||
|
}
|
3560
packages/backend/yarn-error.log
Normal file
3560
packages/backend/yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,7 @@
|
|||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"react-toastify": "^10.0.6",
|
"react-toastify": "^10.0.6",
|
||||||
"sort-by": "^0.0.2",
|
"sort-by": "^0.0.2",
|
||||||
|
"swr": "^2.2.5",
|
||||||
"use-debounce": "^10.0.4"
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -32,6 +32,8 @@ import { ToastContainer } from "react-toastify";
|
|||||||
import useColorMode from "./hooks/useColorMode.tsx";
|
import useColorMode from "./hooks/useColorMode.tsx";
|
||||||
import { HelmetProvider } from "react-helmet-async";
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
import { PageMeta } from "./components/mini/util/PageMeta.tsx";
|
import { PageMeta } from "./components/mini/util/PageMeta.tsx";
|
||||||
|
import { SteamStationLeaderboard } from "./pages/steamLeaderboard/SteamStationsLeaderboard.tsx";
|
||||||
|
import { SteamTrainLeaderboard } from "./pages/steamLeaderboard/SteamTrainLeaderboard.tsx";
|
||||||
|
|
||||||
function App()
|
function App()
|
||||||
{
|
{
|
||||||
@ -122,6 +124,29 @@ function App()
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/leaderboard/steam/trains"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<PageMeta title="simrail.alekswilc.dev | Steam Train Leaderboard"
|
||||||
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
|
<SteamTrainLeaderboard/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/leaderboard/steam/stations"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<PageMeta title="simrail.alekswilc.dev | Steam Station Leaderboard"
|
||||||
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
|
<SteamStationLeaderboard/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/profile/:id"
|
path="/profile/:id"
|
||||||
element={
|
element={
|
||||||
|
@ -37,7 +37,7 @@ export const ArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
|||||||
export const FlexArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
export const FlexArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
||||||
<svg
|
<svg
|
||||||
className={ `fill-current ${
|
className={ `fill-current ${
|
||||||
rotated && "rotate-180"
|
!rotated && "rotate-180"
|
||||||
}` }
|
}` }
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
|
@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx";
|
import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx";
|
||||||
import { ArrowIcon } from "../icons/ArrowIcon.tsx";
|
import { ArrowIcon } from "../icons/ArrowIcon.tsx";
|
||||||
import { FaHome, FaClipboardList } from "react-icons/fa";
|
import { FaHome, FaClipboardList } from "react-icons/fa";
|
||||||
import { FaChartSimple, FaTrain, FaBuildingFlag } from "react-icons/fa6";
|
import { FaChartSimple, FaTrain, FaBuildingFlag, FaSteam } from "react-icons/fa6";
|
||||||
|
|
||||||
interface SidebarProps
|
interface SidebarProps
|
||||||
{
|
{
|
||||||
@ -219,7 +219,7 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
to="#"
|
to="#"
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||||
(pathname === "/leaderboard" ||
|
(pathname === "/leaderboard" ||
|
||||||
pathname.includes("leaderboard")) &&
|
(pathname.includes("leaderboard/") && !pathname.includes('leaderboard/steam'))) &&
|
||||||
"bg-graydark dark:bg-meta-4"
|
"bg-graydark dark:bg-meta-4"
|
||||||
}` }
|
}` }
|
||||||
onClick={ (e) =>
|
onClick={ (e) =>
|
||||||
@ -271,6 +271,71 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
} }
|
} }
|
||||||
</SidebarLinkGroup>
|
</SidebarLinkGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<SidebarLinkGroup
|
||||||
|
activeCondition={
|
||||||
|
pathname === "/leaderboard/steam" || pathname.includes("leaderboard/steam")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ (handleClick, open) =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<NavLink
|
||||||
|
to="#"
|
||||||
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||||
|
(pathname === "/leaderboard/steam" ||
|
||||||
|
pathname.includes("leaderboard/steam")) &&
|
||||||
|
"bg-graydark dark:bg-meta-4"
|
||||||
|
}` }
|
||||||
|
onClick={ (e) =>
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
sidebarExpanded
|
||||||
|
? handleClick()
|
||||||
|
: setSidebarExpanded(true);
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
<FaSteam/>
|
||||||
|
{ t("sidebar.steam_leaderboard") }
|
||||||
|
<ArrowIcon rotated={ open }/>
|
||||||
|
</NavLink>
|
||||||
|
<div
|
||||||
|
className={ `translate transform overflow-hidden ${
|
||||||
|
!open && "hidden"
|
||||||
|
}` }
|
||||||
|
>
|
||||||
|
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/leaderboard/steam/stations"
|
||||||
|
className={ ({ isActive }) =>
|
||||||
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
|
(isActive && "!text-white")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaBuildingFlag/>
|
||||||
|
{ t("sidebar.stations") }
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/leaderboard/steam/trains"
|
||||||
|
className={ ({ isActive }) =>
|
||||||
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
|
(isActive && "!text-white")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaTrain/>
|
||||||
|
{ t("sidebar.trains") }
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</SidebarLinkGroup>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import React from "react";
|
|||||||
interface CardDataStatsProps
|
interface CardDataStatsProps
|
||||||
{
|
{
|
||||||
title: string;
|
title: string;
|
||||||
total: string;
|
total: string | number;
|
||||||
rate?: string;
|
rate?: string;
|
||||||
levelUp?: boolean;
|
levelUp?: boolean;
|
||||||
levelDown?: boolean;
|
levelDown?: boolean;
|
||||||
|
@ -17,21 +17,15 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
|
||||||
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
|
||||||
import { formatTime } from "../../../util/time.ts";
|
import { formatTime } from "../../../util/time.ts";
|
||||||
import { FaCheck } from 'react-icons/fa6';
|
import { FaCheck } from "react-icons/fa6";
|
||||||
|
|
||||||
export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord[], error: number }) =>
|
export const StationTable = ({ stations }: { stations: TLeaderboardRecord[] }) =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
|
||||||
{ error === 0 && <ContentLoader/> }
|
|
||||||
{ error === 1 && <div
|
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
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="flex flex-col">
|
||||||
@ -63,8 +57,9 @@ export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord
|
|||||||
>
|
>
|
||||||
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + station.steam }
|
<Link to={ "/profile/" + station.id }
|
||||||
className="color-orchid">{ station.steamName }</Link> { station.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
className="color-orchid">{ station.username }</Link> { station.flags.includes("verified") &&
|
||||||
|
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -74,7 +69,7 @@ export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord
|
|||||||
|
|
||||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
<Link
|
<Link
|
||||||
to={ "/profile/" + station.steam }
|
to={ "/profile/" + 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"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{ t("leaderboard.profile") }
|
{ t("leaderboard.profile") }
|
||||||
@ -83,8 +78,8 @@ export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord
|
|||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div>
|
||||||
</>
|
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,16 +17,13 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
|
||||||
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
|
||||||
import { formatTime } from "../../../util/time.ts";
|
import { formatTime } from "../../../util/time.ts";
|
||||||
import { FaCheck } from "react-icons/fa6";
|
import { FaCheck } from "react-icons/fa6";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
import { FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
import { FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||||
|
|
||||||
export const TrainTable = ({ trains, error, setSortBy, sortBy }: {
|
export const TrainTable = ({ trains, setSortBy, sortBy }: {
|
||||||
trains: TLeaderboardRecord[],
|
trains: TLeaderboardRecord[],
|
||||||
error: number,
|
|
||||||
setSortBy: Dispatch<SetStateAction<string>>
|
setSortBy: Dispatch<SetStateAction<string>>
|
||||||
sortBy: string
|
sortBy: string
|
||||||
}) =>
|
}) =>
|
||||||
@ -35,11 +32,7 @@ export const TrainTable = ({ trains, error, setSortBy, sortBy }: {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
|
||||||
{ error === 0 && <ContentLoader/> }
|
|
||||||
{ error === 1 && <div
|
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
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="flex flex-col">
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-5">
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-5">
|
||||||
@ -53,21 +46,21 @@ export const TrainTable = ({ trains, error, setSortBy, sortBy }: {
|
|||||||
onClick={ () => setSortBy("distance") }>
|
onClick={ () => setSortBy("distance") }>
|
||||||
{ t("leaderboard.distance") }
|
{ t("leaderboard.distance") }
|
||||||
</h5>
|
</h5>
|
||||||
<FlexArrowIcon rotated={ !(sortBy === "distance") }/>
|
<FlexArrowIcon rotated={ sortBy === "distance" || !sortBy }/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||||
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||||
onClick={ () => setSortBy("points") }>
|
onClick={ () => setSortBy("points") }>
|
||||||
{ t("leaderboard.points") }
|
{ t("leaderboard.points") }
|
||||||
</h5>
|
</h5>
|
||||||
<FlexArrowIcon rotated={ !(sortBy === "points") }/>
|
<FlexArrowIcon rotated={ sortBy === "points" }/>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||||
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||||
onClick={ () => setSortBy("time") }>
|
onClick={ () => setSortBy("time") }>
|
||||||
{ t("leaderboard.time") }
|
{ t("leaderboard.time") }
|
||||||
</h5>
|
</h5>
|
||||||
<FlexArrowIcon rotated={ !(sortBy === "time") }/>
|
<FlexArrowIcon rotated={ sortBy === "time" }/>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
@ -86,8 +79,8 @@ export const TrainTable = ({ trains, error, setSortBy, sortBy }: {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + train.steam }
|
<Link to={ "/profile/" + train.id }
|
||||||
className="color-orchid">{ train.steamName }</Link> { train.verified &&
|
className="color-orchid">{ train.username }</Link> { train.flags.includes("verified") &&
|
||||||
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -106,7 +99,7 @@ export const TrainTable = ({ trains, error, setSortBy, sortBy }: {
|
|||||||
|
|
||||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
<Link
|
<Link
|
||||||
to={ "/profile/" + train.steam }
|
to={ "/profile/" + 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"
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
>
|
>
|
||||||
{ t("leaderboard.profile") }
|
{ t("leaderboard.profile") }
|
||||||
@ -115,8 +108,7 @@ export const TrainTable = ({ trains, error, setSortBy, sortBy }: {
|
|||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div>
|
||||||
|
)
|
||||||
</>
|
;
|
||||||
);
|
|
||||||
};
|
};
|
@ -36,7 +36,7 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
|||||||
toast.info(t("log.toasts.report"), {
|
toast.info(t("log.toasts.report"), {
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
});
|
});
|
||||||
void navigator.clipboard.writeText(`;user: \`${ data.userUsername }\`\n;steam: \`${ data.userSteamId }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;station: \`${ data.stationName }\`\n;link: ${ location.href }\n\n`);
|
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`${ data.player.avatar }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;station: \`${ data.stationName }\`\n;link: ${ location.href }\n\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
@ -45,12 +45,12 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
|||||||
<div
|
<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">
|
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">
|
<div className="relative drop-shadow-2">
|
||||||
<img className="rounded-full" src={ data.userAvatar } alt="profile"/>
|
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{ data.userUsername }{ data.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
{ data.player.username }{ data.player.flags.includes('verified') && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -84,9 +84,9 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
|||||||
{ t("log.buttons.copy") }
|
{ t("log.buttons.copy") }
|
||||||
</a>
|
</a>
|
||||||
<Link
|
<Link
|
||||||
to={"/profile/" + data.userSteamId}
|
to={"/profile/" + data.player.id}
|
||||||
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-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10 ${ data.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
>
|
style={ data.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>
|
||||||
|
|
||||||
{ t("log.buttons.profile") }
|
{ t("log.buttons.profile") }
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -37,7 +37,7 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
|||||||
toast.info(t("log.toasts.report"), {
|
toast.info(t("log.toasts.report"), {
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
});
|
});
|
||||||
void navigator.clipboard.writeText(`;user: \`${ data.userUsername }\`\n;steam: \`${ data.userSteamId }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;train: \`${ data.trainNumber }\`\n;link: ${ location.href }\n\n`);
|
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`${ data.player.id }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;train: \`${ data.trainNumber }\`\n;link: ${ location.href }\n\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
@ -46,12 +46,12 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
|||||||
<div
|
<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">
|
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">
|
<div className="relative drop-shadow-2">
|
||||||
<img className="rounded-full" src={ data.userAvatar } alt="profile"/>
|
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{ data.userUsername } { data.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
{ data.player.username } { data.player.flags.includes('verified') && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -89,8 +89,9 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
|||||||
{ t("log.buttons.copy") }
|
{ t("log.buttons.copy") }
|
||||||
</a>
|
</a>
|
||||||
<Link
|
<Link
|
||||||
to={ "/profile/" + data.userSteamId }
|
to={ "/profile/" + data.player.id }
|
||||||
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-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10 ${ data.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
|
style={ data.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||||
>
|
>
|
||||||
|
|
||||||
{ t("log.buttons.profile") }
|
{ t("log.buttons.profile") }
|
||||||
|
@ -17,26 +17,21 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
|
||||||
import { TStationRecord } from "../../../types/station.ts";
|
import { TStationRecord } from "../../../types/station.ts";
|
||||||
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
|
||||||
import { FaCheck } from 'react-icons/fa6';
|
import { FaCheck } from "react-icons/fa6";
|
||||||
|
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const StationTable = ({ stations, error }: {
|
export const StationTable = ({ stations }: {
|
||||||
stations: TStationRecord[], error: number
|
stations: TStationRecord[]
|
||||||
}) =>
|
}) =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
|
||||||
{ error === 0 && <ContentLoader/> }
|
|
||||||
|
|
||||||
{ error === 1 && <div
|
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
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="flex flex-col">
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
||||||
@ -72,8 +67,9 @@ export const StationTable = ({ stations, error }: {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<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">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + station.userSteamId }
|
<Link to={ "/profile/" + (station.steam ?? station.player.id) }
|
||||||
className="color-orchid">{ station.userUsername }</Link> { station.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
className="color-orchid">{ station.username ?? station.player.username }</Link> { station.player.flags.includes("verified") &&
|
||||||
|
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -88,8 +84,9 @@ export const StationTable = ({ stations, error }: {
|
|||||||
<div
|
<div
|
||||||
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
||||||
<Link
|
<Link
|
||||||
to={ "/profile/" + station.userSteamId }
|
to={ "/profile/" + (station.steam ?? station.player.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"
|
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 ${ station.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
|
style={ station.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||||
>
|
>
|
||||||
{ t("logs.profile") }
|
{ t("logs.profile") }
|
||||||
</Link>
|
</Link>
|
||||||
@ -103,7 +100,6 @@ export const StationTable = ({ stations, error }: {
|
|||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -18,23 +18,17 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TTrainRecord } from "../../../types/train.ts";
|
import { TTrainRecord } from "../../../types/train.ts";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
import { FaCheck } from "react-icons/fa6";
|
||||||
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
|
||||||
import { FaCheck } from 'react-icons/fa6';
|
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const TrainTable = ({ trains, error }: {
|
export const TrainTable = ({ trains }: {
|
||||||
trains: TTrainRecord[], error: number
|
trains: TTrainRecord[]
|
||||||
}) =>
|
}) =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
{ error === 2 && <WarningAlert title={ t("content_loader.notfound.header") }
|
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
|
||||||
{ error === 0 && <ContentLoader/> }
|
|
||||||
{ error === 1 && <div
|
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
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="flex flex-col">
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-6">
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-6">
|
||||||
@ -80,8 +74,9 @@ export const TrainTable = ({ trains, error }: {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<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">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + train.userSteamId }
|
<Link to={ "/profile/" + (train.steam ?? train.player.id) }
|
||||||
className="color-orchid">{ train.userUsername }</Link> { train.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
className="color-orchid">{ train.username ?? train.player.username }</Link> { train.player.flags.includes("verified") &&
|
||||||
|
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -104,8 +99,9 @@ export const TrainTable = ({ trains, error }: {
|
|||||||
<div
|
<div
|
||||||
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
||||||
<Link
|
<Link
|
||||||
to={ "/profile/" + train.userSteamId }
|
to={ "/profile/" + (train.steam ?? train.player.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"
|
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 ${ train.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
|
style={ train.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||||
>
|
>
|
||||||
{ t("logs.profile") }
|
{ t("logs.profile") }
|
||||||
</Link>
|
</Link>
|
||||||
@ -119,7 +115,6 @@ export const TrainTable = ({ trains, error }: {
|
|||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -35,12 +35,12 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
<div
|
<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">
|
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">
|
<div className="relative drop-shadow-2">
|
||||||
<img className="rounded-full" src={ data.steam.avatarfull } alt="profile"/>
|
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{ data.steam.personname } { data.player.verified &&
|
{ data.player.username } { data.player.flags.includes('verified') &&
|
||||||
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@ -83,21 +83,21 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{ t("profile.trains.distance") }
|
{ t("profile.trains.distance") }
|
||||||
</h5>
|
</h5>
|
||||||
<FlexArrowIcon rotated={ !(sortTrainsBy === "distance") }/>
|
<FlexArrowIcon rotated={ sortTrainsBy === "distance" || !sortTrainsBy }/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
||||||
onClick={ () => setSortTrainsBy("score") }>
|
onClick={ () => setSortTrainsBy("score") }>
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{ t("profile.trains.points") }
|
{ t("profile.trains.points") }
|
||||||
</h5>
|
</h5>
|
||||||
<FlexArrowIcon rotated={ !(sortTrainsBy === "score") }/>
|
<FlexArrowIcon rotated={ sortTrainsBy === "score" }/>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
||||||
onClick={ () => setSortTrainsBy("time") }>
|
onClick={ () => setSortTrainsBy("time") }>
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{ t("profile.trains.time") }
|
{ t("profile.trains.time") }
|
||||||
</h5>
|
</h5>
|
||||||
<FlexArrowIcon rotated={ !(sortTrainsBy === "time") }/>
|
<FlexArrowIcon rotated={ sortTrainsBy === "time" }/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={ `grid grid-cols-3 sm:grid-cols-4 border-t border-t-stroke dark:border-t-strokedark` }
|
className={ `grid grid-cols-3 sm:grid-cols-4 border-t border-t-stroke dark:border-t-strokedark` }
|
||||||
key={ 1 }
|
key={ trainName }
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<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">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
@ -137,7 +137,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
<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) }>
|
<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>
|
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.stations.header") }</h1>
|
||||||
<ArrowIcon rotated={ showTrains }/>
|
<ArrowIcon rotated={ showStations }/>
|
||||||
</div>
|
</div>
|
||||||
{ showStations &&
|
{ showStations &&
|
||||||
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
||||||
@ -159,7 +159,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
const station = data.player.dispatcherStats[ stationName ];
|
const station = data.player.dispatcherStats[ stationName ];
|
||||||
return <div
|
return <div
|
||||||
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
|
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
|
||||||
key={ 1 }
|
key={ stationName }
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<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">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
|
import { formatTime } from "../../../util/time.ts";
|
||||||
|
import { FaCheck } from "react-icons/fa6";
|
||||||
|
|
||||||
|
export const SteamStationTable = ({ stations }: { stations: TLeaderboardRecord[] }) =>
|
||||||
|
{
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-3">
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.user") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.time") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.actions") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ stations.map((station, key) => (
|
||||||
|
<div
|
||||||
|
className={ `grid grid-cols-2 sm:grid-cols-3 ${ stations.length === (key + 1) // todo: ...
|
||||||
|
? ""
|
||||||
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
|
}` }
|
||||||
|
key={ station.id }
|
||||||
|
>
|
||||||
|
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
||||||
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
<Link to={ "/profile/" + station.id }
|
||||||
|
className="color-orchid">{ station.username }</Link> { station.flags.includes("verified") &&
|
||||||
|
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-3">{ formatTime(station.steamDispatcherTime * 1000 * 60) }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
|
<Link
|
||||||
|
to={ "/profile/" + 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("leaderboard.profile") }
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
|
import { FaCheck } from "react-icons/fa6";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||||
|
|
||||||
|
export const SteamTrainTable = ({ trains, setSortBy, sortBy }: {
|
||||||
|
trains: TLeaderboardRecord[],
|
||||||
|
setSortBy: Dispatch<SetStateAction<string>>
|
||||||
|
sortBy: string
|
||||||
|
}) =>
|
||||||
|
{
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
|
<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("leaderboard.user") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||||
|
onClick={ () => setSortBy("distance") }>
|
||||||
|
{ t("leaderboard.distance") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortBy === "distance" || !sortBy }/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||||
|
onClick={ () => setSortBy("points") }>
|
||||||
|
{ t("leaderboard.points") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortBy === "points" }/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.actions") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ trains.map((train, key) => (
|
||||||
|
<div
|
||||||
|
className={ `grid grid-cols-3 sm:grid-cols-4 ${ trains.length === (key + 1)
|
||||||
|
? ""
|
||||||
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
|
}` }
|
||||||
|
key={ train.id }
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||||
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
<Link to={ "/profile/" + train.id }
|
||||||
|
className="color-orchid">{ train.username }</Link> { train.flags.includes("verified") &&
|
||||||
|
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-6">{ (train.steamTrainDistance / 1000).toFixed(2) }km</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-5">{ train.steamTrainScore }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
|
<Link
|
||||||
|
to={ "/profile/" + 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("leaderboard.profile") }
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
;
|
||||||
|
};
|
@ -133,6 +133,7 @@
|
|||||||
"stations": "Stations",
|
"stations": "Stations",
|
||||||
"trains": "Trains",
|
"trains": "Trains",
|
||||||
"leaderboard": "Leaderboard",
|
"leaderboard": "Leaderboard",
|
||||||
|
"steam_leaderboard": "Steam leaderboard",
|
||||||
"info": "INFO",
|
"info": "INFO",
|
||||||
"admin": "ADMIN"
|
"admin": "ADMIN"
|
||||||
},
|
},
|
||||||
|
@ -133,6 +133,7 @@
|
|||||||
"stations": "Stacje",
|
"stations": "Stacje",
|
||||||
"trains": "Pociągi",
|
"trains": "Pociągi",
|
||||||
"leaderboard": "Tablica wyników",
|
"leaderboard": "Tablica wyników",
|
||||||
|
"steam_leaderboard": "Tablica wyników steam",
|
||||||
"info": "INFO",
|
"info": "INFO",
|
||||||
"admin": "ADMIN"
|
"admin": "ADMIN"
|
||||||
},
|
},
|
||||||
|
@ -14,49 +14,33 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useTranslation, Trans } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TStatsResponse } from "../types/stats.ts";
|
import { TStatsResponse } from "../types/stats.ts";
|
||||||
import { WarningAlert } from "../components/mini/alerts/Warning.tsx";
|
import { WarningAlert } from "../components/mini/alerts/Warning.tsx";
|
||||||
import { CardDataStats } from "../components/mini/util/CardDataStats.tsx";
|
import { CardDataStats } from "../components/mini/util/CardDataStats.tsx";
|
||||||
|
import { fetcher } from "../util/fetcher.ts";
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { LoadError } from "../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
|
||||||
export const Home: React.FC = () =>
|
export const Home = () =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [ commit, setCommit ] = useState("");
|
const { data, error } = useSWR<TStatsResponse>("/stats/", fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
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);
|
|
||||||
|
|
||||||
// ADD ALERT IF API DOESN'T WORK! toast?
|
|
||||||
setTrains(data.data.stats.trains);
|
|
||||||
setDispatchers(data.data.stats.dispatchers);
|
|
||||||
setProfiles(data.data.stats.profiles);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex pb-5">
|
<div className="flex pb-5">
|
||||||
<WarningAlert description={ t("preview.description") } title={ t("preview.title") }/>
|
<WarningAlert description={ t("preview.description") } title={ t("preview.title") }/>
|
||||||
|
{ error && <LoadError /> }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10">
|
<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">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6 xl:grid-cols-3 2xl:gap-7.5">
|
||||||
<CardDataStats title={ t("home.stats.trains") } total={ trains.toString() }/>
|
<CardDataStats title={ t("home.stats.trains") } total={ data?.data?.stats?.trains ?? "-" }/>
|
||||||
<CardDataStats title={ t("home.stats.dispatchers") } total={ dispatchers.toString() }/>
|
<CardDataStats title={ t("home.stats.dispatchers") } total={ data?.data?.stats?.dispatchers ?? "-" }/>
|
||||||
<CardDataStats title={ t("home.stats.profiles") } total={ profiles.toString() }/>
|
<CardDataStats title={ t("home.stats.profiles") } total={ data?.data?.stats?.profiles ?? "-" }/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -114,10 +98,10 @@ export const Home: React.FC = () =>
|
|||||||
to={ "https://tailadmin.com/" }>TailAdmin</Link>
|
to={ "https://tailadmin.com/" }>TailAdmin</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>{ version && <Link className="color-orchid"
|
<p>{ data?.data?.git?.version && <Link className="color-orchid"
|
||||||
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/releases/tag/${ version }` }>{ version }</Link> }{ version && commit && " | " }{ commit &&
|
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/releases/tag/${ data?.data?.git?.version }` }>{ data?.data?.git?.version }</Link> }{ data?.data?.git?.version && data?.data?.git?.commit && " | " }{ data?.data?.git?.commit &&
|
||||||
<Link className="color-orchid"
|
<Link className="color-orchid"
|
||||||
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/commit/${ commit }` }>{ commit }</Link> }</p>
|
to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/commit/${ data?.data?.git?.commit }` }>{ data?.data?.git?.commit }</Link> }</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -14,42 +14,39 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TLeaderboardRecord } from "../../types/leaderboard.ts";
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { StationTable } from "../../components/pages/leaderboard/StationTable.tsx";
|
import { StationTable } from "../../components/pages/leaderboard/StationTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const StationLeaderboard = () =>
|
export const StationLeaderboard = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(`/leaderboard/station/?${params.toString()}`, fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }leaderboard/station/`).then(x => x.json()).then(x =>
|
|
||||||
{
|
|
||||||
|
|
||||||
setData(x.data.records);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
setSearchParams(searchParams);
|
|
||||||
|
|
||||||
setData([]);
|
const params = new URLSearchParams();
|
||||||
setError(0);
|
searchValue && params.set("q", searchValue);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/station/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
|
||||||
{
|
setSearchParams(params.toString());
|
||||||
setData(x.data.records);
|
setParams(params);
|
||||||
setError(x.data.records.length > 0 ? 1 : 2);
|
|
||||||
});
|
|
||||||
}, [ searchValue ]);
|
}, [ searchValue ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -62,11 +59,23 @@ export const StationLeaderboard = () =>
|
|||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
<StationTable stations={ data } error={ error }/>
|
<>
|
||||||
|
{ error && <LoadError /> }
|
||||||
|
|
||||||
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
|
{ data && (data && data.code === 404) || (data && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
|
{ data && data.code === 200 && data.data && !!data?.data?.records?.length && <StationTable stations={ data.data.records } /> }
|
||||||
|
</>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -14,49 +14,41 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TLeaderboardRecord } from "../../types/leaderboard.ts";
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { TrainTable } from "../../components/pages/leaderboard/TrainTable.tsx";
|
import { TrainTable } from "../../components/pages/leaderboard/TrainTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
|
||||||
export const TrainLeaderboard = () =>
|
export const TrainLeaderboard = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(`/leaderboard/train/?${ params.toString() }`, fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
const [ sortBy, setSortBy ] = useState(searchParams.get("distance") ?? "");
|
const [ sortBy, setSortBy ] = useState("distance");
|
||||||
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/`).then(x => x.json()).then(x =>
|
|
||||||
{
|
|
||||||
setData(x.data.records);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
const [ error, setError ] = useState<0 | 1 | 2>(0);
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
setSearchParams(searchParams);
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
searchValue && params.set("q", searchValue);
|
||||||
|
sortBy && params.set("s", sortBy);
|
||||||
|
|
||||||
searchValue && params.set('q', searchValue);
|
|
||||||
sortBy && params.set('s', sortBy);
|
|
||||||
|
|
||||||
setData([]);
|
setSearchParams(params.toString());
|
||||||
setError(0);
|
setParams(params);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/?${params.toString()}`).then(x => x.json()).then(x =>
|
|
||||||
{
|
|
||||||
setData(x.data.records);
|
|
||||||
setError(x.data.records.length > 0 ? 1 : 2);
|
|
||||||
});
|
|
||||||
}, [ searchValue, sortBy ]);
|
}, [ searchValue, sortBy ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -69,12 +61,23 @@ export const TrainLeaderboard = () =>
|
|||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
<TrainTable trains={ data } error={ error } setSortBy={setSortBy} sortBy={sortBy}/>
|
<>
|
||||||
|
{ error && <LoadError/> }
|
||||||
|
|
||||||
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
|
{ (data && data.code === 404) || (data && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{ data && data.code === 200 && !!data?.data?.records?.length && <TrainTable trains={ data?.data?.records } setSortBy={ setSortBy } sortBy={ sortBy }/> }
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -14,74 +14,52 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx";
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TLogResponse, TLogStationData, TLogTrainData } from "../../types/log.ts";
|
|
||||||
import { StationLog } from "../../components/pages/log/StationLog.tsx";
|
import { StationLog } from "../../components/pages/log/StationLog.tsx";
|
||||||
import { TrainLog } from "../../components/pages/log/TrainLog.tsx";
|
import { TrainLog } from "../../components/pages/log/TrainLog.tsx";
|
||||||
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
export const Log = () =>
|
export const Log = () =>
|
||||||
{
|
{
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
const { data, error, isLoading } = useSWR(`/log/${ id }`, fetcher, { refreshInterval: 30_000, errorRetryCount: 5 });
|
||||||
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
|
|
||||||
const [ trainData, setTrainData ] = useState<TLogTrainData>(undefined!);
|
|
||||||
const [ stationData, setStationData ] = useState<TLogStationData>(undefined!);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/log/${ id }`).then(x => x.json()).then((data: TLogResponse) =>
|
|
||||||
{
|
|
||||||
switch (data.code)
|
|
||||||
{
|
|
||||||
case 404:
|
|
||||||
setError(2);
|
|
||||||
break;
|
|
||||||
case 403:
|
|
||||||
// NOT_IMPLEMENTED
|
|
||||||
setError(3);
|
|
||||||
break;
|
|
||||||
case 200:
|
|
||||||
setError(1);
|
|
||||||
|
|
||||||
"trainNumber" in data.data ? setTrainData(data.data) : setStationData(data.data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* ERROR */}
|
||||||
|
{ error && <LoadError /> }
|
||||||
{/* LOADING */ }
|
{/* LOADING */ }
|
||||||
{ error === 0 && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
{/* NOT FOUND */ }
|
{/* NOT FOUND */ }
|
||||||
{ error === 2 && <PageMeta title="simrail.alekswilc.dev | Record not found"
|
{ data && data.code === 404 && <PageMeta title="simrail.alekswilc.dev | Record not found"
|
||||||
description="This record could not be found."/> }
|
description="This record could not be found."/> }
|
||||||
{ error === 2 && <WarningAlert title={ t("log.errors.notfound.title") }
|
{ data && data.code === 404 && <WarningAlert title={ t("log.errors.notfound.title") }
|
||||||
description={ t("log.errors.notfound.description") }/> }
|
description={ t("log.errors.notfound.description") }/> }
|
||||||
{/* BLACKLISTED LOG */ }
|
{/* BLACKLISTED LOG */ }
|
||||||
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted record"
|
{ data && data.code === 403 && <PageMeta title="simrail.alekswilc.dev | Blacklisted record"
|
||||||
description="The record has been blocked."/> }
|
description="The record has been blocked."/> }
|
||||||
{ error === 3 && <WarningAlert title={ t("log.errors.blacklist.title") }
|
{ data && data.code === 403 && <WarningAlert title={ t("log.errors.blacklist.title") }
|
||||||
description={ t("log.errors.blacklist.description") }/> }
|
description={ t("log.errors.blacklist.description") }/> }
|
||||||
{/* SUCCESS */ }
|
{/* SUCCESS */ }
|
||||||
{ error === 1 && stationData && <PageMeta
|
{ data && data.code === 200 && !("trainNumber" in data.data) && <PageMeta
|
||||||
title={ `simrail.alekswilc.dev | ${ stationData.userUsername }` }
|
title={ `simrail.alekswilc.dev | ${ data.data.player.username }` }
|
||||||
image={ stationData.userAvatar }
|
image={ data.data.player.avatar }
|
||||||
description={ `${ stationData.stationName } - ${ stationData.stationShort }` }/> }
|
description={ `${ data.data.stationName } - ${ data.data.stationShort }` }/> }
|
||||||
{ error === 1 && stationData && < StationLog data={ stationData }/> }
|
{ data && data.code === 200 && !("trainNumber" in data.data) && data.data &&
|
||||||
|
< StationLog data={ data.data }/> }
|
||||||
|
|
||||||
{ error === 1 && trainData && <PageMeta
|
{ data && data.code === 200 && ("trainNumber" in data.data) && <PageMeta
|
||||||
title={ `simrail.alekswilc.dev | ${ trainData.userUsername }` }
|
title={ `simrail.alekswilc.dev | ${ data.data.player.username }` }
|
||||||
image={ trainData.userAvatar }
|
image={ data.data.player.avatar }
|
||||||
description={ `${ trainData.trainName } - ${ trainData.trainNumber }` }/> }
|
description={ `${ data.data.trainName } - ${ data.data.trainNumber }` }/> }
|
||||||
{ error === 1 && trainData && < TrainLog data={ trainData }/> }
|
{ data && data.code === 200 && ("trainNumber" in data.data) && < TrainLog data={ data.data }/> }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,37 +17,33 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { StationTable } from "../../components/pages/logs/StationTable.tsx";
|
import { StationTable } from "../../components/pages/logs/StationTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { TStationRecord } from "../../types/station.ts";
|
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export const StationLogs = () =>
|
export const StationLogs = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TStationRecord[]>([]);
|
const [params, setParams] = useState(new URLSearchParams());
|
||||||
|
const { data, error, isLoading } = useSWR(`/stations/?${params.toString()}`, fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
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 [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
setSearchParams(searchParams);
|
|
||||||
|
|
||||||
setData([]);
|
const params = new URLSearchParams();
|
||||||
setError(0);
|
searchValue && params.set('q', searchValue);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/stations/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
|
||||||
{
|
setSearchParams(params.toString());
|
||||||
setData(x.data.records);
|
setParams(params);
|
||||||
setError(x.data.records.length > 0 ? 1 : 2);
|
|
||||||
});
|
|
||||||
}, [ searchValue ]);
|
}, [ searchValue ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -60,11 +56,22 @@ export const StationLogs = () =>
|
|||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
<StationTable stations={ data } error={ error }/>
|
<>
|
||||||
|
{ error && <LoadError /> }
|
||||||
|
|
||||||
|
{isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
|
{ (data && data.code === 404) || (data && data.code === 200 && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
|
{data && data.code === 200 && !!data?.data?.records?.length && <StationTable stations={data.data.records} /> }
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -15,41 +15,35 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { TTrainRecord } from "../../types/train.ts";
|
|
||||||
import { TrainTable } from "../../components/pages/logs/TrainTable.tsx";
|
import { TrainTable } from "../../components/pages/logs/TrainTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
export const TrainLogs = () =>
|
export const TrainLogs = () =>
|
||||||
{
|
{
|
||||||
const [ data, setData ] = useState<TTrainRecord[]>([]);
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
|
const { data, error, isLoading } = useSWR(`/trains/?${ params.toString() }`, fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
|
|
||||||
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 [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
setSearchParams(searchParams);
|
|
||||||
|
|
||||||
setData([]);
|
const params = new URLSearchParams();
|
||||||
setError(0);
|
searchValue && params.set("q", searchValue);
|
||||||
fetch(`${ import.meta.env.VITE_API_URL }/trains/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
|
||||||
{
|
setSearchParams(params.toString());
|
||||||
setData(x.data.records);
|
setParams(params);
|
||||||
setError(x.data.records.length > 0 ? 1 : 2);
|
|
||||||
});
|
|
||||||
}, [ searchValue ]);
|
}, [ searchValue ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -62,11 +56,22 @@ export const TrainLogs = () =>
|
|||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
<TrainTable trains={ data } error={ error }/>
|
<>
|
||||||
|
{ error && <LoadError/> }
|
||||||
|
|
||||||
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
|
{ (data && data.code === 404) || (data && data.code === 200 && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
|
{ data && data.code === 200 && !!data?.data?.records?.length && <TrainTable trains={ data.data.records }/> }
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -14,64 +14,44 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { TProfileData, TProfileResponse } from "../../types/profile.ts";
|
|
||||||
import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx";
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
import { ProfileCard } from "../../components/pages/profile/Profile.tsx";
|
import { ProfileCard } from "../../components/pages/profile/Profile.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
||||||
import { formatTime } from "../../util/time.ts";
|
import { formatTime } from "../../util/time.ts";
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
|
||||||
|
|
||||||
export const Profile = () =>
|
export const Profile = () =>
|
||||||
{
|
{
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [ error, setError ] = useState<0 | 1 | 2 | 3>(0);
|
const { data, error, isLoading } = useSWR(`/profiles/${ id }`, fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
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);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* LOADING */ }
|
{/* LOADING */ }
|
||||||
{ error === 0 && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
|
{/* ERROR */}
|
||||||
|
{ error && <LoadError /> }
|
||||||
{/* NOT FOUND */ }
|
{/* NOT FOUND */ }
|
||||||
{ error === 2 && <PageMeta title="simrail.alekswilc.dev | Profile not found"
|
{ data && data.code === 404 && <PageMeta title="simrail.alekswilc.dev | Profile not found"
|
||||||
description="Player's profile could not be found or the player has a private Steam profile."/> }
|
description="Player's profile could not be found or the player has a private Steam profile."/> }
|
||||||
{ error === 2 && <WarningAlert title={ t("profile.errors.notfound.title") }
|
{ data && data.code === 404 && <WarningAlert title={ t("profile.errors.notfound.title") }
|
||||||
description={ t("profile.errors.notfound.description") }/> }
|
description={ t("profile.errors.notfound.description") }/> }
|
||||||
{/* BLACKLISTED PROFILE */ }
|
|
||||||
{ error === 3 && <PageMeta title="simrail.alekswilc.dev | Blacklisted profile"
|
|
||||||
description="This player's profile has been blocked."/> }
|
|
||||||
{ error === 3 && <WarningAlert title={ t("profile.errors.blacklist.title") }
|
|
||||||
description={ t("profile.errors.blacklist.description") }/> }
|
|
||||||
{/* SUCCESS */ }
|
{/* SUCCESS */ }
|
||||||
{ error === 1 && <PageMeta image={ data.steam.avatarfull }
|
{ data && data.code === 200 && <PageMeta image={ data.data.player.username }
|
||||||
title={ `simrail.alekswilc.dev | ${ data.steam.personname }'s profile` }
|
title={ `simrail.alekswilc.dev | ${ data.data.player.username }'s profile` }
|
||||||
description={ `${ data.player.trainDistance ? 0 : ((data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
||||||
${ data.player.dispatcherTime ? 0 : formatTime(data.player.dispatcherTime) } dispatcher experience` }/> }
|
${ data.data.player.dispatcherTime ? 0 : formatTime(data.data.player.dispatcherTime) } dispatcher experience` }/> }
|
||||||
{ error === 1 && <ProfileCard data={ data }/> }
|
{ data && data.code === 200 && <ProfileCard data={ data.data }/> }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { SteamStationTable } from "../../components/pages/steamLeaderboard/SteamStationTable.tsx";
|
||||||
|
|
||||||
|
export const SteamStationLeaderboard = () =>
|
||||||
|
{
|
||||||
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(`/steam/leaderboard/station/?${params.toString()}`, fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
|
|
||||||
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
searchValue && params.set("q", searchValue);
|
||||||
|
|
||||||
|
setSearchParams(params.toString());
|
||||||
|
setParams(params);
|
||||||
|
}, [ searchValue ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setSearchItem(searchParams.get("q") ?? "");
|
||||||
|
}, [ searchParams ]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
{
|
||||||
|
setSearchItem(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
|
<>
|
||||||
|
{ error && <LoadError /> }
|
||||||
|
|
||||||
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
|
{ data && (data && data.code === 404) || (data && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
|
{ data && data.code === 200 && data.data && !!data?.data?.records?.length && <SteamStationTable stations={ data.data.records } /> }
|
||||||
|
</>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { fetcher } from "../../util/fetcher.ts";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
import { SteamTrainTable } from "../../components/pages/steamLeaderboard/SteamTrainTable.tsx";
|
||||||
|
|
||||||
|
export const SteamTrainLeaderboard = () =>
|
||||||
|
{
|
||||||
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(`/steam/leaderboard/train/?${ params.toString() }`, fetcher, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
|
const [ sortBy, setSortBy ] = useState("distance");
|
||||||
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
searchValue && params.set("q", searchValue);
|
||||||
|
sortBy && params.set("s", sortBy);
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(params.toString());
|
||||||
|
setParams(params);
|
||||||
|
}, [ searchValue, sortBy ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setSearchItem(searchParams.get("q") ?? "");
|
||||||
|
}, [ searchParams ]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
{
|
||||||
|
setSearchItem(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
|
<>
|
||||||
|
{ error && <LoadError/> }
|
||||||
|
|
||||||
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
|
{ (data && data.code === 404) || (data && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{ data && data.code === 200 && !!data?.data?.records?.length && <SteamTrainTable trains={ data?.data?.records } setSortBy={ setSortBy } sortBy={ sortBy }/> }
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -28,16 +28,17 @@ export interface TLeaderboardData
|
|||||||
|
|
||||||
export interface TLeaderboardRecord
|
export interface TLeaderboardRecord
|
||||||
{
|
{
|
||||||
id: string;
|
"id": string,
|
||||||
steam: string;
|
"username": string,
|
||||||
steamName: string;
|
"avatar": string,
|
||||||
trainTime: number;
|
"trainTime": number,
|
||||||
trainPoints: number;
|
"trainPoints": number,
|
||||||
trainDistance: number;
|
"trainDistance": number,
|
||||||
dispatcherTime: number;
|
"dispatcherTime": number,
|
||||||
dispatcherStats?: { [ key: string ]: TLeaderboardDispatcherStat };
|
"steamDispatcherTime": number,
|
||||||
trainStats?: { [ key: string ]: TLeaderboardTrainStat };
|
"steamTrainDistance": number,
|
||||||
verified: boolean;
|
"steamTrainScore": number,
|
||||||
|
"flags": string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLeaderboardDispatcherStat
|
export interface TLeaderboardDispatcherStat
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { TProfilePlayer } from "./profile.ts";
|
||||||
|
|
||||||
export interface TLogResponse
|
export interface TLogResponse
|
||||||
{
|
{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -23,31 +25,25 @@ export interface TLogResponse
|
|||||||
|
|
||||||
export interface TLogTrainData
|
export interface TLogTrainData
|
||||||
{
|
{
|
||||||
id: string;
|
|
||||||
trainNumber: string;
|
"id": string,
|
||||||
userSteamId: string;
|
"trainNumber": string,
|
||||||
userUsername: string;
|
"leftDate": number,
|
||||||
userAvatar: string;
|
"joinedDate": number,
|
||||||
leftDate: number;
|
"distance": number,
|
||||||
distance?: number;
|
"points": number,
|
||||||
points?: number;
|
"server": string,
|
||||||
server: string;
|
"trainName": string,
|
||||||
trainName: string;
|
player: TProfilePlayer
|
||||||
joinedDate?: number;
|
|
||||||
verified: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLogStationData
|
export interface TLogStationData
|
||||||
{
|
{
|
||||||
id: string;
|
"id": string,
|
||||||
userSteamId: string;
|
"joinedDate": number,
|
||||||
userUsername: string;
|
"leftDate": number,
|
||||||
userAvatar: string;
|
"stationName": string,
|
||||||
leftDate: number;
|
"stationShort": string,
|
||||||
stationName: string;
|
"server": string,
|
||||||
stationShort: string;
|
player: TProfilePlayer
|
||||||
server: string;
|
|
||||||
joinedDate?: number;
|
|
||||||
verified: boolean;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -33,58 +33,29 @@ export interface TProfileSuccessResponse
|
|||||||
export interface TProfileData
|
export interface TProfileData
|
||||||
{
|
{
|
||||||
player: TProfilePlayer;
|
player: TProfilePlayer;
|
||||||
steam: TProfileSteam;
|
|
||||||
steamStats: TProfileSteamStats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TProfilePlayer
|
export interface TProfilePlayer
|
||||||
{
|
{
|
||||||
id: string;
|
"id": string,
|
||||||
steam: string;
|
"username": string,
|
||||||
steamName: string;
|
"avatar": string,
|
||||||
trainTime: number;
|
"trainTime": number,
|
||||||
dispatcherTime: number;
|
"trainPoints": number,
|
||||||
dispatcherStats: Record<string, TProfileDispatcherStatsRecord>;
|
"trainDistance": number,
|
||||||
trainStats: Record<string, TProfileTrainStatsRecord>;
|
"dispatcherTime": number,
|
||||||
trainDistance: number;
|
"steamDispatcherTime": number,
|
||||||
trainPoints: number;
|
"steamTrainDistance": number,
|
||||||
verified: boolean;
|
"steamTrainScore": number,
|
||||||
}
|
"flags": string[]
|
||||||
|
|
||||||
export interface TProfileDispatcherStatsRecord
|
trainStats: Record<string, {
|
||||||
{
|
|
||||||
time: number;
|
time: number;
|
||||||
}
|
|
||||||
|
|
||||||
export interface TProfileTrainStatsRecord
|
|
||||||
{
|
|
||||||
distance: number;
|
|
||||||
score: number;
|
score: number;
|
||||||
|
distance: number;
|
||||||
|
}>
|
||||||
|
|
||||||
|
dispatcherStats: Record<string, {
|
||||||
time: 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;
|
|
||||||
}
|
}
|
@ -14,6 +14,8 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { TProfilePlayer } from "./profile.ts";
|
||||||
|
|
||||||
export interface TStationResponse
|
export interface TStationResponse
|
||||||
{
|
{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -28,14 +30,16 @@ export interface TStationData
|
|||||||
|
|
||||||
export interface TStationRecord
|
export interface TStationRecord
|
||||||
{
|
{
|
||||||
id: string;
|
"id": string,
|
||||||
userSteamId: string;
|
"joinedDate": number,
|
||||||
userUsername: string;
|
"leftDate": number,
|
||||||
userAvatar: string;
|
"stationName": string,
|
||||||
leftDate: number;
|
"stationShort": string,
|
||||||
stationName: string;
|
"server": string,
|
||||||
stationShort: string;
|
username: string
|
||||||
server: string;
|
steam: string;
|
||||||
joinedDate?: number;
|
|
||||||
verified: boolean;
|
|
||||||
|
player: TProfilePlayer
|
||||||
|
|
||||||
}
|
}
|
@ -14,6 +14,8 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { TProfilePlayer } from "./profile.ts";
|
||||||
|
|
||||||
export interface TTrainResponse
|
export interface TTrainResponse
|
||||||
{
|
{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -28,17 +30,17 @@ export interface TTrainData
|
|||||||
|
|
||||||
export interface TTrainRecord
|
export interface TTrainRecord
|
||||||
{
|
{
|
||||||
id: string;
|
"id": string,
|
||||||
trainNumber: string;
|
"trainNumber": string,
|
||||||
userSteamId: string;
|
"joinedDate": number,
|
||||||
userUsername: string;
|
"leftDate": number,
|
||||||
userAvatar: string;
|
"distance": number,
|
||||||
joinedDate?: number;
|
"points": number,
|
||||||
leftDate: number;
|
"server": string,
|
||||||
distance: number;
|
"trainName": string,
|
||||||
points: number;
|
username: string
|
||||||
server: string;
|
steam: string;
|
||||||
trainName: string;
|
|
||||||
verified: boolean;
|
player: TProfilePlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,28 +14,4 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Model, model, Schema } from "mongoose";
|
export const fetcher = (url: string) => fetch(`${ import.meta.env.VITE_API_URL }${url}`, { signal: AbortSignal.timeout(2500) }).then((res) => res.json());
|
||||||
|
|
||||||
|
|
||||||
export const raw_schema = {
|
|
||||||
steam: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const schema = new Schema<IBlacklist>(raw_schema);
|
|
||||||
|
|
||||||
export type TMBlacklist = Model<IBlacklist>
|
|
||||||
|
|
||||||
export const MBlacklist = model<IBlacklist>("blacklist", schema);
|
|
||||||
|
|
||||||
export interface IBlacklist
|
|
||||||
{
|
|
||||||
steam: string;
|
|
||||||
status: boolean;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user