Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
996f16a313 | |||
0f8b1a4729 | |||
badefb0f7f | |||
2ba037fab5 | |||
637134081a | |||
96c17ffe85 | |||
8db158afe9 | |||
6015349c6f | |||
cd191a6f61 | |||
08bfe767bb | |||
6a4ebf56c4 | |||
1e4cafc07f | |||
623bdd8d42 | |||
51b3ff2e22 | |||
816ee5c454 | |||
9a3f004c72 | |||
f528da171b | |||
b23921a28c | |||
e045ded046 | |||
859b7ab3cc | |||
34432e9622 | |||
3af371703b | |||
fcef4e428e | |||
ca8e270c5e | |||
a9d35c604e | |||
8c24a29de2 | |||
e079a1177e | |||
7bf053e452 | |||
328f665c5c | |||
05fe82eff1 | |||
3143ce1058 | |||
85d18190a7 | |||
45434f5a2d | |||
922b7ea633 | |||
f0f01ccda1 | |||
947bd5dedc | |||
a2a9fd25b5 | |||
afdc700a64 | |||
f5ba173b24 | |||
7459649829 | |||
31c1f3fc40 | |||
df1d9df212 | |||
680a18d15a | |||
40f31ba723 | |||
f9974a3430 | |||
c5249da57e | |||
85e00e8d52 | |||
5841c77913 | |||
84d2972869 | |||
94407bc7c4 | |||
47695d7e4f | |||
529d8b8020 | |||
3db6ced4d2 | |||
e5614da846 | |||
71a5b77235 | |||
7617738ab2 | |||
9c3d3e0767 | |||
294068ee97 | |||
29a7ab3a6b | |||
b4cee2447d | |||
a4091c92e4 | |||
77a9540be4 | |||
f8f5a38add | |||
4c7482b919 | |||
f10c623aa8 | |||
4e090ed281 | |||
1b9c616e16 | |||
7fe575a651 | |||
f33c54ba26 | |||
a9094fd1ed | |||
47d98aba82 | |||
c8efbb92f5 | |||
e460fdefe8 | |||
7ed9df1e9d | |||
094e00842a | |||
608894ee1e | |||
895725dc66 | |||
0275d3e0d1 | |||
a46b2237cf | |||
70eced72d2 | |||
0004148cbf | |||
cfcf5cdaaf | |||
b7667304d2 | |||
7e29802f8e | |||
17473db3b5 | |||
990bb8b552 | |||
3f3e369fb7 | |||
e84afd359a | |||
3ce2d4501a | |||
bb1245e528 | |||
fe0d59cad3 |
31
.drone.yml
Normal file
31
.drone.yml
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
|
||||
steps:
|
||||
- name: build-backend
|
||||
image: plugins/docker
|
||||
settings:
|
||||
insecure: true
|
||||
repo: 10.5.0.103:1222/simrail-backend
|
||||
registry: 10.5.0.103:1222
|
||||
context: './packages/backend'
|
||||
dockerfile: './packages/backend/Dockerfile'
|
||||
tags:
|
||||
- latest
|
||||
|
||||
- name: build-frontend
|
||||
image: plugins/docker
|
||||
settings:
|
||||
insecure: true
|
||||
repo: 10.5.0.103:1222/simrail-frontend
|
||||
registry: 10.5.0.103:1222
|
||||
context: './packages/frontend'
|
||||
dockerfile: './packages/frontend/Dockerfile'
|
||||
tags:
|
||||
- latest
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
7
.idea/copyright/profiles_settings.xml
generated
Normal file
7
.idea/copyright/profiles_settings.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="gnu agpl alekswilc">
|
||||
<module2copyright>
|
||||
<element module="Project Files" copyright="gnu agpl alekswilc" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<Languages>
|
||||
<language minSize="457" name="TypeScript" />
|
||||
</Languages>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="file://$PROJECT_DIR$/packages/frontend" libraries="{script}" />
|
||||
</component>
|
||||
</project>
|
@ -4,7 +4,6 @@
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "yarn workspace backend build && yarn workspace frontend build",
|
||||
"postbuild": "copyfiles --error ./LICENSE.txt ./dist && copyfiles --error ./LICENSE.txt ./dist/frontend/",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn workspace backend start\" \"yarn workspace frontend dev\""
|
||||
},
|
||||
"workspaces": [
|
||||
|
20
packages/backend/Dockerfile
Normal file
20
packages/backend/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
FROM node:21-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN yarn add -D typescript
|
||||
RUN yarn rawbuild
|
||||
|
||||
RUN ls
|
||||
|
||||
|
||||
# Install Doppler CLI
|
||||
RUN wget -q -t3 'https://packages.doppler.com/public/cli/rsa.8004D9FF50437357.key' -O /etc/apk/keys/cli@doppler-8004D9FF50437357.rsa.pub && \
|
||||
echo 'https://packages.doppler.com/public/cli/alpine/any-version/main' | tee -a /etc/apk/repositories && \
|
||||
apk add doppler
|
||||
|
||||
|
||||
|
||||
ENTRYPOINT ["doppler", "run", "--"]
|
||||
CMD ["node", "/app/dist"]
|
9
packages/backend/backend.iml
Normal file
9
packages/backend/backend.iml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -4,7 +4,8 @@
|
||||
"main": "../../dist/backend/index.js",
|
||||
"version": "3.0.0",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "docker build --progress=plain -t simrailpro:backend .",
|
||||
"rawbuild": "yarn tsc",
|
||||
"start": "yarn build && doppler run node ../../dist/backend/index.js"
|
||||
},
|
||||
"author": "Aleksander <alekswilc> Wilczyński",
|
||||
@ -15,6 +16,7 @@
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"tsc": "^2.0.4",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"type": "module",
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -40,6 +40,24 @@ interface ActiveStation
|
||||
steam: string;
|
||||
}
|
||||
|
||||
import { Server } from "@simrail/types";
|
||||
import { generateUrl } from "../../util/imgproxy.js";
|
||||
|
||||
const sortFunction = (a: ActiveStation | ActiveTrain, b: ActiveStation | ActiveTrain) =>
|
||||
{
|
||||
if (a.server.includes("pl") && !b.server.includes("pl"))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!a.server.includes("pl") && b.server.includes("pl"))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export class ActivePlayersRoute
|
||||
{
|
||||
static load()
|
||||
@ -48,15 +66,22 @@ export class ActivePlayersRoute
|
||||
|
||||
app.get("/train", async (req, res) =>
|
||||
{
|
||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const sserver = req.query.server?.toString();
|
||||
|
||||
let a: ActiveTrain[] = [];
|
||||
|
||||
for (const data of Object.values(client.trains))
|
||||
for (const data of sserver ? [ client.trains[ sserver as Server["ServerCode"] ] ] : Object.values(client.trains))
|
||||
{
|
||||
for (const d of data.filter(d => d.TrainData.ControlledBySteamID))
|
||||
{
|
||||
const p = await PlayerUtil.getPlayer(d.TrainData.ControlledBySteamID!);
|
||||
|
||||
if (p && process.env.IMGPROXY_KEY)
|
||||
{
|
||||
p.avatar = generateUrl(p.avatar);
|
||||
}
|
||||
|
||||
p && a.push({
|
||||
server: d.ServerCode,
|
||||
player: p,
|
||||
@ -71,31 +96,42 @@ export class ActivePlayersRoute
|
||||
|
||||
if (s)
|
||||
{
|
||||
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.trainName) || c.test(d.trainNumber) ).length === s.length);
|
||||
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.trainName) || c.test(d.trainNumber)).length === s.length);
|
||||
}
|
||||
|
||||
a = arrayGroupBy(a, d => d.server);
|
||||
a = arrayGroupBy(a, d => d.server)
|
||||
.sort(sortFunction);
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({ records: a })
|
||||
.setData({
|
||||
records: a,
|
||||
servers: Object.keys(client.stations),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
app.get("/station", async (req, res) =>
|
||||
{
|
||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const sserver = req.query.server?.toString();
|
||||
|
||||
let a: ActiveStation[] = [];
|
||||
|
||||
for (const server of Object.keys(client.stations))
|
||||
for (const server of sserver ? [ sserver ] : Object.keys(client.stations))
|
||||
{
|
||||
for (const d of client.stations[ server ].filter(d => d.DispatchedBy.length && d.DispatchedBy[ 0 ]?.SteamId))
|
||||
{
|
||||
// todo: optimize
|
||||
const p = await PlayerUtil.getPlayer(d.DispatchedBy[ 0 ].SteamId!);
|
||||
|
||||
if (p && process.env.IMGPROXY_KEY)
|
||||
{
|
||||
p.avatar = generateUrl(p.avatar);
|
||||
}
|
||||
|
||||
p && a.push({
|
||||
server: server,
|
||||
player: p,
|
||||
@ -110,16 +146,20 @@ export class ActivePlayersRoute
|
||||
|
||||
if (s)
|
||||
{
|
||||
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.stationName) || c.test(d.stationShort) ).length === s.length);
|
||||
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.stationName) || c.test(d.stationShort)).length === s.length);
|
||||
}
|
||||
|
||||
|
||||
a = arrayGroupBy(a, d => d.server);
|
||||
a = arrayGroupBy(a, d => d.server)
|
||||
.sort(sortFunction);
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({ records: a })
|
||||
.setData({
|
||||
records: a,
|
||||
servers: Object.keys(client.stations),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -18,6 +18,9 @@ import { Router } from "express";
|
||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { MAdmin } from "../../mongo/admin.js";
|
||||
import { MProfile } from "../../mongo/profile.js";
|
||||
import { PlayerUtil } from "../../util/PlayerUtil.js";
|
||||
import { getVehicle } from "../../util/contants.js";
|
||||
import { isTruthyAndGreaterThanZero } from "../../util/functions.js";
|
||||
|
||||
export class AdminRoute
|
||||
{
|
||||
@ -203,6 +206,101 @@ export class AdminRoute
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
app.post("/profile/:playerId/forceUpdate", async (req, res) =>
|
||||
{
|
||||
const token = req.headers[ "x-auth-token" ];
|
||||
|
||||
if (!token)
|
||||
{
|
||||
res.status(400).json(new ErrorResponseBuilder()
|
||||
.setCode(400)
|
||||
.setData("Missing token").toJSON());
|
||||
return;
|
||||
}
|
||||
|
||||
const admin = await MAdmin.findOne({ token });
|
||||
|
||||
if (!admin)
|
||||
{
|
||||
res.status(401).json(new ErrorResponseBuilder()
|
||||
.setCode(401)
|
||||
.setData("Invalid token").toJSON());
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await MProfile.findOne({
|
||||
id: req.params.playerId,
|
||||
});
|
||||
|
||||
if (!player)
|
||||
{
|
||||
res.status(401).json(new ErrorResponseBuilder()
|
||||
.setCode(401)
|
||||
.setData("Invalid playerId").toJSON());
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = await PlayerUtil.getPlayerStats(player.id);
|
||||
|
||||
if (!stats)
|
||||
{
|
||||
res.status(401).json(new ErrorResponseBuilder()
|
||||
.setCode(401)
|
||||
.setData("Invalid playerId (2)").toJSON());
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
if (player.steamTrainDistance > player.trainDistance)
|
||||
{
|
||||
player.trainDistance = player.steamTrainDistance;
|
||||
}
|
||||
if (player.steamTrainScore > player.trainPoints)
|
||||
{
|
||||
player.trainPoints = player.steamTrainScore;
|
||||
}
|
||||
|
||||
const sum = Object.keys(player.trainStats).filter(x => x !== "N/A").map(x => player.trainStats[ x ]).reduce((acc, obj) =>
|
||||
{
|
||||
acc.time += obj.time;
|
||||
acc.distance += obj.distance;
|
||||
acc.score += obj.score;
|
||||
return acc;
|
||||
}, { time: 0, distance: 0, score: 0 });
|
||||
|
||||
|
||||
player.trainStats[ "N/A" ] = {
|
||||
time: 0,
|
||||
distance: player.trainDistance - sum.distance,
|
||||
score: player.trainPoints - sum.score,
|
||||
};
|
||||
|
||||
|
||||
if (typeof player.createdAt !== "number")
|
||||
{
|
||||
player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
||||
}
|
||||
|
||||
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
||||
|
||||
player.username = playerData?.personaname ?? player.username;
|
||||
player.avatar = playerData?.avatarfull ?? player.avatar;
|
||||
|
||||
await MProfile.updateOne({ id: player.id }, player);
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({})
|
||||
.toJSON(),
|
||||
);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,8 +17,9 @@
|
||||
import { Router } from "express";
|
||||
import { PipelineStage } from "mongoose";
|
||||
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
||||
import { generateUrl } from "../../util/imgproxy.js";
|
||||
|
||||
const generateSearch = (regex: RegExp) => [
|
||||
{
|
||||
@ -33,7 +34,7 @@ const sortyByMap: Record<string, any> = {
|
||||
time: { trainTime: -1 },
|
||||
points: { trainPoints: -1 },
|
||||
distance: { trainDistance: -1 },
|
||||
}
|
||||
};
|
||||
|
||||
export class LeaderboardRoute
|
||||
{
|
||||
@ -43,14 +44,24 @@ export class LeaderboardRoute
|
||||
|
||||
app.get("/train", async (req, res) =>
|
||||
{
|
||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
|
||||
const limit = parseInt(req.query.limit as string) || 12;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
|
||||
if (page < 0 || limit < 0)
|
||||
{
|
||||
res.status(400).json(new ErrorResponseBuilder()
|
||||
.setCode(400).setData("Invalid page and/or limit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const filter: PipelineStage[] = [
|
||||
{
|
||||
$match: {
|
||||
flags: { $nin: ["hidden", "leaderboard_hidden"] }
|
||||
}
|
||||
}
|
||||
flags: { $nin: [ "hidden", "leaderboard_hidden" ] },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
s && filter.push({
|
||||
@ -61,31 +72,68 @@ export class LeaderboardRoute
|
||||
},
|
||||
});
|
||||
|
||||
const sortBy = sortyByMap[req.query.s?.toString() ?? 'distance'] ?? sortyByMap.distance;
|
||||
const sortBy = sortyByMap[ req.query.sort_by?.toString() ?? "distance" ] ?? sortyByMap.distance;
|
||||
|
||||
const records = await MProfile.aggregate(filter)
|
||||
.sort(sortBy)
|
||||
.limit(10);
|
||||
filter.push({
|
||||
$sort: sortBy,
|
||||
});
|
||||
|
||||
filter.push({
|
||||
$facet: {
|
||||
data: [
|
||||
{ $match: {} },
|
||||
{ $skip: (page - 1) * limit },
|
||||
{ $limit: limit },
|
||||
],
|
||||
count: [
|
||||
{ $count: "count" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MProfile.aggregate(filter);
|
||||
|
||||
|
||||
process.env.IMGPROXY_KEY && records[ 0 ].data && records[ 0 ].data.forEach((p: IProfile) =>
|
||||
{
|
||||
if (p.avatar.includes("imgproxy.alekswilc.dev"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
p.avatar = generateUrl(p.avatar);
|
||||
});
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({ records: records.map(x => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
||||
.setData({
|
||||
records: records[ 0 ].data.map((x: IProfile) => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])),
|
||||
pages: Math.floor((records[ 0 ]?.count?.[ 0 ]?.count ?? 0) / limit),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
app.get("/station", async (req, res) =>
|
||||
{
|
||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
|
||||
const limit = parseInt(req.query.limit as string) || 12;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
|
||||
if (page < 0 || limit < 0)
|
||||
{
|
||||
res.status(400).json(new ErrorResponseBuilder()
|
||||
.setCode(400).setData("Invalid page and/or limit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const filter: PipelineStage[] = [
|
||||
{
|
||||
$match: {
|
||||
flags: { $nin: ["hidden", "leaderboard_hidden"] }
|
||||
}
|
||||
}
|
||||
flags: { $nin: [ "hidden", "leaderboard_hidden" ] },
|
||||
},
|
||||
},
|
||||
];
|
||||
s && filter.push({
|
||||
$match: {
|
||||
@ -95,14 +143,43 @@ export class LeaderboardRoute
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MProfile.aggregate(filter)
|
||||
.sort({ dispatcherTime: -1 })
|
||||
.limit(10);
|
||||
filter.push({
|
||||
$sort: {
|
||||
dispatcherTime: -1,
|
||||
},
|
||||
});
|
||||
|
||||
filter.push({
|
||||
$facet: {
|
||||
data: [
|
||||
{ $match: {} },
|
||||
{ $skip: (page - 1) * limit },
|
||||
{ $limit: limit },
|
||||
],
|
||||
count: [
|
||||
{ $count: "count" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MProfile.aggregate(filter);
|
||||
|
||||
process.env.IMGPROXY_KEY && records[ 0 ].data && records[ 0 ].data.forEach((p: IProfile) =>
|
||||
{
|
||||
if (p.avatar.includes("imgproxy.alekswilc.dev"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
p.avatar = generateUrl(p.avatar);
|
||||
});
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({ records: records.map(x => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
||||
.setData({
|
||||
records: records[ 0 ].data.map((x: IProfile) => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])),
|
||||
pages: Math.floor((records[ 0 ]?.count?.[ 0 ]?.count ?? 0) / limit),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -19,6 +19,7 @@ import { MTrainLog } from "../../mongo/trainLog.js";
|
||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { MStationLog } from "../../mongo/stationLog.js";
|
||||
import { IProfile } from "../../mongo/profile.js";
|
||||
import { generateUrl } from "../../util/imgproxy.js";
|
||||
|
||||
|
||||
export class LogRoute
|
||||
@ -60,7 +61,13 @@ export class LogRoute
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json(new SuccessResponseBuilder().setCode(200).setData(log.toJSON()));
|
||||
|
||||
if (process.env.IMGPROXY_KEY)
|
||||
{
|
||||
log.player.avatar = generateUrl(log.player.avatar, "rs:auto:256:256:1/f:png");
|
||||
}
|
||||
|
||||
res.status(200).json(new SuccessResponseBuilder().setCode(200).setData(log.toJSON()).toJSON());
|
||||
});
|
||||
|
||||
return app;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -15,8 +15,35 @@
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { PipelineStage } from "mongoose";
|
||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { PlayerUtil } from "../../util/PlayerUtil.js";
|
||||
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||
import { escapeRegexString } from "../../util/functions.js";
|
||||
import { generateUrl } from "../../util/imgproxy.js";
|
||||
|
||||
const generateSearch = (regex: RegExp) => [
|
||||
{
|
||||
id: { $regex: regex },
|
||||
},
|
||||
{
|
||||
username: { $regex: regex },
|
||||
},
|
||||
];
|
||||
|
||||
type ActiveTrain = {
|
||||
type: "train"
|
||||
trainNumber: string
|
||||
trainName: string
|
||||
server: string
|
||||
}
|
||||
|
||||
type ActiveStation = {
|
||||
type: "station",
|
||||
stationName: string
|
||||
stationShort: string
|
||||
server: string
|
||||
}
|
||||
|
||||
export class ProfilesRoute
|
||||
{
|
||||
@ -40,25 +67,129 @@ export class ProfilesRoute
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.flags.includes('hidden'))
|
||||
if (player.flags.includes("hidden"))
|
||||
{
|
||||
res.status(403).json(new ErrorResponseBuilder()
|
||||
.setCode(403).setData("Profile blocked!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.flags.includes('private'))
|
||||
if (player.flags.includes("private"))
|
||||
{
|
||||
res.status(404).json(new ErrorResponseBuilder()
|
||||
.setCode(404).setData("Profile is private!"));
|
||||
return;
|
||||
}
|
||||
|
||||
let active: ActiveStation | ActiveTrain = undefined!;
|
||||
|
||||
for (const x of Object.keys(client.trains))
|
||||
{
|
||||
const data = client.trains[ x ].find(x => x.TrainData.ControlledBySteamID === player.id);
|
||||
if (data)
|
||||
{
|
||||
active = {
|
||||
type: "train",
|
||||
trainNumber: data.TrainNoLocal,
|
||||
trainName: data.TrainName,
|
||||
server: x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const x of Object.keys(client.stations))
|
||||
{
|
||||
const data = client.stations[ x ].find(x => x.DispatchedBy[ 0 ]?.SteamId === player.id);
|
||||
if (data)
|
||||
{
|
||||
active = {
|
||||
type: "station",
|
||||
stationName: data.Name,
|
||||
stationShort: data.Prefix,
|
||||
server: x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (process.env.IMGPROXY_KEY)
|
||||
{
|
||||
player.avatar = generateUrl(player.avatar, "rs:auto:256:256:1/f:png");
|
||||
}
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({
|
||||
player
|
||||
player,
|
||||
active,
|
||||
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/", async (req, res) =>
|
||||
{
|
||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
|
||||
const limit = parseInt(req.query.limit as string) || 12;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
|
||||
if (page < 0 || limit < 0)
|
||||
{
|
||||
res.status(400).json(new ErrorResponseBuilder()
|
||||
.setCode(400).setData("Invalid page and/or limit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const filter: PipelineStage[] = [
|
||||
{
|
||||
$match: {
|
||||
flags: { $nin: [ "hidden" ] },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
s && filter.push({
|
||||
$match: {
|
||||
$and: [
|
||||
...s.map(x => ({ $or: generateSearch(x) })),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
filter.push({
|
||||
$facet: {
|
||||
data: [
|
||||
{ $match: {} },
|
||||
{ $skip: (page - 1) * limit },
|
||||
{ $limit: limit },
|
||||
],
|
||||
count: [
|
||||
{ $count: "count" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MProfile.aggregate(filter);
|
||||
|
||||
|
||||
process.env.IMGPROXY_KEY && records[ 0 ].data && records[ 0 ].data.forEach((p: IProfile) =>
|
||||
{
|
||||
if (p.avatar.includes("imgproxy.alekswilc.dev"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
p.avatar = generateUrl(p.avatar);
|
||||
});
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({
|
||||
records: records[ 0 ].data,
|
||||
pages: Math.floor((records[ 0 ]?.count?.[ 0 ]?.count ?? 0) / limit),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -18,8 +18,9 @@ import { Router } from "express";
|
||||
import { IStationLog, MStationLog } from "../../mongo/stationLog.js";
|
||||
import { PipelineStage } from "mongoose";
|
||||
import { escapeRegexString } from "../../util/functions.js";
|
||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { MProfile } from "../../mongo/profile.js";
|
||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||
import { generateUrl } from "../../util/imgproxy.js";
|
||||
|
||||
const generateSearch = (regex: RegExp) => [
|
||||
{
|
||||
@ -47,7 +48,19 @@ export class StationsRoute
|
||||
|
||||
app.get("/", async (req, res) =>
|
||||
{
|
||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x.trim()), "i"));
|
||||
const server = req.query.server?.toString();
|
||||
|
||||
const limit = parseInt(req.query.limit as string) || 12;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
|
||||
if (page < 0 || limit < 0)
|
||||
{
|
||||
res.status(400).json(new ErrorResponseBuilder()
|
||||
.setCode(400).setData("Invalid page and/or limit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const filter: PipelineStage[] = [];
|
||||
|
||||
s && filter.push({
|
||||
@ -58,16 +71,56 @@ export class StationsRoute
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MStationLog.aggregate(filter)
|
||||
.sort({ leftDate: -1 })
|
||||
.limit(30);
|
||||
server && filter.push({
|
||||
$match: {
|
||||
server: {
|
||||
$regex: new RegExp(escapeRegexString(server), "i"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await MProfile.populate(records, { path: "player" });
|
||||
filter.push({
|
||||
$sort: {
|
||||
leftDate: -1,
|
||||
},
|
||||
});
|
||||
|
||||
filter.push({
|
||||
$facet: {
|
||||
data: [
|
||||
{ $match: {} },
|
||||
{ $skip: (page - 1) * limit },
|
||||
{ $limit: limit },
|
||||
],
|
||||
count: [
|
||||
{ $count: "count" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MStationLog.aggregate(filter);
|
||||
|
||||
await MProfile.populate(records, { path: "data.player" });
|
||||
|
||||
process.env.IMGPROXY_KEY && records[ 0 ].data && records[ 0 ].data.forEach((p: IStationLog & {
|
||||
player: IProfile
|
||||
}) =>
|
||||
{
|
||||
if (p.player.avatar.includes("imgproxy.alekswilc.dev"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
p.player.avatar = generateUrl(p.player.avatar);
|
||||
});
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder<{ records: IStationLog[] }>()
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({ records })
|
||||
.setData({
|
||||
records: records[ 0 ].data,
|
||||
pages: Math.floor((records[ 0 ]?.count?.[ 0 ]?.count ?? 0) / limit),
|
||||
servers: Object.keys(client.stations),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -46,7 +46,6 @@ export class StatsRoute
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,9 +17,10 @@
|
||||
import { Router } from "express";
|
||||
import { PipelineStage } from "mongoose";
|
||||
import { ITrainLog, MTrainLog } from "../../mongo/trainLog.js";
|
||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||
import { escapeRegexString } from "../../util/functions.js";
|
||||
import { MProfile } from "../../mongo/profile.js";
|
||||
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||
import { generateUrl } from "../../util/imgproxy.js";
|
||||
|
||||
const generateSearch = (regex: RegExp) => [
|
||||
{
|
||||
@ -44,10 +45,20 @@ export class TrainsRoute
|
||||
|
||||
app.get("/", async (req, res) =>
|
||||
{
|
||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||
const server = req.query.server?.toString();
|
||||
|
||||
const filter: PipelineStage[] = [];
|
||||
|
||||
const limit = parseInt(req.query.limit as string) || 12;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
|
||||
if (page < 0 || limit < 0)
|
||||
{
|
||||
res.status(400).json(new ErrorResponseBuilder()
|
||||
.setCode(400).setData("Invalid page and/or limit"));
|
||||
return;
|
||||
}
|
||||
|
||||
s && filter.push({
|
||||
$match: {
|
||||
@ -57,17 +68,55 @@ export class TrainsRoute
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MTrainLog.aggregate(filter)
|
||||
.sort({ leftDate: -1 })
|
||||
.limit(30);
|
||||
server && filter.push({
|
||||
$match: {
|
||||
server: {
|
||||
$regex: new RegExp(escapeRegexString(server), "i"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await MProfile.populate(records, { path: "player" });
|
||||
filter.push({
|
||||
$sort: {
|
||||
leftDate: -1,
|
||||
},
|
||||
});
|
||||
|
||||
filter.push({
|
||||
$facet: {
|
||||
data: [
|
||||
{ $match: {} },
|
||||
{ $skip: (page - 1) * limit },
|
||||
{ $limit: limit },
|
||||
],
|
||||
count: [
|
||||
{ $count: "count" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const records = await MTrainLog.aggregate(filter);
|
||||
|
||||
await MProfile.populate(records, { path: "data.player" });
|
||||
|
||||
process.env.IMGPROXY_KEY && records[ 0 ].data && records[ 0 ].data.forEach((p: ITrainLog & {
|
||||
player: IProfile
|
||||
}) =>
|
||||
{
|
||||
if (p.player.avatar.includes("imgproxy.alekswilc.dev"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
p.player.avatar = generateUrl(p.player.avatar);
|
||||
});
|
||||
|
||||
res.json(
|
||||
new SuccessResponseBuilder<{ records: ITrainLog[] }>()
|
||||
new SuccessResponseBuilder()
|
||||
.setCode(200)
|
||||
.setData({
|
||||
records
|
||||
records: records[ 0 ].data,
|
||||
pages: Math.floor((records[ 0 ]?.count?.[ 0 ]?.count ?? 0) / limit),
|
||||
servers: Object.keys(client.stations),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -24,6 +24,7 @@ import { TrainsModule } from "./modules/trains.js";
|
||||
import { Server, Station, Train } from "@simrail/types";
|
||||
import dayjs from "dayjs";
|
||||
import { TMProfile } from "./mongo/profile.js";
|
||||
import { GitUtil } from "./util/git.js";
|
||||
|
||||
;(async () =>
|
||||
{
|
||||
@ -47,6 +48,7 @@ import { TMProfile } from "./mongo/profile.js";
|
||||
}
|
||||
|
||||
ApiModule.load(); // TODO: use fastify
|
||||
GitUtil.getData();
|
||||
|
||||
if (process.env.NODE_ENV === "development")
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -85,26 +85,37 @@ export class StationsModule
|
||||
player.steamTrainScore = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
|
||||
|
||||
|
||||
if ((player.steamTrainDistance > player.trainDistance) || (player.trainPoints > player.steamTrainScore))
|
||||
if (player.steamTrainDistance > player.trainDistance)
|
||||
{
|
||||
player.trainStats[ "N/A" ] = {
|
||||
time: 0, distance: player.steamTrainDistance > player.trainDistance ? player.steamTrainDistance - player.trainDistance : player.trainDistance,
|
||||
score: player.trainPoints > player.steamTrainScore ? player.steamTrainScore - player.trainPoints : player.trainPoints,
|
||||
};
|
||||
|
||||
if (player.steamTrainDistance > player.trainDistance)
|
||||
{
|
||||
player.trainDistance = player.steamTrainDistance;
|
||||
}
|
||||
if (player.trainPoints > player.steamTrainScore)
|
||||
{
|
||||
player.trainPoints = player.steamTrainScore;
|
||||
}
|
||||
player.trainDistance = player.steamTrainDistance;
|
||||
}
|
||||
if (player.steamTrainScore > player.trainPoints)
|
||||
{
|
||||
player.trainPoints = player.steamTrainScore;
|
||||
}
|
||||
|
||||
const sum = Object.keys(player.trainStats).filter(x => x !== "N/A").map(x => player.trainStats[ x ]).reduce((acc, obj) =>
|
||||
{
|
||||
acc.time += obj.time;
|
||||
acc.distance += obj.distance;
|
||||
acc.score += obj.score;
|
||||
return acc;
|
||||
}, { time: 0, distance: 0, score: 0 });
|
||||
|
||||
|
||||
player.trainStats[ "N/A" ] = {
|
||||
time: 0,
|
||||
distance: player.trainDistance - sum.distance,
|
||||
score: player.trainPoints - sum.score,
|
||||
};
|
||||
|
||||
|
||||
player.flags = player.flags.filter(x => x !== "private");
|
||||
|
||||
if (typeof player.createdAt !== 'number') player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
||||
if (typeof player.createdAt !== "number")
|
||||
{
|
||||
player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -61,10 +61,14 @@ export class TrainsModule
|
||||
const vehicleName = getVehicle(vehicle) ?? vehicle;
|
||||
|
||||
if (!isTruthyAndGreaterThanZero(distance))
|
||||
{
|
||||
distance = 0;
|
||||
}
|
||||
|
||||
if (!isTruthyAndGreaterThanZero(points))
|
||||
{
|
||||
points = 0;
|
||||
}
|
||||
|
||||
|
||||
if (!player.trainStats)
|
||||
@ -114,33 +118,45 @@ export class TrainsModule
|
||||
player.steamDispatcherTime = stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0;
|
||||
player.steamTrainScore = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
|
||||
|
||||
if ((player.steamTrainDistance > player.trainDistance) || (player.steamTrainScore > player.trainPoints))
|
||||
{
|
||||
player.trainStats[ "N/A" ] = {
|
||||
time: 0, distance: player.steamTrainDistance > player.trainDistance ? player.steamTrainDistance - player.trainDistance : player.trainDistance,
|
||||
score: player.steamTrainScore > player.trainPoints ? player.steamTrainScore - player.trainPoints : player.trainPoints,
|
||||
};
|
||||
|
||||
if (player.steamTrainDistance > player.trainDistance)
|
||||
{
|
||||
player.trainDistance = player.steamTrainDistance;
|
||||
}
|
||||
if (player.steamTrainScore > player.trainPoints)
|
||||
{
|
||||
player.trainPoints = player.steamTrainScore;
|
||||
}
|
||||
if (player.steamTrainDistance > player.trainDistance)
|
||||
{
|
||||
player.trainDistance = player.steamTrainDistance;
|
||||
}
|
||||
if (player.steamTrainScore > player.trainPoints)
|
||||
{
|
||||
player.trainPoints = player.steamTrainScore;
|
||||
}
|
||||
|
||||
const sum = Object.keys(player.trainStats).filter(x => x !== "N/A").map(x => player.trainStats[ x ]).reduce((acc, obj) =>
|
||||
{
|
||||
acc.time += obj.time;
|
||||
acc.distance += obj.distance;
|
||||
acc.score += obj.score;
|
||||
return acc;
|
||||
}, { time: 0, distance: 0, score: 0 });
|
||||
|
||||
|
||||
player.trainStats[ "N/A" ] = {
|
||||
time: 0,
|
||||
distance: player.trainDistance - sum.distance,
|
||||
score: player.trainPoints - sum.score,
|
||||
};
|
||||
|
||||
|
||||
player.flags = player.flags.filter(x => x !== "private");
|
||||
|
||||
if (typeof player.createdAt !== 'number') player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
||||
if (typeof player.createdAt !== "number")
|
||||
{
|
||||
player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
||||
|
||||
!stats && !player.flags.includes('private') && player.flags.push("private");
|
||||
!stats && !player.flags.includes("private") && player.flags.push("private");
|
||||
|
||||
player.flags = [...new Set(player.flags)];
|
||||
player.flags = [ ...new Set(player.flags) ];
|
||||
|
||||
player.username = playerData?.personaname ?? player.username;
|
||||
player.avatar = playerData?.avatarfull ?? player.avatar;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -35,6 +35,6 @@ export const MAdmin = model<IAdmin>("admin", schema);
|
||||
|
||||
export interface IAdmin
|
||||
{
|
||||
token: string
|
||||
username: string
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -55,8 +55,8 @@ export const raw_schema = {
|
||||
|
||||
player: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "profile"
|
||||
}
|
||||
ref: "profile",
|
||||
},
|
||||
};
|
||||
|
||||
const schema = new Schema<IStationLog>(raw_schema);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -63,8 +63,8 @@ export const raw_schema = {
|
||||
},
|
||||
player: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "profile"
|
||||
}
|
||||
ref: "profile",
|
||||
},
|
||||
};
|
||||
|
||||
const schema = new Schema<ITrainLog>(raw_schema);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
2
packages/backend/src/types/typings.d.ts
vendored
2
packages/backend/src/types/typings.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -18,7 +18,7 @@ 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 steamKeys: string[] = JSON.parse(process.env.STEAM_APIKEY!);
|
||||
|
||||
const steamFetch = (url: string) =>
|
||||
{
|
||||
@ -28,12 +28,13 @@ const steamFetch = (url: string) =>
|
||||
{
|
||||
const req = () =>
|
||||
{
|
||||
const steamKey = steamKeys[ Math.floor(Math.random() * steamKeys.length) ];
|
||||
|
||||
fetch(url, { signal: AbortSignal.timeout(10000) }).then(x => x.json())
|
||||
fetch(url.replace("[STEAMKEY]", steamKey), { signal: AbortSignal.timeout(10000) }).then(x => x.json())
|
||||
.then(x => res(x))
|
||||
.catch(() =>
|
||||
{
|
||||
console.log("STEAM request failed! ", url.replace(STEAM_API_KEY!, "[XXX]"), retries);
|
||||
console.log("STEAM request failed! ", url.replace("[STEAMKEY]", steamKey), retries);
|
||||
|
||||
retries++;
|
||||
setTimeout(() => req(), retries * 1000);
|
||||
@ -54,7 +55,7 @@ export class PlayerUtil
|
||||
|
||||
if (!player)
|
||||
{
|
||||
const data = await steamFetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${ STEAM_API_KEY }&format=json&steamids=${ steamId }`) as IPlayerPayload;
|
||||
const data = await steamFetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=[STEAMKEY]&format=json&steamids=${ steamId }`) as IPlayerPayload;
|
||||
|
||||
assert(data.response.players, "Expected data.response.players to be truthy");
|
||||
|
||||
@ -85,14 +86,14 @@ export class PlayerUtil
|
||||
|
||||
if (stats)
|
||||
{
|
||||
trainStats['N/A'] = {
|
||||
trainStats[ "N/A" ] = {
|
||||
score: stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0,
|
||||
distance: stats?.stats?.find(x => x.name === "DISTANCE_M")?.value ?? 0,
|
||||
time: 0,
|
||||
};
|
||||
|
||||
dispatcherStats['N/A'] = {
|
||||
time: (stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0) * 1000 * 60
|
||||
dispatcherStats[ "N/A" ] = {
|
||||
time: (stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0) * 1000 * 60,
|
||||
};
|
||||
|
||||
trainPoints = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
|
||||
@ -149,7 +150,7 @@ export class PlayerUtil
|
||||
|
||||
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 }`) as IPlayerPayload;
|
||||
const data = await steamFetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=[STEAMKEY]&format=json&steamids=${ steamId }`) as IPlayerPayload;
|
||||
|
||||
if (!data?.response?.players?.length)
|
||||
{
|
||||
@ -161,7 +162,7 @@ export class PlayerUtil
|
||||
|
||||
public static async getPlayerStats(steamId: string)
|
||||
{
|
||||
const data = await steamFetch(`https://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=[STEAMKEY]&steamid=${ steamId }`) as IPlayerStatsPayload;
|
||||
|
||||
if (!data.playerstats?.stats)
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -65,7 +65,8 @@ export class SimrailClient extends EventEmitter
|
||||
public constructor()
|
||||
{
|
||||
super();
|
||||
this.setup().then(() => {
|
||||
this.setup().then(() =>
|
||||
{
|
||||
void this.update(false);
|
||||
});
|
||||
|
||||
@ -76,38 +77,48 @@ export class SimrailClient extends EventEmitter
|
||||
|
||||
private async setup()
|
||||
{
|
||||
if (!await redis.get('last_updated')) {
|
||||
await redis.json.set('trains_occupied', '$', {});
|
||||
await redis.json.set('trains', '$', []);
|
||||
await redis.json.set('stations', '$', []);
|
||||
await redis.json.set('stations_occupied', '$', {});
|
||||
if (!await redis.get("last_updated"))
|
||||
{
|
||||
await redis.json.set("trains_occupied", "$", {});
|
||||
await redis.json.set("trains", "$", []);
|
||||
await redis.json.set("stations", "$", []);
|
||||
await redis.json.set("stations_occupied", "$", {});
|
||||
}
|
||||
|
||||
const lastUpdated = Date.now() - (Number(await redis.get('last_updated')) ?? 0);
|
||||
const lastUpdated = Date.now() - (Number(await redis.get("last_updated")) ?? 0);
|
||||
|
||||
if (lastUpdated > 300_000) {
|
||||
console.log('REDIS: last updated more than > 5 mins');
|
||||
await redis.json.set('trains_occupied', '$', {});
|
||||
await redis.json.set('trains', '$', []);
|
||||
await redis.json.set('stations', '$', []);
|
||||
await redis.json.set('stations_occupied', '$', {});
|
||||
if (lastUpdated > 300_000)
|
||||
{
|
||||
console.log("REDIS: last updated more than > 5 mins");
|
||||
await redis.json.set("trains_occupied", "$", {});
|
||||
await redis.json.set("trains", "$", []);
|
||||
await redis.json.set("stations", "$", []);
|
||||
await redis.json.set("stations_occupied", "$", {});
|
||||
}
|
||||
|
||||
if (!await redis.json.get('stations'))
|
||||
redis.json.set('stations', '$', []);
|
||||
if (!await redis.json.get('trains'))
|
||||
redis.json.set('trains', '$', []);
|
||||
if (!await redis.json.get('trains_occupied'))
|
||||
redis.json.set('trains_occupied', '$', {});
|
||||
if (!await redis.json.get('stations_occupied'))
|
||||
redis.json.set('stations_occupied', '$', {});
|
||||
if (!await redis.json.get("stations"))
|
||||
{
|
||||
redis.json.set("stations", "$", []);
|
||||
}
|
||||
if (!await redis.json.get("trains"))
|
||||
{
|
||||
redis.json.set("trains", "$", []);
|
||||
}
|
||||
if (!await redis.json.get("trains_occupied"))
|
||||
{
|
||||
redis.json.set("trains_occupied", "$", {});
|
||||
}
|
||||
if (!await redis.json.get("stations_occupied"))
|
||||
{
|
||||
redis.json.set("stations_occupied", "$", {});
|
||||
}
|
||||
|
||||
this.stations = (await redis.json.get('stations') as unknown as SimrailClient['stations']);
|
||||
this.stationsOccupied = (await redis.json.get('stations_occupied') as unknown as SimrailClient['stationsOccupied']);
|
||||
this.trains = (await redis.json.get('trains') as unknown as SimrailClient['trains']);
|
||||
this.trainsOccupied = (await redis.json.get('trains_occupied') as unknown as SimrailClient['trainsOccupied']);
|
||||
this.stations = (await redis.json.get("stations") as unknown as SimrailClient["stations"]);
|
||||
this.stationsOccupied = (await redis.json.get("stations_occupied") as unknown as SimrailClient["stationsOccupied"]);
|
||||
this.trains = (await redis.json.get("trains") as unknown as SimrailClient["trains"]);
|
||||
this.trainsOccupied = (await redis.json.get("trains_occupied") as unknown as SimrailClient["trainsOccupied"]);
|
||||
|
||||
redis.set('last_updated', Date.now().toString());
|
||||
redis.set("last_updated", Date.now().toString());
|
||||
}
|
||||
|
||||
private async processStation(server: Server, stations: ApiResponse<Station>)
|
||||
@ -127,7 +138,7 @@ export class SimrailClient extends EventEmitter
|
||||
{
|
||||
this.stations[ server.ServerCode ] = stations.data;
|
||||
redis.json.set("stations", "$", this.stations);
|
||||
redis.set('last_updated', Date.now().toString());
|
||||
redis.set("last_updated", Date.now().toString());
|
||||
}
|
||||
|
||||
for (const x of stations.data)
|
||||
@ -167,7 +178,7 @@ export class SimrailClient extends EventEmitter
|
||||
|
||||
this.stations[ server.ServerCode ] = stations.data;
|
||||
redis.json.set("stations", "$", this.stations);
|
||||
redis.set('last_updated', Date.now().toString());
|
||||
redis.set("last_updated", Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +199,7 @@ export class SimrailClient extends EventEmitter
|
||||
{
|
||||
this.trains[ server.ServerCode ] = trains.data;
|
||||
redis.json.set("trains", "$", this.trains);
|
||||
redis.set('last_updated', Date.now().toString());
|
||||
redis.set("last_updated", Date.now().toString());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -252,13 +263,15 @@ export class SimrailClient extends EventEmitter
|
||||
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`);
|
||||
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`);
|
||||
if (points < 0)
|
||||
{
|
||||
console.warn(`Player ${ playerId }, Train ${ data.TrainNoLocal } - distance < 0`);
|
||||
points = 0;
|
||||
}
|
||||
|
||||
@ -273,14 +286,14 @@ export class SimrailClient extends EventEmitter
|
||||
this.trains[ server.ServerCode ] = trains.data;
|
||||
redis.json.set("trains", "$", this.trains);
|
||||
redis.json.set("trains_occupied", "$", this.trainsOccupied);
|
||||
redis.set('last_updated', Date.now().toString());
|
||||
redis.set("last_updated", Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private async update(needSetup: boolean = false)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -290,7 +303,7 @@ export class SimrailClient extends EventEmitter
|
||||
return;
|
||||
}
|
||||
|
||||
if (needSetup)
|
||||
if (needSetup)
|
||||
{
|
||||
await this.setup();
|
||||
}
|
||||
@ -299,8 +312,8 @@ export class SimrailClient extends EventEmitter
|
||||
// TODO: check performance
|
||||
for (const server of servers)
|
||||
{
|
||||
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 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>;
|
||||
|
||||
await this.processStation(server, stations);
|
||||
await this.processTrain(server, trains);
|
||||
|
@ -1,4 +1,21 @@
|
||||
import wcmatch from 'wildcard-match'
|
||||
/*
|
||||
* Copyright (C) 2025 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 wcmatch from "wildcard-match";
|
||||
|
||||
/*
|
||||
E186_134 = "Traxx/E186-134",
|
||||
@ -48,85 +65,82 @@ E186_134 = "Traxx/E186-134",
|
||||
|
||||
export const trainsList = [
|
||||
{
|
||||
train: 'Traxx (E186)',
|
||||
train: "Traxx (E186)",
|
||||
pattern: [
|
||||
'Traxx/E186-*',
|
||||
]
|
||||
"Traxx/E186-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'Dragon2 (E6ACTa, E6ACTadb)',
|
||||
train: "Dragon2 (E6ACTa, E6ACTadb)",
|
||||
pattern: [
|
||||
'Dragon2/E6ACTa-*',
|
||||
'Dragon2/E6ACTadb-*'
|
||||
]
|
||||
"Dragon2/E6ACTa-*",
|
||||
"Dragon2/E6ACTadb-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'Dragon2 (ET25)',
|
||||
train: "Dragon2 (ET25)",
|
||||
pattern: [
|
||||
'Dragon2/ET25-*',
|
||||
]
|
||||
"Dragon2/ET25-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'Pendolino (ED250)',
|
||||
train: "Pendolino (ED250)",
|
||||
pattern: [
|
||||
'Pendolino/ED250-*',
|
||||
]
|
||||
"Pendolino/ED250-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'EN57',
|
||||
train: "EN57",
|
||||
pattern: [
|
||||
'EN57/EN57-*',
|
||||
]
|
||||
"EN57/EN57-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'EN71',
|
||||
train: "EN71",
|
||||
pattern: [
|
||||
'EN57/EN71-*',
|
||||
]
|
||||
"EN57/EN71-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'EN76',
|
||||
train: "EN76",
|
||||
pattern: [
|
||||
'Elf/EN76-*',
|
||||
]
|
||||
"Elf/EN76-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'EN96',
|
||||
train: "EN96",
|
||||
pattern: [
|
||||
'Elf/EN96-*',
|
||||
]
|
||||
"Elf/EN96-*",
|
||||
],
|
||||
},
|
||||
{
|
||||
train: 'EP07',
|
||||
train: "EP07",
|
||||
pattern: [
|
||||
'4E/EP07-*',
|
||||
]
|
||||
"4E/EP07-*",
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
train: 'EP08',
|
||||
train: "EP08",
|
||||
pattern: [
|
||||
'4E/EP08-*',
|
||||
]
|
||||
"4E/EP08-*",
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
train: 'ET22',
|
||||
train: "ET22",
|
||||
pattern: [
|
||||
'201E/ET22-*',
|
||||
]
|
||||
"201E/ET22-*",
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
train: 'EU07',
|
||||
train: "EU07",
|
||||
pattern: [
|
||||
'4E/EU07-*',
|
||||
]
|
||||
}
|
||||
]
|
||||
"4E/EU07-*",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export const getVehicle = (name: string) => {
|
||||
export const getVehicle = (name: string) =>
|
||||
{
|
||||
return trainsList.find(x => wcmatch(x.pattern)(name))?.train;
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -26,17 +26,23 @@ export const removeProperties = <T>(data: any, names: string[]) =>
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
|
||||
export const escapeRegexString = (str: string) => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
export const escapeRegexString = (str: string) =>
|
||||
{
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
export const isTruthyAndGreaterThanZero = (data: number) => {
|
||||
if (!data) return false;
|
||||
export const isTruthyAndGreaterThanZero = (data: number) =>
|
||||
{
|
||||
if (!data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return data > 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const arrayGroupBy = <T>(array: T[], predicate: (value: T, index: number, array: T[]) => string) =>
|
||||
Object.values((array.reduce((acc, value, index, array) => {
|
||||
(acc[predicate(value, index, array)] ||= []).push(value);
|
||||
Object.values((array.reduce((acc, value, index, array) =>
|
||||
{
|
||||
(acc[ predicate(value, index, array) ] ||= []).push(value);
|
||||
return acc;
|
||||
}, {} as { [key: string]: T[] }))).flat();
|
||||
}, {} as { [ key: string ]: T[] }))).flat();
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -18,36 +18,38 @@ import { execSync } from "child_process";
|
||||
|
||||
export class GitUtil
|
||||
{
|
||||
private static cache: { lastUpdated: number, version?: string, commit?: string } = undefined!;
|
||||
private static cache: { version?: string, commit?: string } = undefined!;
|
||||
|
||||
public static getLatestVersion()
|
||||
private static getLatestVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = execSync("git describe --tags --exact-match").toString();
|
||||
return data.replace("\n", "");
|
||||
} catch
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
return process.env.CURRENT_VERSION;
|
||||
// try
|
||||
// {
|
||||
// const data = execSync("git describe --tags --exact-match").toString();
|
||||
// return data.replace("\n", "");
|
||||
// } catch
|
||||
// {
|
||||
// return undefined;
|
||||
// }
|
||||
}
|
||||
|
||||
public static getLatestCommit()
|
||||
private static getLatestCommit()
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = execSync("git rev-parse --short HEAD").toString();
|
||||
return data.replace("\n", "");
|
||||
} catch
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
return process.env.CURRENT_COMMIT;
|
||||
// try
|
||||
// {
|
||||
// const data = execSync("git rev-parse --short HEAD").toString();
|
||||
// return data.replace("\n", "");
|
||||
// } catch
|
||||
// {
|
||||
// return undefined;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
public static getData()
|
||||
{
|
||||
if (this.cache && (this.cache.lastUpdated - Date.now()) < 30_000)
|
||||
if (this.cache)
|
||||
{
|
||||
return this.cache;
|
||||
}
|
||||
@ -55,7 +57,6 @@ export class GitUtil
|
||||
const data = {
|
||||
version: this.getLatestVersion(),
|
||||
commit: this.getLatestCommit(),
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
|
||||
this.cache = data;
|
||||
|
42
packages/backend/src/util/imgproxy.ts
Normal file
42
packages/backend/src/util/imgproxy.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { createHmac, randomBytes } from "node:crypto";
|
||||
import { GitUtil } from "./git.js";
|
||||
|
||||
export const imgProxySign = (target: string) =>
|
||||
{
|
||||
const hmac = createHmac("sha256", Buffer.from(process.env.IMGPROXY_KEY!, 'hex'));
|
||||
hmac.update(Buffer.from(process.env.IMGPROXY_SALT!, 'hex'));
|
||||
hmac.update(target);
|
||||
|
||||
return hmac.digest("base64url");
|
||||
};
|
||||
|
||||
export const generateUrl = (url: string, options: string = "rs:auto:128:128:1/f:png") =>
|
||||
{
|
||||
if (url.includes('https://proxy.cdn.alekswilc.dev/')) return url;
|
||||
|
||||
if (process.env.NODE_ENV === "development")
|
||||
{
|
||||
options += "/cb:" + randomBytes(4).toString('hex');
|
||||
} else if (GitUtil.getData().version) {
|
||||
options += "/cb:" + GitUtil.getData().version;
|
||||
}
|
||||
|
||||
const signature = imgProxySign(`/${ options }/plain/${ url }`);
|
||||
return `https://proxy.cdn.alekswilc.dev/${ signature }/${ options }/plain/${ url }`;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -55,7 +55,7 @@
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "../../dist/backend", /* Specify an output folder for all emitted files. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
|
File diff suppressed because it is too large
Load Diff
17
packages/frontend/Dockerfile
Normal file
17
packages/frontend/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN npm install
|
||||
|
||||
RUN npm i -g serve vite
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run rawbuild
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "serve", "-s", "dist" ]
|
10
packages/frontend/frontend.iml
Normal file
10
packages/frontend/frontend.iml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="script" level="application" />
|
||||
</component>
|
||||
</module>
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
~ Copyright (C) 2025 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
|
||||
@ -19,6 +19,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/jpg" href="/favicon.png"/>
|
||||
<link rel="preconnect" href="https://umami.alekswilc.dev/script.js">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>simrail.pro | Simrail Logs</title>
|
||||
|
||||
|
@ -5,29 +5,23 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"rawbuild": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build": "docker build --progress=plain -t simrailpro:frontend ."
|
||||
},
|
||||
"dependencies": {
|
||||
"apexcharts": "^3.41.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"flatpickr": "^4.6.13",
|
||||
"headlessui": "^0.0.0",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"match-sorter": "^6.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-apexcharts": "^1.4.1",
|
||||
"react-cookie": "^7.2.2",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^15.0.2",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"react-toastify": "^10.0.6",
|
||||
"sort-by": "^0.0.2",
|
||||
"swr": "^2.2.5",
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
@ -42,7 +36,7 @@
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^4.4.7",
|
||||
"vite": "^6.2.0",
|
||||
"webpack": "^5.88.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
4
packages/frontend/public/robots.txt
Normal file
4
packages/frontend/public/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Disallow: /api/
|
||||
Disallow: /cgi-bin/
|
||||
Sitemap: https://simrail.pro/sitemap.xml
|
42
packages/frontend/public/sitemap.xml
Normal file
42
packages/frontend/public/sitemap.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://simrail.pro/</loc>
|
||||
<lastmod>2024-12-24T23:09:07+00:00</lastmod>
|
||||
<priority>1.00</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://simrail.pro/profiles</loc>
|
||||
<lastmod>2024-12-24T23:09:07+00:00</lastmod>
|
||||
<priority>0.90</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://simrail.pro/leaderboard</loc>
|
||||
<lastmod>2024-12-24T23:09:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://simrail.pro/logs/stations</loc>
|
||||
<lastmod>2024-12-24T23:09:07+00:00</lastmod>
|
||||
<priority>0.70</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://simrail.pro/logs/trains</loc>
|
||||
<lastmod>2024-12-24T23:09:07+00:00</lastmod>
|
||||
<priority>0.70</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://simrail.pro/active/stations</loc>
|
||||
<lastmod>2024-12-24T23:09:07+00:00</lastmod>
|
||||
<priority>0.60</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://simrail.pro/active/trains</loc>
|
||||
<lastmod>2024-12-24T23:09:07+00:00</lastmod>
|
||||
<priority>0.60</priority>
|
||||
</url>
|
||||
</urlset>
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -21,11 +21,10 @@ import { Loader } from "./components/mini/loaders/PageLoader.tsx";
|
||||
import { Home } from "./pages/Home";
|
||||
import DefaultLayout from "./layout/DefaultLayout";
|
||||
import "./i18n";
|
||||
import { TrainLeaderboard } from "./pages/leaderboard/TrainLeaderboard.tsx";
|
||||
import { StationLeaderboard } from "./pages/leaderboard/StationsLeaderboard.tsx";
|
||||
import { Leaderboard } from "./pages/leaderboard/Leaderboard.tsx";
|
||||
import { TrainLogs } from "./pages/logs/TrainLogs.tsx";
|
||||
import { StationLogs } from "./pages/logs/StationLogs.tsx";
|
||||
import { Profile } from "./pages/profile/Profile.tsx";
|
||||
import { Profile } from "./pages/profiles/Profile.tsx";
|
||||
import { Log } from "./pages/log/Log.tsx";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
@ -36,6 +35,7 @@ import { ActiveStationsPlayers } from "./pages/activePlayers/ActiveStationsPlaye
|
||||
import { ActiveTrainPlayers } from "./pages/activePlayers/ActiveTrainPlayers.tsx";
|
||||
import { AuthProvider } from "./hooks/useAuth.tsx";
|
||||
import { NotFoundError } from "./pages/errors/NotFound.tsx";
|
||||
import { Profiles } from "./pages/profiles/Profiles.tsx";
|
||||
|
||||
function App()
|
||||
{
|
||||
@ -55,135 +55,135 @@ function App()
|
||||
|
||||
return <HelmetProvider>
|
||||
<AuthProvider>
|
||||
{ loading ? (
|
||||
<Loader/>
|
||||
) : (
|
||||
<>
|
||||
<ToastContainer
|
||||
position="top-center"
|
||||
autoClose={ 1500 }
|
||||
hideProgressBar={ false }
|
||||
newestOnTop={ false }
|
||||
closeOnClick
|
||||
rtl={ false }
|
||||
pauseOnHover
|
||||
theme={ theme as "light" | "dark" }
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Home"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<Home/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/leaderboard/trains"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Train Leaderboard"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<TrainLeaderboard/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{ loading ? (
|
||||
<Loader/>
|
||||
) : (
|
||||
<>
|
||||
<ToastContainer
|
||||
position="top-center"
|
||||
autoClose={ 1500 }
|
||||
hideProgressBar={ false }
|
||||
newestOnTop={ false }
|
||||
closeOnClick
|
||||
rtl={ false }
|
||||
pauseOnHover
|
||||
theme={ theme as "light" | "dark" }
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Home"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<Home/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/leaderboard/"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Train Leaderboard"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<Leaderboard/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/logs/trains"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Trains Logs"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<TrainLogs/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/logs/trains"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Trains Logs"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<TrainLogs/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/logs/stations"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Stations Logs"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<StationLogs/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/logs/stations"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Stations Logs"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<StationLogs/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/leaderboard/stations"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Station Leaderboard"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<StationLeaderboard/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/active/trains"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Active Trains"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<ActiveTrainPlayers/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/active/stations"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Active Station"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<ActiveStationsPlayers/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/active/trains"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Active Trains"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<ActiveTrainPlayers/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profile/:id"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Profile"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
{/* page meta is modified in component! */ }
|
||||
<Profile/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/active/stations"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Active Station"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
<ActiveStationsPlayers/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profiles/"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Profiles"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
{/* page meta is modified in component! */ }
|
||||
<Profiles/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/profile/:id"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Profile"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
{/* page meta is modified in component! */ }
|
||||
<Profile/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/log/:id"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Log"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
{/* page title is modified after API response */ }
|
||||
<Log/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/log/:id"
|
||||
element={
|
||||
<>
|
||||
<PageMeta title="simrail.pro | Log"
|
||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||
{/* page title is modified after API response */ }
|
||||
<Log/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<>
|
||||
<NotFoundError/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</DefaultLayout>
|
||||
</>
|
||||
) }
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<>
|
||||
<NotFoundError/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</DefaultLayout>
|
||||
</>
|
||||
) }
|
||||
</AuthProvider>
|
||||
</HelmetProvider>;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -45,10 +45,10 @@ const DarkModeSwitcher = () =>
|
||||
}` }
|
||||
>
|
||||
<span className="dark:hidden">
|
||||
<LightIcon/>
|
||||
<LightIcon />
|
||||
</span>
|
||||
<span className="hidden dark:inline-block">
|
||||
<DarkIcon/>
|
||||
<DarkIcon />
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -76,10 +76,13 @@ export const Header = (props: {
|
||||
<div className="flex items-center gap-3 2xsm:gap-7">
|
||||
<ul className="flex items-center gap-2 2xsm:gap-4">
|
||||
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("pl") }>
|
||||
<ReactCountryFlag countryCode={ "PL" } svg/>
|
||||
<ReactCountryFlag countryCode={ "PL" } svg alt={ "PL" }/>
|
||||
</a>
|
||||
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("en") }>
|
||||
<ReactCountryFlag countryCode={ "US" } svg/>
|
||||
<ReactCountryFlag countryCode={ "US" } svg alt={ "EN" }/>
|
||||
</a>
|
||||
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("cs") }>
|
||||
<ReactCountryFlag countryCode={ "CZ" } svg alt={ "CZ" }/>
|
||||
</a>
|
||||
</ul>
|
||||
<ul className="flex items-center gap-2 2xsm:gap-4">
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
43
packages/frontend/src/components/mini/icons/UserIcons.tsx
Normal file
43
packages/frontend/src/components/mini/icons/UserIcons.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { ReactNode, useState } from "react";
|
||||
import { FaUserShield, FaUserSlash, FaUserLock } from "react-icons/fa6";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const UseHover = ({ children, hover }: { children: ReactNode, hover: ReactNode }) => {
|
||||
const [isHovered, setIsHovered] = useState(true);
|
||||
|
||||
|
||||
return <div className={"inline"} onMouseEnter={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(true)}>
|
||||
{isHovered ? children : hover}
|
||||
</div>
|
||||
}
|
||||
|
||||
export const UserIcons = ({ flags }: { flags: string[] }) =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <> { flags.includes('administrator') &&
|
||||
<UseHover hover={ <button
|
||||
className="inline-flex rounded-full bg-[#DC3545] py-1 px-3 text-sm font-medium text-white hover:bg-opacity-90">{t("icons.admin")}</button> }><FaUserShield
|
||||
className={ "inline text-meta-1 ml-1" }/></UseHover> } { flags.includes("leaderboard_hidden") &&
|
||||
<UseHover hover={ <button
|
||||
className="inline-flex rounded-full bg-[#F9C107] py-1 px-3 text-sm font-medium text-[#212B36] hover:bg-opacity-90">{t("icons.leaderboard_hidden")}</button> }><FaUserLock
|
||||
className={ "inline text-meta-6 ml-1" }/></UseHover> } { flags.includes("hidden") &&
|
||||
<UseHover hover={ <button
|
||||
className="inline-flex rounded-full bg-[#DC3545] py-1 px-3 text-sm font-medium text-white hover:bg-opacity-90">{ t("icons.hidden") }</button> }>
|
||||
<FaUserSlash className={ "inline text-meta-1 ml-1" }/></UseHover> }</>;
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -19,7 +19,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ErrorAlertIcon } from "../icons/AlertIcons.tsx";
|
||||
|
||||
|
||||
export const LoadError = () =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,61 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export const ConfirmModal = ({ showModal, setShowModal, onConfirm, title, description }: { showModal: boolean; setShowModal: Dispatch<SetStateAction<boolean>>; onConfirm: () => void; title: string; description: string; }) => {
|
||||
export const ConfirmModal = ({ showModal, setShowModal, onConfirm, title, description }: {
|
||||
showModal: boolean;
|
||||
setShowModal: Dispatch<SetStateAction<boolean>>;
|
||||
onConfirm: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
}) =>
|
||||
{
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal ? (
|
||||
<>
|
||||
<div
|
||||
className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||
>
|
||||
<div className="relative w-auto my-6 mx-auto max-w-3xl">
|
||||
<>
|
||||
{ showModal ? (
|
||||
<>
|
||||
<div
|
||||
className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||
>
|
||||
<div className="relative w-auto my-6 mx-auto max-w-3xl">
|
||||
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-strokedark outline-none focus:outline-none">
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-strokedark outline-none focus:outline-none">
|
||||
|
||||
<div className="flex items-start justify-between p-5">
|
||||
<h3 className="text-3xl font-semibold text-meta-2">
|
||||
{ title }
|
||||
</h3>
|
||||
<button
|
||||
className="p-1 ml-auto bg-transparent border-0 text-meta-2 opacity-5 float-right text-3xl leading-none font-semibold outline-none focus:outline-none"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
<div className="flex items-start justify-between p-5">
|
||||
<h3 className="text-3xl font-semibold text-meta-2">
|
||||
{ title }
|
||||
</h3>
|
||||
<button
|
||||
className="p-1 ml-auto bg-transparent border-0 text-meta-2 opacity-5 float-right text-3xl leading-none font-semibold outline-none focus:outline-none"
|
||||
onClick={ () => setShowModal(false) }
|
||||
>
|
||||
<span className="bg-transparent text-meta-2 opacity-5 h-6 w-6 text-2xl block outline-none focus:outline-none">
|
||||
×
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative p-6 flex-auto">
|
||||
<p className="my-4 text-blueGray-500 text-lg leading-relaxed text-meta-2">
|
||||
{ description }
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative p-6 flex-auto">
|
||||
<p className="my-4 text-blueGray-500 text-lg leading-relaxed text-meta-2">
|
||||
{ description }
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-end p-6 rounded-b">
|
||||
<button
|
||||
className="text-red-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
||||
type="button"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
className="bg-emerald-500 text-white active:bg-emerald-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
||||
type="button"
|
||||
onClick={() => { setShowModal(false); onConfirm(); }}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<div className="flex items-center justify-end p-6 rounded-b">
|
||||
<button
|
||||
className="text-red-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
||||
type="button"
|
||||
onClick={ () => setShowModal(false) }
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
className="bg-emerald-500 text-white active:bg-emerald-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
||||
type="button"
|
||||
onClick={ () =>
|
||||
{
|
||||
setShowModal(false);
|
||||
onConfirm();
|
||||
} }
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
</>
|
||||
) : null }
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx";
|
||||
import { ArrowIcon } from "../icons/ArrowIcon.tsx";
|
||||
import { FaHome, FaClipboardList } from "react-icons/fa";
|
||||
import { FaChartSimple, FaTrain, FaBuildingFlag, FaBolt } from "react-icons/fa6";
|
||||
import { FaChartSimple, FaTrain, FaBuildingFlag, FaBolt, FaUsers } from "react-icons/fa6";
|
||||
import { useAuth } from "../../../hooks/useAuth.tsx";
|
||||
|
||||
interface SidebarProps
|
||||
@ -101,8 +101,7 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
return (
|
||||
<aside
|
||||
ref={ sidebar }
|
||||
className={ `absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
|
||||
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||
className={ `absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${ sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||
}` }
|
||||
>
|
||||
{/* <!-- SIDEBAR HEADER --> */ }
|
||||
@ -126,9 +125,8 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
<li>
|
||||
<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 === "/" &&
|
||||
"bg-graydark dark: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 === "/" &&
|
||||
"bg-graydark dark:bg-meta-4"
|
||||
}` }
|
||||
>
|
||||
<FaHome/>
|
||||
@ -143,7 +141,32 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
{ t("sidebar.info") }
|
||||
</h3>
|
||||
|
||||
<li>
|
||||
<NavLink
|
||||
to="/profiles"
|
||||
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 === "/profiles" &&
|
||||
"bg-graydark dark:bg-meta-4"
|
||||
}` }
|
||||
>
|
||||
<FaUsers/>
|
||||
{ t("sidebar.profiles") }
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<NavLink
|
||||
to="/leaderboard"
|
||||
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.includes("/leaderboard") &&
|
||||
"bg-graydark dark:bg-meta-4"
|
||||
}` }
|
||||
>
|
||||
<FaChartSimple/>
|
||||
{ t("sidebar.leaderboard") }
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<SidebarLinkGroup
|
||||
isOpen={ true }
|
||||
activeCondition={
|
||||
pathname === "/logs" || pathname.includes("logs")
|
||||
}
|
||||
@ -154,10 +177,9 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
<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 === "/logs" ||
|
||||
pathname.includes("logs")) &&
|
||||
"bg-graydark dark: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 === "/logs" ||
|
||||
pathname.includes("logs")) &&
|
||||
"bg-graydark dark:bg-meta-4"
|
||||
}` }
|
||||
onClick={ (e) =>
|
||||
{
|
||||
@ -173,8 +195,7 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
</NavLink>
|
||||
|
||||
<div
|
||||
className={ `translate transform overflow-hidden ${
|
||||
!open && "hidden"
|
||||
className={ `translate transform overflow-hidden ${ !open && "hidden"
|
||||
}` }
|
||||
>
|
||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||
@ -210,72 +231,8 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
</SidebarLinkGroup>
|
||||
|
||||
<SidebarLinkGroup
|
||||
activeCondition={
|
||||
pathname === "/leaderboard" || pathname.includes("leaderboard")
|
||||
}
|
||||
>
|
||||
{ (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" ||
|
||||
(pathname.includes("leaderboard/") && !pathname.includes('leaderboard/steam'))) &&
|
||||
"bg-graydark dark:bg-meta-4"
|
||||
}` }
|
||||
onClick={ (e) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
sidebarExpanded
|
||||
? handleClick()
|
||||
: setSidebarExpanded(true);
|
||||
} }
|
||||
>
|
||||
<FaChartSimple/>
|
||||
{ t("sidebar.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/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/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>
|
||||
isOpen={ true }
|
||||
|
||||
|
||||
<SidebarLinkGroup
|
||||
activeCondition={
|
||||
pathname === "/active/trains" || pathname.includes("active/trains")
|
||||
}
|
||||
@ -286,10 +243,9 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
<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 === "/active" ||
|
||||
pathname.includes("/active/")) &&
|
||||
"bg-graydark dark: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 === "/active" ||
|
||||
pathname.includes("/active/")) &&
|
||||
"bg-graydark dark:bg-meta-4"
|
||||
}` }
|
||||
onClick={ (e) =>
|
||||
{
|
||||
@ -299,13 +255,12 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
: setSidebarExpanded(true);
|
||||
} }
|
||||
>
|
||||
<FaBolt />
|
||||
<FaBolt/>
|
||||
{ t("sidebar.active_players") }
|
||||
<ArrowIcon rotated={ open }/>
|
||||
</NavLink>
|
||||
<div
|
||||
className={ `translate transform overflow-hidden ${
|
||||
!open && "hidden"
|
||||
className={ `translate transform overflow-hidden ${ !open && "hidden"
|
||||
}` }
|
||||
>
|
||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||
@ -347,286 +302,19 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
||||
</h3>
|
||||
|
||||
<li>
|
||||
<p className="group relative flex items-center rounded-sm py-2 px-4 text-sm text-bodydark1 duration-300 ease-in-out ">{t("sidebar.logged", { username })}</p>
|
||||
<p className="group relative flex items-center rounded-sm py-2 px-4 text-sm text-bodydark1 duration-300 ease-in-out ">{ t("sidebar.logged", { username }) }</p>
|
||||
</li>
|
||||
<button onClick={() => {
|
||||
window.localStorage.setItem('auth_token', 'undefined');
|
||||
<button onClick={ () =>
|
||||
{
|
||||
window.localStorage.setItem("auth_token", "undefined");
|
||||
window.location.reload();
|
||||
}} className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center text-sm text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||
>{t("sidebar.logout")}
|
||||
} }
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center text-sm text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||
>{ t("sidebar.logout") }
|
||||
</button>
|
||||
|
||||
</ul> }
|
||||
</div>
|
||||
|
||||
{/* TODO: add admin panel with simple auth */ }
|
||||
{/*{false && <div>*/ }
|
||||
{/* <h3 className="mb-4 ml-4 text-sm font-semibold text-bodydark2">*/ }
|
||||
{/* {t('sidebar.admin')}*/ }
|
||||
{/* </h3>*/ }
|
||||
|
||||
{/* <ul className="mb-6 flex flex-col gap-1.5">*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Chart --> */ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/chart"*/ }
|
||||
{/* 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.includes('chart') && 'bg-graydark dark:bg-meta-4'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* >*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className="fill-current"*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="19"*/ }
|
||||
{/* viewBox="0 0 18 19"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <g clipPath="url(#clip0_130_9801)">*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M10.8563 0.55835C10.5188 0.55835 10.2095 0.8396 10.2095 1.20522V6.83022C10.2095 7.16773 10.4907 7.4771 10.8563 7.4771H16.8751C17.0438 7.4771 17.2126 7.39272 17.3251 7.28022C17.4376 7.1396 17.4938 6.97085 17.4938 6.8021C17.2688 3.28647 14.3438 0.55835 10.8563 0.55835ZM11.4751 6.15522V1.8521C13.8095 2.13335 15.6938 3.8771 16.1438 6.18335H11.4751V6.15522Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M15.3845 8.7427H9.1126V2.69582C9.1126 2.35832 8.83135 2.07707 8.49385 2.07707C8.40947 2.07707 8.3251 2.07707 8.24072 2.07707C3.96572 2.04895 0.506348 5.53645 0.506348 9.81145C0.506348 14.0864 3.99385 17.5739 8.26885 17.5739C12.5438 17.5739 16.0313 14.0864 16.0313 9.81145C16.0313 9.6427 16.0313 9.47395 16.0032 9.33332C16.0032 8.99582 15.722 8.7427 15.3845 8.7427ZM8.26885 16.3083C4.66885 16.3083 1.77197 13.4114 1.77197 9.81145C1.77197 6.3802 4.47197 3.53957 7.8751 3.3427V9.36145C7.8751 9.69895 8.15635 10.0083 8.52197 10.0083H14.7938C14.6813 13.4958 11.7845 16.3083 8.26885 16.3083Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </g>*/ }
|
||||
{/* <defs>*/ }
|
||||
{/* <clipPath id="clip0_130_9801">*/ }
|
||||
{/* <rect*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="18"*/ }
|
||||
{/* fill="white"*/ }
|
||||
{/* transform="translate(0 0.052124)"*/ }
|
||||
{/* />*/ }
|
||||
{/* </clipPath>*/ }
|
||||
{/* </defs>*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* Chart*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Chart --> */ }
|
||||
|
||||
{/* */ }{/*{/* <!-- Menu Item Ui Elements --> */ }
|
||||
{/* <SidebarLinkGroup*/ }
|
||||
{/* activeCondition={pathname === '/ui' || pathname.includes('ui')}*/ }
|
||||
{/* >*/ }
|
||||
{/* {(handleClick, open) => {*/ }
|
||||
{/* return (*/ }
|
||||
{/* <React.Fragment>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="#"*/ }
|
||||
{/* className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${*/ }
|
||||
{/* (pathname === '/ui' || pathname.includes('ui')) &&*/ }
|
||||
{/* 'bg-graydark dark:bg-meta-4'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* onClick={(e) => {*/ }
|
||||
{/* e.preventDefault();*/ }
|
||||
{/* sidebarExpanded*/ }
|
||||
{/* ? handleClick()*/ }
|
||||
{/* : setSidebarExpanded(true);*/ }
|
||||
{/* }}*/ }
|
||||
{/* >*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className="fill-current"*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="19"*/ }
|
||||
{/* viewBox="0 0 18 19"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <g clipPath="url(#clip0_130_9807)">*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M15.7501 0.55835H2.2501C1.29385 0.55835 0.506348 1.34585 0.506348 2.3021V7.53335C0.506348 8.4896 1.29385 9.2771 2.2501 9.2771H15.7501C16.7063 9.2771 17.4938 8.4896 17.4938 7.53335V2.3021C17.4938 1.34585 16.7063 0.55835 15.7501 0.55835ZM16.2563 7.53335C16.2563 7.8146 16.0313 8.0396 15.7501 8.0396H2.2501C1.96885 8.0396 1.74385 7.8146 1.74385 7.53335V2.3021C1.74385 2.02085 1.96885 1.79585 2.2501 1.79585H15.7501C16.0313 1.79585 16.2563 2.02085 16.2563 2.3021V7.53335Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M6.13135 10.9646H2.2501C1.29385 10.9646 0.506348 11.7521 0.506348 12.7083V15.8021C0.506348 16.7583 1.29385 17.5458 2.2501 17.5458H6.13135C7.0876 17.5458 7.8751 16.7583 7.8751 15.8021V12.7083C7.90322 11.7521 7.11572 10.9646 6.13135 10.9646ZM6.6376 15.8021C6.6376 16.0833 6.4126 16.3083 6.13135 16.3083H2.2501C1.96885 16.3083 1.74385 16.0833 1.74385 15.8021V12.7083C1.74385 12.4271 1.96885 12.2021 2.2501 12.2021H6.13135C6.4126 12.2021 6.6376 12.4271 6.6376 12.7083V15.8021Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M15.75 10.9646H11.8688C10.9125 10.9646 10.125 11.7521 10.125 12.7083V15.8021C10.125 16.7583 10.9125 17.5458 11.8688 17.5458H15.75C16.7063 17.5458 17.4938 16.7583 17.4938 15.8021V12.7083C17.4938 11.7521 16.7063 10.9646 15.75 10.9646ZM16.2562 15.8021C16.2562 16.0833 16.0312 16.3083 15.75 16.3083H11.8688C11.5875 16.3083 11.3625 16.0833 11.3625 15.8021V12.7083C11.3625 12.4271 11.5875 12.2021 11.8688 12.2021H15.75C16.0312 12.2021 16.2562 12.4271 16.2562 12.7083V15.8021Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </g>*/ }
|
||||
{/* <defs>*/ }
|
||||
{/* <clipPath id="clip0_130_9807">*/ }
|
||||
{/* <rect*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="18"*/ }
|
||||
{/* fill="white"*/ }
|
||||
{/* transform="translate(0 0.052124)"*/ }
|
||||
{/* />*/ }
|
||||
{/* </clipPath>*/ }
|
||||
{/* </defs>*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* UI Elements*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/ }
|
||||
{/* open && 'rotate-180'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* width="20"*/ }
|
||||
{/* height="20"*/ }
|
||||
{/* viewBox="0 0 20 20"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <path*/ }
|
||||
{/* fillRule="evenodd"*/ }
|
||||
{/* clipRule="evenodd"*/ }
|
||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu Start --> */ }
|
||||
{/* <div*/ }
|
||||
{/* className={`translate transform overflow-hidden ${*/ }
|
||||
{/* !open && 'hidden'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* >*/ }
|
||||
{/* <ul className="mb-5.5 mt-4 flex flex-col gap-2.5 pl-6">*/ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/ui/alerts"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Alerts*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/ui/buttons"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Buttons*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* </ul>*/ }
|
||||
{/* </div>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu End --> */ }
|
||||
{/* </React.Fragment>*/ }
|
||||
{/* );*/ }
|
||||
{/* }}*/ }
|
||||
{/* </SidebarLinkGroup>*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Ui Elements --> */ }
|
||||
|
||||
{/* */ }{/*{/* <!-- Menu Item Auth Pages --> */ }
|
||||
{/* <SidebarLinkGroup*/ }
|
||||
{/* activeCondition={*/ }
|
||||
{/* pathname === '/auth' || pathname.includes('auth')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* {(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 === '/auth' || pathname.includes('auth')) &&*/ }
|
||||
{/* 'bg-graydark dark:bg-meta-4'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* onClick={(e) => {*/ }
|
||||
{/* e.preventDefault();*/ }
|
||||
{/* sidebarExpanded*/ }
|
||||
{/* ? handleClick()*/ }
|
||||
{/* : setSidebarExpanded(true);*/ }
|
||||
{/* }}*/ }
|
||||
{/* >*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className="fill-current"*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="19"*/ }
|
||||
{/* viewBox="0 0 18 19"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <g clipPath="url(#clip0_130_9814)">*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M12.7127 0.55835H9.53457C8.80332 0.55835 8.18457 1.1771 8.18457 1.90835V3.84897C8.18457 4.18647 8.46582 4.46772 8.80332 4.46772C9.14082 4.46772 9.45019 4.18647 9.45019 3.84897V1.88022C9.45019 1.82397 9.47832 1.79585 9.53457 1.79585H12.7127C13.3877 1.79585 13.9221 2.33022 13.9221 3.00522V15.0709C13.9221 15.7459 13.3877 16.2802 12.7127 16.2802H9.53457C9.47832 16.2802 9.45019 16.2521 9.45019 16.1959V14.2552C9.45019 13.9177 9.16894 13.6365 8.80332 13.6365C8.43769 13.6365 8.18457 13.9177 8.18457 14.2552V16.1959C8.18457 16.9271 8.80332 17.5459 9.53457 17.5459H12.7127C14.0908 17.5459 15.1877 16.4209 15.1877 15.0709V3.03335C15.1877 1.65522 14.0627 0.55835 12.7127 0.55835Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* <path*/ }
|
||||
{/* d="M10.4346 8.60205L7.62207 5.7333C7.36895 5.48018 6.97519 5.48018 6.72207 5.7333C6.46895 5.98643 6.46895 6.38018 6.72207 6.6333L8.46582 8.40518H3.45957C3.12207 8.40518 2.84082 8.68643 2.84082 9.02393C2.84082 9.36143 3.12207 9.64268 3.45957 9.64268H8.49395L6.72207 11.4427C6.46895 11.6958 6.46895 12.0896 6.72207 12.3427C6.83457 12.4552 7.00332 12.5114 7.17207 12.5114C7.34082 12.5114 7.50957 12.4552 7.62207 12.3145L10.4346 9.4458C10.6877 9.24893 10.6877 8.85518 10.4346 8.60205Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </g>*/ }
|
||||
{/* <defs>*/ }
|
||||
{/* <clipPath id="clip0_130_9814">*/ }
|
||||
{/* <rect*/ }
|
||||
{/* width="18"*/ }
|
||||
{/* height="18"*/ }
|
||||
{/* fill="white"*/ }
|
||||
{/* transform="translate(0 0.052124)"*/ }
|
||||
{/* />*/ }
|
||||
{/* </clipPath>*/ }
|
||||
{/* </defs>*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* Authentication*/ }
|
||||
{/* <svg*/ }
|
||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/ }
|
||||
{/* open && 'rotate-180'*/ }
|
||||
{/* }`}*/ }
|
||||
{/* width="20"*/ }
|
||||
{/* height="20"*/ }
|
||||
{/* viewBox="0 0 20 20"*/ }
|
||||
{/* fill="none"*/ }
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
||||
{/* >*/ }
|
||||
{/* <path*/ }
|
||||
{/* fillRule="evenodd"*/ }
|
||||
{/* clipRule="evenodd"*/ }
|
||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/ }
|
||||
{/* fill=""*/ }
|
||||
{/* />*/ }
|
||||
{/* </svg>*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu Start --> */ }
|
||||
{/* <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="/auth/signin"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Sign In*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* <li>*/ }
|
||||
{/* <NavLink*/ }
|
||||
{/* to="/auth/signup"*/ }
|
||||
{/* 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')*/ }
|
||||
{/* }*/ }
|
||||
{/* >*/ }
|
||||
{/* Sign Up*/ }
|
||||
{/* </NavLink>*/ }
|
||||
{/* </li>*/ }
|
||||
{/* </ul>*/ }
|
||||
{/* </div>*/ }
|
||||
{/* */ }{/*{/* <!-- Dropdown Menu End --> */ }
|
||||
{/* </React.Fragment>*/ }
|
||||
{/* );*/ }
|
||||
{/* }}*/ }
|
||||
{/* </SidebarLinkGroup>*/ }
|
||||
{/* */ }{/*{/* <!-- Menu Item Auth Pages --> */ }
|
||||
{/* </ul>*/ }
|
||||
{/*</div>}*/ }
|
||||
</nav>
|
||||
{/* <!-- Sidebar Menu --> */ }
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -20,11 +20,12 @@ interface SidebarLinkGroupProps
|
||||
{
|
||||
children: (handleClick: () => void, open: boolean) => ReactNode;
|
||||
activeCondition: boolean;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) =>
|
||||
const SidebarLinkGroup = ({ children, activeCondition, isOpen }: SidebarLinkGroupProps) =>
|
||||
{
|
||||
const [ open, setOpen ] = useState<boolean>(activeCondition);
|
||||
const [ open, setOpen ] = useState<boolean>(isOpen ?? activeCondition);
|
||||
|
||||
const handleClick = () =>
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
78
packages/frontend/src/components/mini/util/Paginator.tsx
Normal file
78
packages/frontend/src/components/mini/util/Paginator.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export const Paginator = ({ page, setPage, pages }: {
|
||||
page: number,
|
||||
pages: number,
|
||||
setPage: Dispatch<SetStateAction<number>>
|
||||
}) =>
|
||||
{
|
||||
let numbers = [ 1, page - 2, page - 1, page, page + 1, page + 2 ];
|
||||
|
||||
page === 1 && (numbers = [ page, page + 1, page + 2, page + 3, page + 4 ]);
|
||||
|
||||
page === 2 && (numbers = [ page - 1, page, page + 1, page + 2, page + 3 ]);
|
||||
|
||||
page === 3 && (numbers = [ page - 2, page - 1, page, page + 1, page + 2 ]);
|
||||
|
||||
(page === pages) && (numbers = [ 1, page - 4, page - 3, page - 2, page - 1 ]);
|
||||
|
||||
(page === (pages - 1)) && (numbers = [ 1, page - 3, page - 2, page - 1, page ]);
|
||||
|
||||
(page === (pages - 2)) && (numbers = [ 1, page - 2, page - 1, page, page + 1 ]);
|
||||
|
||||
numbers = numbers.filter(x => (pages + 1) >= x && x > 0);
|
||||
|
||||
return <div
|
||||
className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark flex flex-row align-center justify-center p-2">
|
||||
<ul className="flex flex-wrap items-center">
|
||||
<li>
|
||||
<a className="cursor-pointer flex h-9 w-9 items-center justify-center rounded-l-md border border-stroke hover:border-primary hover:bg-gray hover:text-primary dark:border-strokedark dark:hover:border-primary dark:hover:bg-graydark"
|
||||
onClick={ () => setPage(page => (page - 1) < 1 ? 1 : page - 1) }>
|
||||
<svg className="fill-current" width="8" height="16" viewBox="0 0 8 16" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.17578 15.1156C7.00703 15.1156 6.83828 15.0593 6.72578 14.9187L0.369531 8.44995C0.116406 8.19683 0.116406 7.80308 0.369531 7.54995L6.72578 1.0812C6.97891 0.828076 7.37266 0.828076 7.62578 1.0812C7.87891 1.33433 7.87891 1.72808 7.62578 1.9812L1.71953 7.99995L7.65391 14.0187C7.90703 14.2718 7.90703 14.6656 7.65391 14.9187C7.48516 15.0312 7.34453 15.1156 7.17578 15.1156Z"
|
||||
fill=""></path>
|
||||
</svg>
|
||||
</a></li>
|
||||
|
||||
{ numbers.map(num =>
|
||||
{
|
||||
return <li>
|
||||
<a onClick={ () => setPage(num) }
|
||||
className={ `cursor-pointer flex items-center justify-center border border-stroke border-l-transparent py-[5px] px-4 font-medium hover:border-primary hover:bg-gray hover:text-primary dark:border-strokedark dark:hover:border-primary dark:hover:bg-graydark select-none ${ page === num && "text-primary border-primary dark:border-primary" }` }>{ num }</a>
|
||||
</li>;
|
||||
}) }
|
||||
|
||||
{ !!pages && <li>
|
||||
<a className={ `cursor-pointer flex items-center justify-center border border-stroke border-l-transparent py-[5px] px-4 font-medium hover:border-primary hover:bg-gray hover:text-primary dark:border-strokedark dark:hover:border-primary dark:hover:bg-graydark select-none ${ page === pages && "text-primary border-primary dark:border-primary" }` }
|
||||
onClick={ () => setPage(pages) }>{ pages }</a></li> }
|
||||
<li>
|
||||
<a className="cursor-pointer flex h-9 w-9 items-center justify-center rounded-r-md border border-stroke border-l-transparent hover:border-primary hover:bg-gray hover:text-primary dark:border-strokedark dark:hover:border-primary dark:hover:bg-graydark select-none"
|
||||
onClick={ () => setPage(page => (page + 1) > pages ? pages : page + 1) }>
|
||||
<svg className="fill-current" width="8" height="16" viewBox="0 0 8 16" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.819531 15.1156C0.650781 15.1156 0.510156 15.0593 0.369531 14.9468C0.116406 14.6937 0.116406 14.3 0.369531 14.0468L6.27578 7.99995L0.369531 1.9812C0.116406 1.72808 0.116406 1.33433 0.369531 1.0812C0.622656 0.828076 1.01641 0.828076 1.26953 1.0812L7.62578 7.54995C7.87891 7.80308 7.87891 8.19683 7.62578 8.44995L1.26953 14.9187C1.15703 15.0312 0.988281 15.1156 0.819531 15.1156Z"
|
||||
fill="">
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>;
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -14,25 +14,109 @@
|
||||
* See LICENSE for more.
|
||||
*/
|
||||
|
||||
import { ChangeEventHandler } from "react";
|
||||
import { ChangeEventHandler, Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const SearchWithServerSelector = ({ searchItem, handleInputChange, children, servers, server, setServer }: {
|
||||
searchItem: string;
|
||||
handleInputChange: ChangeEventHandler,
|
||||
children?: ReactNode
|
||||
servers: string[];
|
||||
server: string;
|
||||
setServer: Dispatch<SetStateAction<string>>
|
||||
}) =>
|
||||
{
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
return <>
|
||||
<div
|
||||
className="col-span-12 rounded-sm border border-stroke bg-white px-5 p-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
||||
<div className="flex flex-col gap-6 xl:flex-row items-center justify-center">
|
||||
<div className="w-full xl:w-1/2 grow">
|
||||
<input onChange={ handleInputChange }
|
||||
value={ searchItem }
|
||||
type="text" placeholder={ t("search.placeholder_with_station") }
|
||||
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary"/>
|
||||
</div>
|
||||
{ servers?.length &&
|
||||
<div className="items-center justify-center flex gap-2 flex-wrap">
|
||||
{
|
||||
servers.map(x =>
|
||||
{
|
||||
return <a
|
||||
onClick={ () =>
|
||||
{
|
||||
setServer(x);
|
||||
} }
|
||||
className={ `cursor-pointer 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 ${ server === x ? "bg-opacity-50" : "" }` }>{ x.toUpperCase() }</a>;
|
||||
})
|
||||
}
|
||||
|
||||
<a
|
||||
onClick={ () =>
|
||||
{
|
||||
setServer(undefined!);
|
||||
} }
|
||||
className={ `cursor-pointer 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 ${ !server ? "bg-opacity-50" : "" }` }>{ t("search.none") }</a>
|
||||
</div> }
|
||||
</div>
|
||||
|
||||
|
||||
{ children }
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
export const Search = ({ searchItem, handleInputChange }: {
|
||||
searchItem: string;
|
||||
handleInputChange: ChangeEventHandler
|
||||
handleInputChange: ChangeEventHandler,
|
||||
}) =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
return <div
|
||||
className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
||||
<div className="flex justify-center items-center">
|
||||
<input
|
||||
className="w-full rounded border border-stroke bg-gray py-3 pl-5 pr-5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary"
|
||||
type="text"
|
||||
onChange={ handleInputChange }
|
||||
value={ searchItem }
|
||||
placeholder={ t("logs.search") }
|
||||
/>
|
||||
|
||||
return <>
|
||||
<div
|
||||
className="col-span-12 rounded-sm border border-stroke bg-white px-5 p-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
||||
<div className="flex flex-col gap-6 xl:flex-row items-center justify-center">
|
||||
<div className="w-full xl:w-1/2 grow">
|
||||
<input onChange={ handleInputChange }
|
||||
value={ searchItem }
|
||||
type="text" placeholder={ t("search.placeholder") }
|
||||
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
</>;
|
||||
};
|
||||
|
||||
export const SearchWithChild = ({ searchItem, handleInputChange, children }: {
|
||||
searchItem: string;
|
||||
handleInputChange: ChangeEventHandler,
|
||||
children: ReactNode
|
||||
}) =>
|
||||
{
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
return <>
|
||||
<div
|
||||
className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
||||
{ children }
|
||||
|
||||
<div className="mt-5 flex flex-col gap-6 xl:flex-row items-center justify-center">
|
||||
<div className="w-full xl:w-1/2 grow">
|
||||
<input onChange={ handleInputChange }
|
||||
value={ searchItem }
|
||||
type="text" placeholder={ t("search.placeholder") }
|
||||
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</>;
|
||||
};
|
@ -1,24 +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 { FaUserShield, FaUserSlash, FaUserLock } from "react-icons/fa6";
|
||||
|
||||
export const UserIcons = ({ flags }: { flags: string[] }) =>
|
||||
{
|
||||
return <> { flags.includes("administrator") &&
|
||||
<FaUserShield className={ "inline text-meta-1 ml-1" }/> } { flags.includes("leaderboard_hidden") &&
|
||||
<FaUserLock className={ "inline text-meta-6 ml-1" }/> } { flags.includes("hidden") &&
|
||||
<FaUserSlash className={ "inline text-meta-1 ml-1" }/> }</>;
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,77 +17,52 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TActiveStationPlayersData } from "../../../types/active.ts";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const ActiveStationTable = ({ stations }: { stations: TActiveStationPlayersData[] }) =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
const report = (data: TActiveStationPlayersData) =>
|
||||
{
|
||||
toast.info(t("log.toasts.report"), {
|
||||
autoClose: 5000,
|
||||
});
|
||||
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`https://steamcommunity.com/profiles/${ data.player.id }\`\n;server: \`${ data.server.toUpperCase() }\`\n;station: \`${ data.stationName }\`\n;link: https://${ location.hostname }/profile/${data.player.id}\n\n`);
|
||||
};
|
||||
|
||||
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("active.server") }
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||
{ stations.map((station) =>
|
||||
{
|
||||
return <div
|
||||
key={ station.server + station.stationShort }
|
||||
className="flex flex-col align-center items-center rounded-sm border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="p-4">
|
||||
<img className="rounded-full" src={ station.player.avatar } alt="Player"/>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("active.user") }
|
||||
</h5>
|
||||
<div className="flex flex-col p-2 align-center items-center">
|
||||
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||
{ station.player.username }
|
||||
<UserIcons flags={ station.player.flags }/>
|
||||
</h4>
|
||||
<p>{ t("log.station.station", { name: station.stationName, short: station.stationShort }) }</p>
|
||||
<p>{ t("log.station.server", { server: station.server.toUpperCase() }) }</p>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("active.station") }
|
||||
</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("active.actions") }
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ stations.map((station, key) => (
|
||||
<div
|
||||
className={ `grid grid-cols-3 sm:grid-cols-4 ${ stations.length === (key + 1) // todo: ...
|
||||
? ""
|
||||
: "border-b border-stroke dark:border-strokedark"
|
||||
}` }
|
||||
key={ station.player.id }
|
||||
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap mt-auto mb-2">
|
||||
<Link 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 ${ station.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ station.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||
<a
|
||||
onClick={ () => report(station) }
|
||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-danger py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||
>
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6">{ station.server.toUpperCase() }</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
||||
<p className="text-black dark:text-white sm:block break-all">
|
||||
<Link to={ "/profile/" + station.steam }
|
||||
className="color-orchid">{ station.username }</Link> <UserIcons
|
||||
flags={ station.player.flags }/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-5">{ station.stationName }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<Link
|
||||
to={ "/profile/" + station.steam }
|
||||
className={ `inline-flex items-center justify-center rounded-md bg-primary py-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("active.profile") }
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
{ t("log.buttons.report") }
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,7 +17,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TActiveTrainPlayersData } from "../../../types/active.ts";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const ActiveTrainTable = ({ trains }: {
|
||||
trains: TActiveTrainPlayersData[],
|
||||
@ -25,74 +26,48 @@ export const ActiveTrainTable = ({ trains }: {
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
const report = (data: TActiveTrainPlayersData) =>
|
||||
{
|
||||
toast.info(t("log.toasts.report"), {
|
||||
autoClose: 5000,
|
||||
});
|
||||
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`https://steamcommunity.com/profiles/${ data.player.id }\`\n;server: \`${ data.server.toUpperCase() }\`\n;train: \`${ data.trainNumber }\`\n;link: https://${ location.hostname }/profile/${ data.player.id }\n\n`);
|
||||
};
|
||||
|
||||
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("active.server") }
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||
{ trains.map((train) =>
|
||||
{
|
||||
return <div
|
||||
key={ train.server + train.trainNumber }
|
||||
className="flex flex-col align-center items-center rounded-sm border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="p-4">
|
||||
<img className="rounded-full"
|
||||
src={ train.player.avatar }
|
||||
alt="Player"/>
|
||||
</div>
|
||||
<div className="flex flex-col p-2 align-center items-center">
|
||||
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||
{ train.player.username }
|
||||
<UserIcons flags={ train.player.flags }/>
|
||||
</h4>
|
||||
<p>{ t("log.train.train", { name: train.trainName, number: train.trainNumber }) }</p>
|
||||
<p>{ t("log.train.server", { server: train.server.toUpperCase() }) }</p>
|
||||
</div>
|
||||
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("active.user") }
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("active.train") }
|
||||
</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("active.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.steam }
|
||||
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap mt-auto mb-2">
|
||||
<Link 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 ${ train.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ train.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||
<a
|
||||
onClick={ () => report(train) }
|
||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-danger py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||
>
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6">{ train.server.toUpperCase() }</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||
<p className="text-black dark:text-white sm:block break-all">
|
||||
<Link to={ "/profile/" + train.steam }
|
||||
className="color-orchid">{ train.username }</Link> <UserIcons
|
||||
flags={ train.player.flags }/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-5">{ train.trainName } { train.trainNumber }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<Link
|
||||
to={ "/profile/" + train.steam }
|
||||
className={ `inline-flex items-center justify-center rounded-md bg-primary py-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("active.profile") }
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
{ t("log.buttons.report") }
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
)
|
||||
;
|
||||
);
|
||||
};
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
import { FaCrown } from "react-icons/fa6";
|
||||
|
||||
export const LeaderboardTable = ({ list, queryType, isUnmodified }: {
|
||||
list: TLeaderboardRecord[];
|
||||
queryType: string;
|
||||
isUnmodified?: boolean
|
||||
}) =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||
{ list.map((player, index) =>
|
||||
{
|
||||
return <div
|
||||
key={ player.id }
|
||||
className="flex flex-col align-center items-center rounded-sm border border-stroke shadow-default dark:border-strokedark dark:bg-boxdark bg-stroke">
|
||||
<div className="p-4">
|
||||
<img className="rounded-full"
|
||||
src={ player.avatar }
|
||||
alt="Player"/>
|
||||
</div>
|
||||
<div className="flex flex-col p-2 align-center items-center">
|
||||
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||
{ player.username }
|
||||
<UserIcons flags={ player.flags }/>
|
||||
{ isUnmodified && index === 0 && <FaCrown className={ `inline text-gold ml-1` }/> }
|
||||
{ isUnmodified && index === 1 && <FaCrown className={ `inline text-silver ml-1` }/> }
|
||||
{ isUnmodified && index === 2 && <FaCrown className={ `inline text-bronze ml-1` }/> }
|
||||
</h4>
|
||||
{
|
||||
queryType === "train" ?
|
||||
<>
|
||||
<p className="break-words">{ t("leaderboard.train.distance", { distance: (player.trainDistance / 1000).toFixed(2) }) }</p>
|
||||
<p className="break-words">{ t("leaderboard.train.points", { points: player.trainPoints }) }</p>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<p>{ t("leaderboard.station.time", { time: formatTime(player.dispatcherTime) }) }</p>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap mt-auto mb-2">
|
||||
<Link to={ "/profile/" + 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 ${ player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,86 +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 { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||
import { formatTime } from "../../../util/time.ts";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
|
||||
export const StationTable = ({ stations }: { stations: TLeaderboardRecord[] }) =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||
|
||||
<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> <UserIcons flags={station.flags} />
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-3">{ formatTime(station.dispatcherTime) }</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 ${ station.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ station.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||
>
|
||||
{ t("leaderboard.profile") }
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
};
|
@ -1,114 +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 { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||
import { formatTime } from "../../../util/time.ts";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
|
||||
export const TrainTable = ({ 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-5">
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("leaderboard.user") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="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 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"
|
||||
onClick={ () => setSortBy("time") }>
|
||||
{ t("leaderboard.time") }
|
||||
</h5>
|
||||
<FlexArrowIcon rotated={ sortBy === "time" }/>
|
||||
</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-5 ${ trains.length === (key + 1)
|
||||
? ""
|
||||
: "border-b border-stroke dark:border-strokedark"
|
||||
}` }
|
||||
key={ train.id }
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||
<p className="text-black dark:text-white sm:block break-all">
|
||||
<Link to={ "/profile/" + train.id }
|
||||
className="color-orchid">{ train.username }</Link> <UserIcons flags={train.flags} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6">{ (train.trainDistance / 1000).toFixed(2) }km</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-5">{ train.trainPoints }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-3">{ formatTime(train.trainTime) }</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 ${ train.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ train.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||
>
|
||||
{ t("leaderboard.profile") }
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
;
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -19,7 +19,7 @@ import { TLogStationData } from "../../../types/log.ts";
|
||||
import dayjs from "dayjs";
|
||||
import { toast } from "react-toastify";
|
||||
import { Link } from "react-router-dom";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
|
||||
|
||||
export const StationLog = ({ data }: { data: TLogStationData }) =>
|
||||
@ -37,21 +37,23 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
||||
toast.info(t("log.toasts.report"), {
|
||||
autoClose: 5000,
|
||||
});
|
||||
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`);
|
||||
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`https://steamcommunity.com/profiles/${ data.player.id }\`\n;server: \`${ data.server.toUpperCase() }\`\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
|
||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||
<div
|
||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
||||
<div className="relative drop-shadow-2">
|
||||
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||
className="mx-auto max-w-44 rounded-full">
|
||||
<div className="relative">
|
||||
<img className="rounded-full"
|
||||
src={ data.player.avatar }
|
||||
alt="Player"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||
{ data.player.username } <UserIcons flags={data.player.flags} />
|
||||
{ data.player.username } <UserIcons flags={ data.player.flags }/>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,7 +87,7 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
||||
{ t("log.buttons.copy") }
|
||||
</a>
|
||||
<Link
|
||||
to={"/profile/" + data.player.id}
|
||||
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 ${ data.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ data.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -19,7 +19,7 @@ import { TLogTrainData } from "../../../types/log.ts";
|
||||
import dayjs from "dayjs";
|
||||
import { toast } from "react-toastify";
|
||||
import { Link } from "react-router-dom";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
|
||||
|
||||
export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
||||
@ -37,21 +37,23 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
||||
toast.info(t("log.toasts.report"), {
|
||||
autoClose: 5000,
|
||||
});
|
||||
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`);
|
||||
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`https://steamcommunity.com/profiles/${ data.player.id }\`\n;server: \`${ data.server.toUpperCase() }\`\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
|
||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||
<div
|
||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
||||
<div className="relative drop-shadow-2">
|
||||
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||
className="mx-auto max-w-44 rounded-full">
|
||||
<div className="relative">
|
||||
<img className="rounded-full"
|
||||
src={ data.player.avatar }
|
||||
alt="Player"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||
{ data.player.username } <UserIcons flags={data.player.flags} />
|
||||
{ data.player.username } <UserIcons flags={ data.player.flags }/>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -18,7 +18,9 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { TStationRecord } from "../../../types/station.ts";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
|
||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||
export const StationTable = ({ stations }: {
|
||||
@ -27,75 +29,59 @@ export const StationTable = ({ stations }: {
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
const report = (data: TStationRecord) =>
|
||||
{
|
||||
toast.info(t("log.toasts.report"), {
|
||||
autoClose: 5000,
|
||||
});
|
||||
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`https://steamcommunity.com/profiles/${ data.player.id }\`\n;server: \`${ data.server.toUpperCase() }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;station: \`${ data.stationName }\`\n;link: https://${ location.hostname }/log/${ data.id }\n\n`);
|
||||
};
|
||||
|
||||
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("logs.user") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.station") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.time") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.actions") }
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ stations.map((station, key) => (
|
||||
<div
|
||||
className={ `grid grid-cols-3 sm:grid-cols-4 ${ stations.length === (key + 1)
|
||||
? ""
|
||||
: "border-b border-stroke dark:border-strokedark"
|
||||
}` }
|
||||
key={ station.id }
|
||||
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||
{ stations.map((station) =>
|
||||
{
|
||||
return <div
|
||||
key={ station.id }
|
||||
className="flex flex-col align-center items-center rounded-sm border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="p-4">
|
||||
<img className="rounded-full"
|
||||
src={ station.player.avatar }
|
||||
alt="Player"/>
|
||||
</div>
|
||||
<div className="flex flex-col p-2 align-center items-center">
|
||||
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||
{ station.player.username }
|
||||
<UserIcons flags={ station.player.flags }/>
|
||||
</h4>
|
||||
<p className={'break-words'}>{ t("log.station.station", { name: station.stationName, short: station.stationShort }) }</p>
|
||||
<p>{ t("log.station.server", { server: station.server.toUpperCase() }) }</p>
|
||||
{ station.joinedDate &&
|
||||
<p>{ t("log.station.joined", { date: dayjs(station.joinedDate).format("HH:mm DD/MM/YYYY") }) }</p> }
|
||||
<p>{ t("log.station.left", { date: dayjs(station.leftDate).format("HH:mm DD/MM/YYYY") }) }</p>
|
||||
{ station.joinedDate &&
|
||||
<p>{ t("log.station.spent", { date: dayjs.duration(station.leftDate - station.joinedDate).format("H[h] m[m]") }) }</p> }
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap mt-auto mb-2">
|
||||
<Link 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 ${ station.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ station.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||
<Link to={ "/log/" + station.id }
|
||||
className={ `inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5` }>{ t("log.buttons.record") }</Link>
|
||||
|
||||
<a
|
||||
onClick={ () => report(station) }
|
||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-danger py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-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">
|
||||
<Link to={ "/profile/" + (station.steam ?? station.player.id) }
|
||||
className="color-orchid">{ station.username ?? station.player.username }</Link> <UserIcons flags={station.player.flags} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6 sm:block break-all">{ station.server.toUpperCase() } - { station.stationName ?? "--" }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-3">{ dayjs(station.leftDate).format("HH:mm DD/MM/YYYY") }</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
||||
<Link
|
||||
to={ "/profile/" + (station.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 ${ station.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ station.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||
>
|
||||
{ t("logs.profile") }
|
||||
</Link>
|
||||
<Link
|
||||
to={ "/log/" + station.id }
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||
>
|
||||
{ t("logs.record") }
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
{ t("log.buttons.report") }
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -18,7 +18,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TTrainRecord } from "../../../types/train.ts";
|
||||
import dayjs from "dayjs";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||
export const TrainTable = ({ trains }: {
|
||||
@ -27,93 +28,61 @@ export const TrainTable = ({ trains }: {
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const report = (data: TTrainRecord) =>
|
||||
{
|
||||
toast.info(t("log.toasts.report"), {
|
||||
autoClose: 5000,
|
||||
});
|
||||
void navigator.clipboard.writeText(`;user: \`${ data.player.username }\`\n;steam: \`https://steamcommunity.com/profiles/${ data.player.id }\`\n;server: \`${ data.server.toUpperCase() }\`\n;left: <t:${ Math.floor(data.leftDate / 1000) }>${ data.joinedDate ? `\n;joined: <t:${ Math.floor(data.joinedDate / 1000) }>` : "" }\n;train: \`${ data.trainNumber }\`\n;link: https://${ location.hostname }/log/${ data.id }\n\n`);
|
||||
};
|
||||
|
||||
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-6">
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.user") }
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||
{ trains.map((train) =>
|
||||
{
|
||||
return <div
|
||||
key={ train.id }
|
||||
className="flex flex-col align-center items-center rounded-sm border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="p-4">
|
||||
<img className="rounded-full"
|
||||
src={ train.player.avatar }
|
||||
alt="Player"/>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.train") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.points") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.distance") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.time") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("logs.actions") }
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col p-2 align-center items-center">
|
||||
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||
{ train.player.username }
|
||||
<UserIcons flags={ train.player.flags }/>
|
||||
</h4>
|
||||
<p>{ t("log.train.train", { name: train.trainName, number: train.trainNumber }) }</p>
|
||||
<p>{ t("log.train.server", { server: train.server.toUpperCase() }) }</p>
|
||||
{ train.joinedDate &&
|
||||
<p>{ t("log.train.joined", { date: dayjs(train.joinedDate).format("HH:mm DD/MM/YYYY") }) }</p> }
|
||||
<p>{ t("log.train.left", { date: dayjs(train.leftDate).format("HH:mm DD/MM/YYYY") }) }</p>
|
||||
{ train.joinedDate &&
|
||||
<p>{ t("log.train.spent", { date: dayjs.duration(train.leftDate - train.joinedDate).format("H[h] m[m]") }) }</p> }
|
||||
|
||||
{ trains.map((train, key) => (
|
||||
<div
|
||||
className={ `grid grid-cols-3 sm:grid-cols-6 ${ trains.length === (key + 1)
|
||||
? ""
|
||||
: "border-b border-stroke dark:border-strokedark"
|
||||
}` }
|
||||
key={ train.id }
|
||||
<p>{ t("log.train.distance", { distance: train.distance ? (train.distance / 1000).toFixed(2) : "--" }) }</p>
|
||||
|
||||
<p>{ t("log.train.points", { points: train.points || "--" }) }</p>
|
||||
</div>
|
||||
|
||||
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap mt-auto mb-2">
|
||||
<Link 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 ${ train.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ train.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||
<Link to={ "/log/" + train.id }
|
||||
className={ `inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5` }>{ t("log.buttons.record") }</Link>
|
||||
|
||||
<a
|
||||
onClick={ () => report(train) }
|
||||
className="cursor-pointer inline-flex items-center justify-center rounded-md bg-danger py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-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">
|
||||
<Link to={ "/profile/" + (train.steam ?? train.player.id) }
|
||||
className="color-orchid">{ train.username ?? train.player.username }</Link> <UserIcons flags={train.player.flags} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6 sm:block break-all">{ train.server.toUpperCase() } - { train.trainNumber ?? "--" }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6">{ train.distance ? train.points : "--" }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-5">{ train.distance ? `${ (train.distance / 1000).toFixed(2) }km` : "--" }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-3">{ dayjs(train.leftDate).format("HH:mm DD/MM/YYYY") }</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap ">
|
||||
<Link
|
||||
to={ "/profile/" + (train.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 ${ train.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ train.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||
>
|
||||
{ t("logs.profile") }
|
||||
</Link>
|
||||
<Link
|
||||
to={ "/log/" + train.id }
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||
>
|
||||
{ t("logs.record") }
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
{ t("log.buttons.report") }
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -24,7 +24,7 @@ import { ConfirmModal } from "../../mini/modal/ConfirmModal.tsx";
|
||||
import { post } from "../../../util/fetcher.ts";
|
||||
import { toast } from "react-toastify";
|
||||
import dayjs from "dayjs";
|
||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
|
||||
export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
{
|
||||
@ -61,11 +61,24 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
});
|
||||
};
|
||||
|
||||
const adminForceUpdate = () =>
|
||||
{
|
||||
post(`/admin/profile/${ data.player.id }/forceUpdate`, {}, { "X-Auth-Token": token })
|
||||
.then((response) =>
|
||||
{
|
||||
if (response.code === 200)
|
||||
{
|
||||
toast.success(t("admin.update.alert"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const { t } = useTranslation();
|
||||
return <>
|
||||
<ConfirmModal showModal={ hideLeaderboardStatsModal } setShowModal={ setHideLeaderboardStatsModal }
|
||||
onConfirm={ adminToggleHideLeaderboardPlayerProfile } title={ t("admin.hideLeaderboard.modal.title") }
|
||||
onConfirm={ adminToggleHideLeaderboardPlayerProfile }
|
||||
title={ t("admin.hideLeaderboard.modal.title") }
|
||||
description={ t("admin.hideLeaderboard.modal.description") }/>
|
||||
<ConfirmModal showModal={ hideProfileModal } setShowModal={ setHideProfileModal }
|
||||
onConfirm={ adminHidePlayerProfile } title={ t("admin.hide.modal.title") }
|
||||
@ -74,13 +87,15 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||
<div
|
||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
||||
<div className="relative drop-shadow-2">
|
||||
className="mx-auto max-w-44 rounded-full">
|
||||
<div className="relative rounded-full">
|
||||
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||
{ data.active &&
|
||||
<span className="absolute w-full rounded-full border-white bg-[#219653] dark:border-black max-w-5.5 right-0 top-0 h-5.5 border-[3px]"></span> }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||
<h3 className="text-2xl font-semibold text-black dark:text-white">
|
||||
{ data.player.username } <UserIcons flags={ data.player.flags }/>
|
||||
</h3>
|
||||
|
||||
@ -102,8 +117,19 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ data.active && data.active.type === "train" &&
|
||||
<div className="mx-auto text-center">
|
||||
<h4 className="font-semibold text-black dark:text-white">{ t("profile.active.train", { train: `${ data.active.trainName } - ${ data.active.trainNumber }`, server: data.active.server.toUpperCase() }) }</h4>
|
||||
</div> }
|
||||
|
||||
{ data.active && data.active.type === "station" &&
|
||||
<div className="mx-auto text-center">
|
||||
<h4 className="font-semibold text-black dark:text-white">{ t("profile.active.station", { station: `${ data.active.stationName } - ${ data.active.stationShort }`, server: data.active.server.toUpperCase() }) }</h4>
|
||||
</div> }
|
||||
</div>
|
||||
|
||||
|
||||
{ Object.keys(data.player.trainStats || {}).length > 0 &&
|
||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||
<div className="group relative cursor-pointer" onClick={ () => setShowTrains(val => !val) }>
|
||||
@ -113,7 +139,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
|
||||
{ showTrains &&
|
||||
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
||||
<div className="grid grid-cols-3 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("profile.trains.train") }
|
||||
@ -133,13 +159,13 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
</h5>
|
||||
<FlexArrowIcon rotated={ sortTrainsBy === "score" }/>
|
||||
</div>
|
||||
<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") }>
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("profile.trains.time") }
|
||||
</h5>
|
||||
<FlexArrowIcon rotated={ sortTrainsBy === "time" }/>
|
||||
</div>
|
||||
{/*<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") }>*/ }
|
||||
{/* <h5 className="text-sm font-medium uppercase xsm:text-base">*/ }
|
||||
{/* { t("profile.trains.time") }*/ }
|
||||
{/* </h5>*/ }
|
||||
{/* <FlexArrowIcon rotated={ sortTrainsBy === "time" }/>*/ }
|
||||
{/*</div>*/ }
|
||||
</div>
|
||||
|
||||
{ Object.keys(data.player.trainStats).sort((a, b) => data.player.trainStats[ b ][ sortTrainsBy ] - data.player.trainStats[ a ][ sortTrainsBy ]).map(trainName =>
|
||||
@ -147,7 +173,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
const train = data.player.trainStats[ trainName ];
|
||||
|
||||
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-3 border-t border-t-stroke dark:border-t-strokedark` }
|
||||
key={ trainName }
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
||||
@ -164,9 +190,9 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
<p className="text-meta-3">{ train.score }</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-3">{ formatTime(train.time) }</p>
|
||||
</div>
|
||||
{/*<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">*/ }
|
||||
{/* <p className="text-meta-3">{ formatTime(train.time) }</p>*/ }
|
||||
{/*</div>*/ }
|
||||
</div>;
|
||||
}) }
|
||||
|
||||
@ -239,6 +265,11 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
onClick={ () => setHideProfileModal(true) }>
|
||||
{ t("admin.hide.button") }
|
||||
</button>
|
||||
|
||||
<button 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"
|
||||
onClick={ () => adminForceUpdate() }>
|
||||
{ t("admin.update.button") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</> }
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||
import { TProfilePlayer } from "../../../types/profile.ts";
|
||||
// import { formatTime } from "../../../util/time.ts";
|
||||
|
||||
export const ProfilesTable = ({ profiles }: { profiles: TProfilePlayer[] }) =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||
{ profiles.map((player) =>
|
||||
{
|
||||
return <div
|
||||
className="flex flex-col align-center items-center rounded-sm border border-stroke shadow-default dark:border-strokedark dark:bg-boxdark bg-stroke ">
|
||||
<div className="p-4">
|
||||
<img className="rounded-full" src={ player.avatar } alt="Player"/>
|
||||
</div>
|
||||
<div className="flex flex-col p-2 align-center items-center">
|
||||
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||
{ player.username }
|
||||
<UserIcons flags={ player.flags }/>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap mt-auto mb-2">
|
||||
<Link to={ "/profile/" + 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 ${ player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||
style={ player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -255,20 +255,6 @@ span.flatpickr-weekday,
|
||||
@apply bg-primary border-primary dark:border-primary;
|
||||
}
|
||||
|
||||
.custom-input-date::-webkit-calendar-picker-indicator {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.custom-input-date-1::-webkit-calendar-picker-indicator {
|
||||
background-image: url(./images/icon/icon-calendar.svg);
|
||||
}
|
||||
|
||||
.custom-input-date-2::-webkit-calendar-picker-indicator {
|
||||
background-image: url(./images/icon/icon-arrow-down.svg);
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { useContext, createContext, ReactNode } from "react";
|
||||
import useSWR from "swr";
|
||||
import { get } from "../util/fetcher.ts";
|
||||
@ -5,27 +22,32 @@ import useLocalStorage from "./useLocalStorage.tsx";
|
||||
|
||||
export type AdminContext = { isAdmin: boolean; username: string; token: string; };
|
||||
|
||||
const defaultValue: AdminContext = { isAdmin: false, username: '', token: '' };
|
||||
const defaultValue: AdminContext = { isAdmin: false, username: "", token: "" };
|
||||
|
||||
const AuthContext = createContext<AdminContext>(defaultValue);
|
||||
|
||||
|
||||
// {"code":200,"status":true,"data":{"isAdmin":true,"username":"alekswilc","token":"test"}}
|
||||
const getUserAuthData = () => {
|
||||
const [value, _setValue] = useLocalStorage<string|undefined>('auth_token', undefined);
|
||||
const getUserAuthData = () =>
|
||||
{
|
||||
const [ value, _setValue ] = useLocalStorage<string | undefined>("auth_token", undefined);
|
||||
|
||||
if (!value || value === 'undefined')
|
||||
return { isAdmin: false, username: '', token: '' };
|
||||
if (!value || value === "undefined")
|
||||
{
|
||||
return { isAdmin: false, username: "", token: "" };
|
||||
}
|
||||
|
||||
const { data } = useSWR(`/admin/auth/?token=${value}`, get);
|
||||
const { data } = useSWR(`/admin/auth/?token=${ value }`, get);
|
||||
|
||||
return data ? { isAdmin: data.data.isAdmin, username: data.data.username, token: value } : { isAdmin: false, username: '', token: '' };
|
||||
}
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
return <AuthContext.Provider value={getUserAuthData()}>{children}</AuthContext.Provider>;
|
||||
return data ? { isAdmin: data.data.isAdmin, username: data.data.username, token: value } : { isAdmin: false, username: "", token: "" };
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) =>
|
||||
{
|
||||
return <AuthContext.Provider value={ getUserAuthData() }>{ children }</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAuth = () =>
|
||||
{
|
||||
return useContext(AuthContext);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -28,12 +28,14 @@ function useLocalStorage<T>(
|
||||
try
|
||||
{
|
||||
const item = window.localStorage.getItem(key);
|
||||
if (item) {
|
||||
try {
|
||||
if (item)
|
||||
{
|
||||
try
|
||||
{
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
}
|
||||
catch {
|
||||
return item ? item : initialValue;
|
||||
} catch
|
||||
{
|
||||
return (item && item !== "undefined") ? item : initialValue;
|
||||
}
|
||||
}
|
||||
} catch (error)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -19,6 +19,7 @@ import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import translationsInEng from "./languages/en.json";
|
||||
import translationsInPl from "./languages/pl.json";
|
||||
import translationsInCs from "./languages/cs.json";
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
@ -27,6 +28,9 @@ const resources = {
|
||||
pl: {
|
||||
translation: translationsInPl,
|
||||
},
|
||||
cs: {
|
||||
translation: translationsInCs,
|
||||
}
|
||||
};
|
||||
|
||||
void i18n
|
||||
@ -35,10 +39,11 @@ void i18n
|
||||
.init({
|
||||
resources,
|
||||
debug: false,
|
||||
fallbackLng: {
|
||||
"pl-PL": [ "pl" ],
|
||||
fallbackLng: (code: string) => {
|
||||
if (code.includes('pl')) return 'pl'; // Polish
|
||||
if (resources as any['cs'] && code.includes('cs')) return 'cs'; // Czech
|
||||
|
||||
default: [ "en" ],
|
||||
return 'en'; // English
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
173
packages/frontend/src/i18n/languages/cs.json
Normal file
173
packages/frontend/src/i18n/languages/cs.json
Normal file
@ -0,0 +1,173 @@
|
||||
{
|
||||
"search": {
|
||||
"placeholder": "například alekswilc",
|
||||
"placeholder_with_station": "například alekswilc,Katowice",
|
||||
"none": "Žádné"
|
||||
},
|
||||
"home": {
|
||||
"stats": {
|
||||
"trains": "vlaků",
|
||||
"dispatchers": "výpravčích",
|
||||
"profiles": "profilů"
|
||||
},
|
||||
"title": "Simrail Stats",
|
||||
"description": "Nejlepší stránka se záznamy a statistikami hráčů SimRail!",
|
||||
"buttons": {
|
||||
"project": "Stránka projektu",
|
||||
"forum": "Stránka na fóru SimRail",
|
||||
"discord": "Discord server"
|
||||
},
|
||||
"footer": {
|
||||
"license": "Licence:",
|
||||
"powered": "Založeno na:",
|
||||
"thanks": "Speciální poděkování: <bahu>BAHU.PRO hosting</bahu>, <simrailelite>Simrail ELITE discord</simrailelite>, komunita SimRail a moje přítelkyně",
|
||||
"author": "Pro komunitu SimRail vytvořeno s ❤️ uživatelem <anchor>{{author}}</anchor> "
|
||||
}
|
||||
},
|
||||
"notfound": {
|
||||
"title": "Stránka nenalezena",
|
||||
"description": "Vypadá to, že ses ztratil...",
|
||||
"button": "Vrátit se na hlavní stránku"
|
||||
},
|
||||
"leaderboard": {
|
||||
"train": {
|
||||
"distance": "Ujetá vzdálenost: {{distance}} km",
|
||||
"points": "Získané body: {{points}}"
|
||||
},
|
||||
"station": {
|
||||
"time": "Čas strávený v režimu výpravčího: {{time}}"
|
||||
},
|
||||
"buttons": {
|
||||
"trainDistance": "Největší ujetá vzdálenost",
|
||||
"trainPoints": "Nejvyšší počet body",
|
||||
"dispatcherTime": "Nejvíce hodin nahráno v režimu výpravčího"
|
||||
}
|
||||
},
|
||||
"content_loader": {
|
||||
"error": {
|
||||
"header": "Chyba načtení stránky",
|
||||
"description": "Zkontrolujte vaše internetové připojení a načtěte stránku znovu",
|
||||
"report": "Nahlásit chybu",
|
||||
"refresh": "Načíst znovu"
|
||||
},
|
||||
"notfound": {
|
||||
"header": "Nebyla nalezena žádná data",
|
||||
"description": "Vaše vyhledávání nevrátilo žádné výsledky."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"stats": {
|
||||
"distance": "Strojvedoucí",
|
||||
"time": "Výpravčí"
|
||||
},
|
||||
"trains": {
|
||||
"header": "Statistiky vlaků",
|
||||
"train": "Vlak",
|
||||
"distance": "Vzdálenost",
|
||||
"points": "Body",
|
||||
"time": "Čas"
|
||||
},
|
||||
"stations": {
|
||||
"header": "Statistiky stanic",
|
||||
"station": "Stanice",
|
||||
"time": "Čas"
|
||||
},
|
||||
"errors": {
|
||||
"notfound": {
|
||||
"title": "Profil nebyl nalezen",
|
||||
"description": "Profil tohoto hráče nemohl být nalezen, nebo si hráč nastavil soukromý Steam účet."
|
||||
},
|
||||
"blacklist": {
|
||||
"title": "Profil není možné zobrazit",
|
||||
"description": "Profil hráče nemohl být zobrazen kvůli aktivním činnostem moderátorů."
|
||||
}
|
||||
},
|
||||
"info": "Tyto statistiky jsou shromažďovány od {{date}}.",
|
||||
"active": {
|
||||
"train": "Řídí vlak {{train}} na serveru {{server}}",
|
||||
"station": "Výpravčí ve stanici {{station}} na serveru {{server}}"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"errors": {
|
||||
"notfound": {
|
||||
"title": "Podrobnosti nebyly nalezeny",
|
||||
"description": "Podrobnosti tohoto uživatele nebyly nalezeny."
|
||||
},
|
||||
"blacklist": {
|
||||
"title": "Tyto podrobnosti není možné zobrazit",
|
||||
"description": "Podrobnosti hráče nemohly být zobrazeny kvůli aktivním činnostem moderátorů."
|
||||
}
|
||||
},
|
||||
"station": {
|
||||
"header": "Opouští stanici",
|
||||
"server": "Server: {{server}}",
|
||||
"station": "Stanice: {{name}} - {{short}}",
|
||||
"joined": "Čas připojení do stanice: {{date}}",
|
||||
"left": "Čas odpojení ze stanice: {{date}}",
|
||||
"spent": "Čas strávený ve stanici: {{date}}"
|
||||
},
|
||||
"train": {
|
||||
"header": "Opouští vlak",
|
||||
"server": "Server: {{server}}",
|
||||
"train": "Vlak: {{name}} {{number}}",
|
||||
"joined": "Čas připojení do vlaku: {{date}}",
|
||||
"left": "Čas odpojení z vlaku: {{date}}",
|
||||
"spent": "Čas strávený ve vlaku: {{date}}",
|
||||
"distance": "Vzdálenost: {{distance}} km",
|
||||
"points": "Body: {{points}}"
|
||||
},
|
||||
"toasts": {
|
||||
"copied": "Odkaz byl zkopírován!",
|
||||
"report": "Údaje byly zkopírovány do schránky. Můžete je použít v kanálu #multiplayer-help-requests na oficiálním Discord serveru SimRailu. Nezapomeňte připsat důvod nahlášení!"
|
||||
},
|
||||
"buttons": {
|
||||
"report": "Nahlásit",
|
||||
"copy": "Zkopírovat odkaz",
|
||||
"profile": "Profil",
|
||||
"record": "Zobrazit"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"home": "Hlavní stránka",
|
||||
"logs": "Záznamy",
|
||||
"stations": "Stanice",
|
||||
"trains": "Vlaky",
|
||||
"leaderboard": "Žebříčky hráčů",
|
||||
"active_players": "Aktivní hráči",
|
||||
"info": "INFO",
|
||||
"admin": "ADMIN",
|
||||
"logged": "Přihlášen jako {{username}}",
|
||||
"logout": "Odhlásit se",
|
||||
"profiles": "Profily"
|
||||
},
|
||||
"icons": {
|
||||
"admin": "Administrátor",
|
||||
"leaderboard_hidden": "Žebříček je skrytý",
|
||||
"hidden": "Profil je skrytý"
|
||||
},
|
||||
"admin": {
|
||||
"header": "Akce moderátora",
|
||||
"hideLeaderboard": {
|
||||
"modal": {
|
||||
"title": "Jste si jisti?",
|
||||
"description": "Tato akce skryje profil uživatele z žebříčků."
|
||||
},
|
||||
"button": "Skrýt profil v žebříčku",
|
||||
"button2": "Zobrazit profil v žebříčku",
|
||||
"alert": "Profil hráče je skrytý."
|
||||
},
|
||||
"hide": {
|
||||
"modal": {
|
||||
"title": "Jste si jisti?",
|
||||
"description": "Tato akce skryje profil uživatele ze zobrazování v žebříčku."
|
||||
},
|
||||
"button": "Skrýt profil",
|
||||
"alert": "Profil hráče je skrytý."
|
||||
},
|
||||
"update": {
|
||||
"button": "Vynutit aktualizaci",
|
||||
"alert": "Profil je aktualizován!"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,8 @@
|
||||
{
|
||||
"_": {
|
||||
"title": "Simrail Stats",
|
||||
"popup": {
|
||||
"ranking": "Data in the rankings are collected from 19.08.2024"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"title": "Preview version!",
|
||||
"description": "The site is in version V3-PREVIEW, may contain errors. I will be grateful for reporting all errors on the project page (git.alekswilc.dev) or in a private message on discord - alekswilc. Stay tuned!"
|
||||
"search": {
|
||||
"placeholder": "ex. alekswilc",
|
||||
"placeholder_with_station": "ex. alekswilc,Katowice",
|
||||
"none": "None"
|
||||
},
|
||||
"home": {
|
||||
"stats": {
|
||||
@ -19,7 +14,8 @@
|
||||
"description": "Simrail Stats - The best SimRail logs and statistics site!",
|
||||
"buttons": {
|
||||
"project": "Project page",
|
||||
"forum": "Forum Page"
|
||||
"forum": "Forum page",
|
||||
"discord": "Discord server"
|
||||
},
|
||||
"footer": {
|
||||
"license": "License:",
|
||||
@ -34,32 +30,18 @@
|
||||
"button": "Return to homepage."
|
||||
},
|
||||
"leaderboard": {
|
||||
"user": "Player",
|
||||
"time": "Time",
|
||||
"distance": "Distance",
|
||||
"points": "Points",
|
||||
"profile": "Profile",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"active": {
|
||||
"server": "Server",
|
||||
"user": "Player",
|
||||
"train": "Train",
|
||||
"station": "Station",
|
||||
"profile": "Profile",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"logs": {
|
||||
"user": "Player",
|
||||
"time": "Time",
|
||||
"distance": "Distance",
|
||||
"points": "Points",
|
||||
"profile": "Profile",
|
||||
"record": "Record",
|
||||
"train": "Train",
|
||||
"actions": "Actions",
|
||||
"search": "Type to search",
|
||||
"station": "Station"
|
||||
"train": {
|
||||
"distance": "Driven distance: {{distance}}km",
|
||||
"points": "Earned points: {{points}}"
|
||||
},
|
||||
"station": {
|
||||
"time": "Time spent as a dispatcher: {{time}}"
|
||||
},
|
||||
"buttons": {
|
||||
"trainDistance": "Longest distance traveled",
|
||||
"trainPoints": "Most points",
|
||||
"dispatcherTime": "Most hours as dispatcher"
|
||||
}
|
||||
},
|
||||
"content_loader": {
|
||||
"error": {
|
||||
@ -100,7 +82,11 @@
|
||||
"description": "The player's profile could not be displayed due to active moderator actions."
|
||||
}
|
||||
},
|
||||
"info": "Note: This user's statistics are collected since {{date}}."
|
||||
"info": "Note: This user's statistics are collected since {{date}}.",
|
||||
"active": {
|
||||
"train": "Drives the train {{train}}, on the server {{server}}",
|
||||
"station": "Active on station {{station}}, on server {{server}}"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"errors": {
|
||||
@ -133,12 +119,13 @@
|
||||
},
|
||||
"toasts": {
|
||||
"copied": "Link copied to clipboard!",
|
||||
"report": "The outpost exit data has been copied to your clipboard, you can use it to submit to the #multiplayer-help-requests channel on the official simrail Discord server. Don't forget to add a reason for the report!"
|
||||
"report": "The data has been copied to your clipboard, you can use it to submit to the #multiplayer-help-requests channel on the official simrail Discord server. Don't forget to add a reason for the report!"
|
||||
},
|
||||
"buttons": {
|
||||
"report": "Report",
|
||||
"copy": "Copy link",
|
||||
"profile": "Profile"
|
||||
"profile": "Profile",
|
||||
"record": "Record"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
@ -151,10 +138,16 @@
|
||||
"info": "INFO",
|
||||
"admin": "ADMIN",
|
||||
"logged": "Logged as {{username}}",
|
||||
"logout": "Log out"
|
||||
"logout": "Log out",
|
||||
"profiles": "Players"
|
||||
},
|
||||
"icons": {
|
||||
"admin": "Administrator",
|
||||
"leaderboard_hidden": "Leaderboard hidden",
|
||||
"hidden": "Profile hidden"
|
||||
},
|
||||
"admin": {
|
||||
"header": "Akcje moderacyjne",
|
||||
"header": "Moderator actions",
|
||||
"hideLeaderboard": {
|
||||
"modal": {
|
||||
"title": "Are you sure?",
|
||||
@ -171,6 +164,10 @@
|
||||
},
|
||||
"button": "Hide profile",
|
||||
"alert": "Player profile hidden."
|
||||
},
|
||||
"update": {
|
||||
"button": "Force update",
|
||||
"alert": "Profile updated!"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,8 @@
|
||||
{
|
||||
"_": {
|
||||
"title": "Simrail Stats",
|
||||
"popup": {
|
||||
"ranking": "Dane w rankingach zbierane są od 19.08.2024"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"title": "Wersja preview!",
|
||||
"description": "Strona znajduje się w wersji V3-PREVIEW, może zawierać błędy. Będe wdzieczny za zgłaszanie wszystkich błędów na stronie projektu (git.alekswilc.dev) lub w wiadomości prywatnej na discordzie - alekswilc. Stay tuned!"
|
||||
"search": {
|
||||
"placeholder": "np. alekswilc",
|
||||
"placeholder_with_station": "np. alekswilc,Katowice",
|
||||
"none": "Brak"
|
||||
},
|
||||
"home": {
|
||||
"stats": {
|
||||
@ -19,7 +14,8 @@
|
||||
"description": "Najbardziej rozbudowana strona z logami i statystykami gry SimRail!",
|
||||
"buttons": {
|
||||
"project": "Strona projektu",
|
||||
"forum": "Strona na forum"
|
||||
"forum": "Strona na forum",
|
||||
"discord": "Serwer discord"
|
||||
},
|
||||
"footer": {
|
||||
"license": "Licencja:",
|
||||
@ -34,32 +30,18 @@
|
||||
"button": "Wróć do strony głównej"
|
||||
},
|
||||
"leaderboard": {
|
||||
"user": "Gracz",
|
||||
"time": "Czas",
|
||||
"distance": "Dystans",
|
||||
"points": "Punkty",
|
||||
"profile": "Profil",
|
||||
"actions": "Akcje"
|
||||
},
|
||||
"active": {
|
||||
"server": "Serwer",
|
||||
"user": "Gracz",
|
||||
"train": "Pociąg",
|
||||
"station": "Stacja",
|
||||
"profile": "Profil",
|
||||
"actions": "Akcje"
|
||||
},
|
||||
"logs": {
|
||||
"user": "Gracz",
|
||||
"time": "Czas",
|
||||
"distance": "Dystans",
|
||||
"points": "Punkty",
|
||||
"profile": "Profil",
|
||||
"record": "Więcej",
|
||||
"train": "Pociąg",
|
||||
"actions": "Akcje",
|
||||
"search": "Wpisz, aby wyszukać",
|
||||
"station": "Stacja"
|
||||
"train": {
|
||||
"distance": "Przejechany dystans: {{distance}}km",
|
||||
"points": "Zdobyte punkty: {{points}}"
|
||||
},
|
||||
"station": {
|
||||
"time": "Spędzony czas jako dyżurny ruchu: {{time}}"
|
||||
},
|
||||
"buttons": {
|
||||
"trainDistance": "Największy przejechany dystans",
|
||||
"trainPoints": "Najwięcej punktów",
|
||||
"dispatcherTime": "Najwięcej godzin na nastawni"
|
||||
}
|
||||
},
|
||||
"content_loader": {
|
||||
"error": {
|
||||
@ -100,7 +82,11 @@
|
||||
"description": "Profil gracza nie mógł zostać wyświetlony ze względu na aktywne działania moderatora."
|
||||
}
|
||||
},
|
||||
"info": "Uwaga: Statystyki tego użytkownika gromadzone są od {{date}} r."
|
||||
"info": "Uwaga: Statystyki tego użytkownika gromadzone są od {{date}} r.",
|
||||
"active": {
|
||||
"train": "Prowadzi pociąg {{train}}, na serwerze {{server}}",
|
||||
"station": "Aktywny na stacji {{station}}, na serwerze {{server}}"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"errors": {
|
||||
@ -133,12 +119,13 @@
|
||||
},
|
||||
"toasts": {
|
||||
"copied": "Skopiowano link do schowka!",
|
||||
"report": "Do schowka skopiowano dane wyjścia z posterunku, możesz ich użyć do wysłania na kanale #multiplayer-help-requests na oficjalnym serwerze Discord gry simrail. Nie zapomnij dodać powodu zgłoszenia!"
|
||||
"report": "Do schowka skopiowano dane, możesz ich użyć do wysłania na kanale #multiplayer-help-requests na oficjalnym serwerze Discord gry simrail. Nie zapomnij dodać powodu zgłoszenia!"
|
||||
},
|
||||
"buttons": {
|
||||
"report": "Zgłoś",
|
||||
"copy": "Kopiuj link",
|
||||
"profile": "Profil"
|
||||
"profile": "Profil",
|
||||
"record": "Rekord"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
@ -151,10 +138,16 @@
|
||||
"info": "INFO",
|
||||
"admin": "ADMIN",
|
||||
"logged": "Zalogowano jako {{username}}",
|
||||
"logout": "Wyloguj"
|
||||
"logout": "Wyloguj",
|
||||
"profiles": "Gracze"
|
||||
},
|
||||
"icons": {
|
||||
"admin": "Administrator",
|
||||
"leaderboard_hidden": "Tablica wyników ukryta",
|
||||
"hidden": "Profil ukryty"
|
||||
},
|
||||
"admin": {
|
||||
"header": "Moderator actions",
|
||||
"header": "Akcje moderacyjne",
|
||||
"hideLeaderboard": {
|
||||
"modal": {
|
||||
"title": "Czy jesteś pewien?",
|
||||
@ -171,6 +164,10 @@
|
||||
},
|
||||
"button": "Ukryj profil",
|
||||
"alert": "Ukryto profil gracza."
|
||||
},
|
||||
"update": {
|
||||
"button": "Wymuś aktualizacje",
|
||||
"alert": "Zaktualizowano profil!"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
2
packages/frontend/src/lib.d.ts
vendored
2
packages/frontend/src/lib.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -21,12 +21,10 @@ import App from "./App";
|
||||
import "./css/style.css";
|
||||
import "./css/satoshi.css";
|
||||
import "flatpickr/dist/flatpickr.min.css";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime.js";
|
||||
import duration from "dayjs/plugin/duration.js";
|
||||
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -19,7 +19,7 @@ import { Link, useSearchParams } from "react-router-dom";
|
||||
import { TStatsResponse } from "../types/stats.ts";
|
||||
import { CardDataStats } from "../components/mini/util/CardDataStats.tsx";
|
||||
import { get } from "../util/fetcher.ts";
|
||||
import useSWR from 'swr';
|
||||
import useSWR from "swr";
|
||||
import { LoadError } from "../components/mini/loaders/ContentLoader.tsx";
|
||||
|
||||
export const Home = () =>
|
||||
@ -28,10 +28,11 @@ export const Home = () =>
|
||||
|
||||
const { data, error } = useSWR<TStatsResponse>("/stats/", get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
|
||||
if (searchParams.get('admin_token')) {
|
||||
window.localStorage.setItem('auth_token', searchParams.get('admin_token')!);
|
||||
if (searchParams.get("admin_token"))
|
||||
{
|
||||
window.localStorage.setItem("auth_token", searchParams.get("admin_token")!);
|
||||
setSearchParams(new URLSearchParams());
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
}
|
||||
@ -39,13 +40,14 @@ export const Home = () =>
|
||||
return (
|
||||
<>
|
||||
<div className="flex pb-5">
|
||||
{ error && <LoadError /> }
|
||||
{ error && <LoadError/> }
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6 xl:grid-cols-3 2xl:gap-7.5">
|
||||
<CardDataStats title={ t("home.stats.trains") } total={ data?.data?.stats?.trains ?? "-" }/>
|
||||
<CardDataStats title={ t("home.stats.dispatchers") } total={ data?.data?.stats?.dispatchers ?? "-" }/>
|
||||
<CardDataStats title={ t("home.stats.dispatchers") }
|
||||
total={ data?.data?.stats?.dispatchers ?? "-" }/>
|
||||
<CardDataStats title={ t("home.stats.profiles") } total={ data?.data?.stats?.profiles ?? "-" }/>
|
||||
</div>
|
||||
|
||||
@ -105,7 +107,7 @@ export const Home = () =>
|
||||
</p>
|
||||
|
||||
<p>{ data?.data?.git?.version && <Link className="color-orchid"
|
||||
to={ `https://git.alekswilc.dev/simrail/simrail.pro/releases/tag/${ data?.data?.git?.version }` }>{ data?.data?.git?.version }</Link> }{ data?.data?.git?.version && data?.data?.git?.commit && " | " }{ data?.data?.git?.commit &&
|
||||
to={ `https://git.alekswilc.dev/simrail/simrail.pro/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"
|
||||
to={ `https://git.alekswilc.dev/simrail/simrail.pro/commit/${ data?.data?.git?.commit }` }>{ data?.data?.git?.commit }</Link> }</p>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Search } from "../../components/mini/util/Search.tsx";
|
||||
import { SearchWithServerSelector } from "../../components/mini/util/Search.tsx";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { get } from "../../util/fetcher.ts";
|
||||
import useSWR from "swr";
|
||||
@ -33,51 +33,55 @@ export const ActiveStationsPlayers = () =>
|
||||
const { data, error, isLoading } = useSWR(`/active/station/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||
const [ server, setServer ] = useState(searchParams.get("server") ?? "");
|
||||
|
||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
searchValue && params.set("q", searchValue);
|
||||
searchValue && params.set("query", searchValue);
|
||||
server && params.set("server", server);
|
||||
|
||||
setSearchParams(params.toString());
|
||||
setParams(params);
|
||||
}, [ searchValue ]);
|
||||
}, [ searchValue, server ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setSearchItem(searchParams.get("q") ?? "");
|
||||
}, [ searchParams ]);
|
||||
setSearchItem(searchParams.get("query") ?? "");
|
||||
setServer(searchParams.get("server") ?? "");
|
||||
}, []);
|
||||
|
||||
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/> }
|
||||
<SearchWithServerSelector handleInputChange={ handleInputChange } searchItem={ searchItem }
|
||||
servers={ data?.code === 200 ? data?.data?.servers : [] }
|
||||
server={ server } setServer={ setServer }/>
|
||||
{ error && <LoadError/> }
|
||||
|
||||
{ isLoading && <ContentLoader/> }
|
||||
{ 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 && 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 &&
|
||||
<ActiveStationTable stations={ data.data.records }/> }
|
||||
|
||||
{ data && data.code === 200 && data.data && !!data?.data?.records?.length &&
|
||||
<ActiveStationTable stations={ data.data.records }/> }
|
||||
</>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
;
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Search } from "../../components/mini/util/Search.tsx";
|
||||
import { SearchWithServerSelector } from "../../components/mini/util/Search.tsx";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { get } from "../../util/fetcher.ts";
|
||||
@ -32,27 +32,27 @@ export const ActiveTrainPlayers = () =>
|
||||
|
||||
const { data, error, isLoading } = useSWR(`/active/train/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||
const [ server, setServer ] = useState(searchParams.get("server") ?? "");
|
||||
|
||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
searchValue && params.set("q", searchValue);
|
||||
|
||||
searchValue && params.set("query", searchValue);
|
||||
server && params.set("server", server);
|
||||
|
||||
setSearchParams(params.toString());
|
||||
setParams(params);
|
||||
}, [ searchValue ]);
|
||||
}, [ searchValue, server ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setSearchItem(searchParams.get("q") ?? "");
|
||||
}, [ searchParams ]);
|
||||
setSearchItem(searchParams.get("query") ?? "");
|
||||
setServer(searchParams.get("server") ?? "");
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
@ -64,17 +64,21 @@ export const ActiveTrainPlayers = () =>
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||
<SearchWithServerSelector handleInputChange={ handleInputChange } searchItem={ searchItem }
|
||||
servers={ data?.code === 200 ? data?.data?.servers : [] }
|
||||
server={ server } setServer={ setServer }/>
|
||||
<>
|
||||
{ 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 === 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 && <ActiveTrainTable trains={ data?.data?.records } /> }
|
||||
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||
<ActiveTrainTable trains={ data?.data?.records }/> }
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -24,9 +24,7 @@ export const NotFoundError = () =>
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="flex flex-col gap-10">
|
||||
|
||||
<div
|
||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="px-4 pb-6 text-center">
|
||||
|
127
packages/frontend/src/pages/leaderboard/Leaderboard.tsx
Normal file
127
packages/frontend/src/pages/leaderboard/Leaderboard.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 { LeaderboardTable } from "../../components/pages/leaderboard/LeaderboardTable.tsx";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { SearchWithChild } from "../../components/mini/util/Search.tsx";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { get } 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 { Paginator } from "../../components/mini/util/Paginator.tsx";
|
||||
|
||||
export const Leaderboard = () =>
|
||||
{
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const [ params, setParams ] = useState(new URLSearchParams());
|
||||
const [ queryType, setQueryType ] = useState(searchParams.get("type") ?? "train");
|
||||
|
||||
const { data, error, isLoading } = useSWR(`/leaderboard/${ queryType }/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||
const [ sortBy, setSortBy ] = useState(searchParams.get("sort_by") ?? "distance");
|
||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||
|
||||
const [ page, setPage ] = useState(parseInt(searchParams.get("page") as string) || 1);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setSearchItem(searchParams.get("query") ?? "");
|
||||
setSortBy(searchParams.get("sort_by") ?? "distance");
|
||||
setPage(parseInt(searchParams.get("page") as string) || 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const params = new URLSearchParams();
|
||||
searchValue && params.set("query", searchValue);
|
||||
sortBy && params.set("sort_by", sortBy);
|
||||
queryType && params.set("type", queryType);
|
||||
page && params.set("page", page.toString());
|
||||
|
||||
setSearchParams(params.toString());
|
||||
setParams(params);
|
||||
}, [ searchValue, sortBy, page ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setPage(1);
|
||||
}, [ searchValue, sortBy ]);
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
setSearchItem(e.target.value);
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-10">
|
||||
<SearchWithChild handleInputChange={ handleInputChange } searchItem={ searchItem }>
|
||||
<div className="items-center justify-center flex gap-2 flex-wrap sm:flex-nowrap">
|
||||
<a
|
||||
onClick={ () =>
|
||||
{
|
||||
setSortBy("distance");
|
||||
setQueryType("train");
|
||||
} }
|
||||
className={ `cursor-pointer 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 grow ${ (queryType === "train" && sortBy === "distance") ? "bg-opacity-70" : "" }` }>{ t("leaderboard.buttons.trainDistance") }</a>
|
||||
|
||||
<a
|
||||
onClick={ () =>
|
||||
{
|
||||
setSortBy("points");
|
||||
setQueryType("train");
|
||||
} }
|
||||
className={ `cursor-pointer 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 grow ${ (queryType === "train" && sortBy === "points") ? "bg-opacity-70" : "" }` }>{ t("leaderboard.buttons.trainPoints") }</a>
|
||||
|
||||
<a
|
||||
onClick={ () =>
|
||||
{
|
||||
setSortBy(undefined!);
|
||||
setQueryType("station");
|
||||
} }
|
||||
className={ `cursor-pointer inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-70 lg:px-4 xl:px-5 grow ${ queryType === "station" ? "bg-opacity-50" : "" }` }>{ t("leaderboard.buttons.dispatcherTime") }</a>
|
||||
|
||||
</div>
|
||||
</SearchWithChild>
|
||||
<>
|
||||
{ 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 &&
|
||||
<>
|
||||
<LeaderboardTable list={ data.data.records } queryType={ queryType }
|
||||
isUnmodified={ page === 1 && !searchValue }/>
|
||||
<Paginator page={ page } pages={ data.data.pages } setPage={ setPage }/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,82 +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 { ChangeEvent, useEffect, useState } from "react";
|
||||
import { StationTable } from "../../components/pages/leaderboard/StationTable.tsx";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Search } from "../../components/mini/util/Search.tsx";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { get } 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 = () =>
|
||||
{
|
||||
const [ params, setParams ] = useState(new URLSearchParams());
|
||||
|
||||
const { data, error, isLoading } = useSWR(`/leaderboard/station/?${params.toString()}`, get, { 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 && <StationTable stations={ data.data.records } /> }
|
||||
</>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -33,8 +33,8 @@ export const Log = () =>
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ERROR */}
|
||||
{ error && <LoadError /> }
|
||||
{/* ERROR */ }
|
||||
{ error && <LoadError/> }
|
||||
{/* LOADING */ }
|
||||
{ isLoading && <ContentLoader/> }
|
||||
{/* NOT FOUND */ }
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,39 +17,43 @@
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { StationTable } from "../../components/pages/logs/StationTable.tsx";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Search } from "../../components/mini/util/Search.tsx";
|
||||
import { SearchWithServerSelector } from "../../components/mini/util/Search.tsx";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import useSWR from 'swr';
|
||||
import useSWR from "swr";
|
||||
import { get } 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';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Paginator } from "../../components/mini/util/Paginator.tsx";
|
||||
|
||||
export const StationLogs = () =>
|
||||
{
|
||||
const [params, setParams] = useState(new URLSearchParams());
|
||||
const { data, error, isLoading } = useSWR(`/stations/?${params.toString()}`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
const [ params, setParams ] = useState(new URLSearchParams());
|
||||
const { data, error, isLoading } = useSWR(`/stations/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||
const [ server, setServer ] = useState(searchParams.get("server") ?? "");
|
||||
const [ page, setPage ] = useState(parseInt(searchParams.get("page") as string) || 1);
|
||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
searchValue && params.set('q', searchValue);
|
||||
searchValue && params.set("query", searchValue);
|
||||
server && params.set("server", server);
|
||||
page && params.set("page", page.toString());
|
||||
|
||||
setSearchParams(params.toString());
|
||||
setParams(params);
|
||||
}, [ searchValue ]);
|
||||
}, [ searchValue, server, page ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setSearchItem(searchParams.get("q") ?? "");
|
||||
}, [ searchParams ]);
|
||||
setSearchItem(searchParams.get("query") ?? "");
|
||||
setServer(searchParams.get("server") ?? "");
|
||||
setPage(parseInt(searchParams.get("page") as string) || 1);
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
@ -61,16 +65,25 @@ export const StationLogs = () =>
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||
<SearchWithServerSelector handleInputChange={ handleInputChange } searchItem={ searchItem }
|
||||
servers={ data?.code === 200 ? data?.data?.servers : [] }
|
||||
server={ server } setServer={ setServer }
|
||||
/>
|
||||
<>
|
||||
{ error && <LoadError /> }
|
||||
{ error && <LoadError/> }
|
||||
|
||||
{isLoading && <ContentLoader/> }
|
||||
{ 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 === 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} /> }
|
||||
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||
<>
|
||||
<StationTable stations={ data.data.records }/>
|
||||
<Paginator page={ page } pages={ data.data.pages } setPage={ setPage }/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -17,13 +17,14 @@
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { TrainTable } from "../../components/pages/logs/TrainTable.tsx";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Search } from "../../components/mini/util/Search.tsx";
|
||||
import { SearchWithServerSelector } from "../../components/mini/util/Search.tsx";
|
||||
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 { get } from "../../util/fetcher.ts";
|
||||
import useSWR from "swr";
|
||||
import { Paginator } from "../../components/mini/util/Paginator.tsx";
|
||||
|
||||
export const TrainLogs = () =>
|
||||
{
|
||||
@ -31,25 +32,27 @@ export const TrainLogs = () =>
|
||||
const { data, error, isLoading } = useSWR(`/trains/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||
const [ server, setServer ] = useState(searchParams.get("server") ?? "");
|
||||
const [ page, setPage ] = useState(parseInt(searchParams.get("page") as string) || 1);
|
||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
searchValue && params.set("q", searchValue);
|
||||
searchValue && params.set("query", searchValue);
|
||||
server && params.set("server", server);
|
||||
page && params.set("page", page.toString());
|
||||
|
||||
setSearchParams(params.toString());
|
||||
setParams(params);
|
||||
}, [ searchValue ]);
|
||||
}, [ searchValue, server, page ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setSearchItem(searchParams.get("q") ?? "");
|
||||
}, [ searchParams ]);
|
||||
setSearchItem(searchParams.get("query") ?? "");
|
||||
setServer(searchParams.get("server") ?? "");
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
@ -61,16 +64,24 @@ export const TrainLogs = () =>
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||
<SearchWithServerSelector handleInputChange={ handleInputChange } searchItem={ searchItem }
|
||||
servers={ data?.code === 200 ? data?.data?.servers : [] }
|
||||
server={ server } setServer={ setServer }
|
||||
/>
|
||||
<>
|
||||
{ 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 === 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 }/> }
|
||||
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||
<>
|
||||
<TrainTable trains={ data.data.records }/>
|
||||
<Paginator page={ page } pages={ data.data.pages } setPage={ setPage }/>
|
||||
</> }
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -19,18 +19,18 @@ import { useParams } from "react-router-dom";
|
||||
|
||||
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||
import { ProfileCard } from "../../components/pages/profile/Profile.tsx";
|
||||
import { ProfileCard } from "../../components/pages/profiles/Profile.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
||||
import { formatTime } from "../../util/time.ts";
|
||||
import useSWR from 'swr';
|
||||
import useSWR from "swr";
|
||||
import { get } from "../../util/fetcher.ts";
|
||||
|
||||
|
||||
export const Profile = () =>
|
||||
{
|
||||
const { id } = useParams();
|
||||
const { data, error, isLoading } = useSWR(`/profiles/${ id }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
const { data, error, isLoading } = useSWR(`/profiles/${ id }`, get, { refreshInterval: 5_000, errorRetryCount: 5 });
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -38,8 +38,8 @@ export const Profile = () =>
|
||||
<>
|
||||
{/* LOADING */ }
|
||||
{ isLoading && <ContentLoader/> }
|
||||
{/* ERROR */}
|
||||
{ error && <LoadError /> }
|
||||
{/* ERROR */ }
|
||||
{ error && <LoadError/> }
|
||||
{/* BLACKLISTED */ }
|
||||
{ data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
|
||||
description="The player's profile could not be displayed due to active moderator actions."/> }
|
||||
@ -47,14 +47,14 @@ export const Profile = () =>
|
||||
description={ t("profile.errors.blacklist.description") }/> }
|
||||
{/* NOT FOUND */ }
|
||||
{ data && data.code === 404 && <PageMeta title="simrail.pro | 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."/> }
|
||||
{ data && data.code === 404 && <WarningAlert title={ t("profile.errors.notfound.title") }
|
||||
description={ t("profile.errors.notfound.description") }/> }
|
||||
description={ t("profile.errors.notfound.description") }/> }
|
||||
|
||||
{/* SUCCESS */ }
|
||||
{ data && data.code === 200 && <PageMeta image={ data.data.player.username }
|
||||
title={ `simrail.pro | ${ data.data.player.username }'s profile` }
|
||||
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
||||
title={ `simrail.pro | ${ data.data.player.username }'s profile` }
|
||||
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
||||
${ data.data.player.dispatcherTime ? 0 : formatTime(data.data.player.dispatcherTime) } dispatcher experience` }/> }
|
||||
{ data && data.code === 200 && <ProfileCard data={ data.data }/> }
|
||||
</>
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
@ -14,28 +14,26 @@
|
||||
* See LICENSE for more.
|
||||
*/
|
||||
|
||||
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { TrainTable } from "../../components/pages/leaderboard/TrainTable.tsx";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Search } from "../../components/mini/util/Search.tsx";
|
||||
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 { get } 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 { ProfilesTable } from "../../components/pages/profiles/ProfilesTable.tsx";
|
||||
import { Paginator } from "../../components/mini/util/Paginator.tsx";
|
||||
|
||||
export const TrainLeaderboard = () =>
|
||||
export const Profiles = () =>
|
||||
{
|
||||
const [ params, setParams ] = useState(new URLSearchParams());
|
||||
|
||||
const { data, error, isLoading } = useSWR(`/leaderboard/train/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
const { data, error, isLoading } = useSWR(`/profiles/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||
const [ sortBy, setSortBy ] = useState(searchParams.get("s") ?? "distance");
|
||||
const [ page, setPage ] = useState(parseInt(searchParams.get("page") as string) || 1);
|
||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||
|
||||
useEffect(() =>
|
||||
@ -44,17 +42,16 @@ export const TrainLeaderboard = () =>
|
||||
|
||||
const params = new URLSearchParams();
|
||||
searchValue && params.set("q", searchValue);
|
||||
sortBy && params.set("s", sortBy);
|
||||
|
||||
page && params.set("page", page.toString());
|
||||
|
||||
setSearchParams(params.toString());
|
||||
setParams(params);
|
||||
}, [ searchValue, sortBy ]);
|
||||
}, [ searchValue, page ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setSearchItem(searchParams.get("q") ?? "");
|
||||
setSortBy(searchParams.get("s") ?? "distance");
|
||||
setPage(parseInt(searchParams.get("page") as string) || 1);
|
||||
}, [ searchParams ]);
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
@ -73,11 +70,16 @@ export const TrainLeaderboard = () =>
|
||||
|
||||
{ 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 === 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 } setSortBy={ setSortBy } sortBy={ sortBy }/> }
|
||||
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||
<>
|
||||
<ProfilesTable profiles={ data.data.records }/>
|
||||
<Paginator page={ page } pages={ data.data.pages } setPage={ setPage }/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
</>
|
2
packages/frontend/src/react-app-env.d.ts
vendored
2
packages/frontend/src/react-app-env.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||
* Copyright (C) 2025 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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user