forked from simrail/simrail.pro
Compare commits
83 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 |
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",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yarn workspace backend build && yarn workspace frontend build",
|
"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\""
|
"start": "concurrently --kill-others-on-fail \"yarn workspace backend start\" \"yarn workspace frontend dev\""
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"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",
|
"main": "../../dist/backend/index.js",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "docker build --progress=plain -t simrailpro:backend .",
|
||||||
|
"rawbuild": "yarn tsc",
|
||||||
"start": "yarn build && doppler run node ../../dist/backend/index.js"
|
"start": "yarn build && doppler run node ../../dist/backend/index.js"
|
||||||
},
|
},
|
||||||
"author": "Aleksander <alekswilc> Wilczyński",
|
"author": "Aleksander <alekswilc> Wilczyński",
|
||||||
@ -15,6 +16,7 @@
|
|||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
|
"tsc": "^2.0.4",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -40,12 +40,20 @@ interface ActiveStation
|
|||||||
steam: string;
|
steam: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortFunction = (a: ActiveStation | ActiveTrain, b: ActiveStation | ActiveTrain) => {
|
import { Server } from "@simrail/types";
|
||||||
if (a.server.includes('pl') && !b.server.includes('pl'))
|
import { generateUrl } from "../../util/imgproxy.js";
|
||||||
return -1;
|
|
||||||
|
|
||||||
if (!a.server.includes('pl') && b.server.includes('pl'))
|
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 1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
@ -58,15 +66,22 @@ export class ActivePlayersRoute
|
|||||||
|
|
||||||
app.get("/train", async (req, res) =>
|
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[] = [];
|
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))
|
for (const d of data.filter(d => d.TrainData.ControlledBySteamID))
|
||||||
{
|
{
|
||||||
const p = await PlayerUtil.getPlayer(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({
|
p && a.push({
|
||||||
server: d.ServerCode,
|
server: d.ServerCode,
|
||||||
player: p,
|
player: p,
|
||||||
@ -90,24 +105,33 @@ export class ActivePlayersRoute
|
|||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({ records: a })
|
.setData({
|
||||||
|
records: a,
|
||||||
|
servers: Object.keys(client.stations),
|
||||||
|
})
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.get("/station", async (req, res) =>
|
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[] = [];
|
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))
|
for (const d of client.stations[ server ].filter(d => d.DispatchedBy.length && d.DispatchedBy[ 0 ]?.SteamId))
|
||||||
{
|
{
|
||||||
// todo: optimize
|
// todo: optimize
|
||||||
const p = await PlayerUtil.getPlayer(d.DispatchedBy[ 0 ].SteamId!);
|
const p = await PlayerUtil.getPlayer(d.DispatchedBy[ 0 ].SteamId!);
|
||||||
|
|
||||||
|
if (p && process.env.IMGPROXY_KEY)
|
||||||
|
{
|
||||||
|
p.avatar = generateUrl(p.avatar);
|
||||||
|
}
|
||||||
|
|
||||||
p && a.push({
|
p && a.push({
|
||||||
server: server,
|
server: server,
|
||||||
player: p,
|
player: p,
|
||||||
@ -126,14 +150,16 @@ export class ActivePlayersRoute
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
a = arrayGroupBy(a, d => d.server)
|
a = arrayGroupBy(a, d => d.server)
|
||||||
.sort(sortFunction);
|
.sort(sortFunction);
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({ records: a })
|
.setData({
|
||||||
|
records: a,
|
||||||
|
servers: Object.keys(client.stations),
|
||||||
|
})
|
||||||
.toJSON(),
|
.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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -17,8 +17,9 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { PipelineStage } from "mongoose";
|
import { PipelineStage } from "mongoose";
|
||||||
import { IProfile, MProfile } from "../../mongo/profile.js";
|
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 { escapeRegexString, removeProperties } from "../../util/functions.js";
|
||||||
|
import { generateUrl } from "../../util/imgproxy.js";
|
||||||
|
|
||||||
const generateSearch = (regex: RegExp) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
{
|
{
|
||||||
@ -43,7 +44,17 @@ export class LeaderboardRoute
|
|||||||
|
|
||||||
app.get("/train", async (req, res) =>
|
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[] = [
|
const filter: PipelineStage[] = [
|
||||||
{
|
{
|
||||||
@ -61,23 +72,61 @@ 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)
|
filter.push({
|
||||||
.sort(sortBy)
|
$sort: sortBy,
|
||||||
.limit(50);
|
});
|
||||||
|
|
||||||
|
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(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.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(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/station", async (req, res) =>
|
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[] = [
|
const filter: PipelineStage[] = [
|
||||||
{
|
{
|
||||||
@ -94,14 +143,43 @@ export class LeaderboardRoute
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await MProfile.aggregate(filter)
|
filter.push({
|
||||||
.sort({ dispatcherTime: -1 })
|
$sort: {
|
||||||
.limit(50);
|
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(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.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(),
|
.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
|
* 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
|
* 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 { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { MStationLog } from "../../mongo/stationLog.js";
|
import { MStationLog } from "../../mongo/stationLog.js";
|
||||||
import { IProfile } from "../../mongo/profile.js";
|
import { IProfile } from "../../mongo/profile.js";
|
||||||
|
import { generateUrl } from "../../util/imgproxy.js";
|
||||||
|
|
||||||
|
|
||||||
export class LogRoute
|
export class LogRoute
|
||||||
@ -60,7 +61,13 @@ export class LogRoute
|
|||||||
return;
|
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;
|
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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,7 +19,8 @@ import { PipelineStage } from "mongoose";
|
|||||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { PlayerUtil } from "../../util/PlayerUtil.js";
|
import { PlayerUtil } from "../../util/PlayerUtil.js";
|
||||||
import { IProfile, MProfile } from "../../mongo/profile.js";
|
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||||
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
import { escapeRegexString } from "../../util/functions.js";
|
||||||
|
import { generateUrl } from "../../util/imgproxy.js";
|
||||||
|
|
||||||
const generateSearch = (regex: RegExp) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
{
|
{
|
||||||
@ -111,6 +112,11 @@ export class ProfilesRoute
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (process.env.IMGPROXY_KEY)
|
||||||
|
{
|
||||||
|
player.avatar = generateUrl(player.avatar, "rs:auto:256:256:1/f:png");
|
||||||
|
}
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
@ -127,6 +133,16 @@ export class ProfilesRoute
|
|||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
|
|
||||||
|
const 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[] = [
|
const filter: PipelineStage[] = [
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
@ -143,13 +159,38 @@ export class ProfilesRoute
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await MProfile.aggregate(filter)
|
filter.push({
|
||||||
.limit(50);
|
$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(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({ records: records.map(x => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
.setData({
|
||||||
|
records: records[ 0 ].data,
|
||||||
|
pages: Math.floor((records[ 0 ]?.count?.[ 0 ]?.count ?? 0) / limit),
|
||||||
|
})
|
||||||
.toJSON(),
|
.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
|
* 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
|
* 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 { IStationLog, MStationLog } from "../../mongo/stationLog.js";
|
||||||
import { PipelineStage } from "mongoose";
|
import { PipelineStage } from "mongoose";
|
||||||
import { escapeRegexString } from "../../util/functions.js";
|
import { escapeRegexString } from "../../util/functions.js";
|
||||||
import { SuccessResponseBuilder } from "../responseBuilder.js";
|
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { MProfile } from "../../mongo/profile.js";
|
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||||
|
import { generateUrl } from "../../util/imgproxy.js";
|
||||||
|
|
||||||
const generateSearch = (regex: RegExp) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
{
|
{
|
||||||
@ -47,7 +48,19 @@ export class StationsRoute
|
|||||||
|
|
||||||
app.get("/", async (req, res) =>
|
app.get("/", async (req, res) =>
|
||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
const s = req.query.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[] = [];
|
const filter: PipelineStage[] = [];
|
||||||
|
|
||||||
s && filter.push({
|
s && filter.push({
|
||||||
@ -58,16 +71,56 @@ export class StationsRoute
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await MStationLog.aggregate(filter)
|
server && filter.push({
|
||||||
.sort({ leftDate: -1 })
|
$match: {
|
||||||
.limit(30);
|
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(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: IStationLog[] }>()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.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(),
|
.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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -46,7 +46,6 @@ export class StatsRoute
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return app;
|
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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -17,9 +17,10 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { PipelineStage } from "mongoose";
|
import { PipelineStage } from "mongoose";
|
||||||
import { ITrainLog, MTrainLog } from "../../mongo/trainLog.js";
|
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 { 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) => [
|
const generateSearch = (regex: RegExp) => [
|
||||||
{
|
{
|
||||||
@ -44,10 +45,20 @@ export class TrainsRoute
|
|||||||
|
|
||||||
app.get("/", async (req, res) =>
|
app.get("/", async (req, res) =>
|
||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
|
const server = req.query.server?.toString();
|
||||||
|
|
||||||
const filter: PipelineStage[] = [];
|
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({
|
s && filter.push({
|
||||||
$match: {
|
$match: {
|
||||||
@ -57,17 +68,55 @@ export class TrainsRoute
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await MTrainLog.aggregate(filter)
|
server && filter.push({
|
||||||
.sort({ leftDate: -1 })
|
$match: {
|
||||||
.limit(30);
|
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(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: ITrainLog[] }>()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({
|
.setData({
|
||||||
records,
|
records: records[ 0 ].data,
|
||||||
|
pages: Math.floor((records[ 0 ]?.count?.[ 0 ]?.count ?? 0) / limit),
|
||||||
|
servers: Object.keys(client.stations),
|
||||||
})
|
})
|
||||||
.toJSON(),
|
.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
|
* 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
|
* 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
|
* 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
|
* 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 { Server, Station, Train } from "@simrail/types";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { TMProfile } from "./mongo/profile.js";
|
import { TMProfile } from "./mongo/profile.js";
|
||||||
|
import { GitUtil } from "./util/git.js";
|
||||||
|
|
||||||
;(async () =>
|
;(async () =>
|
||||||
{
|
{
|
||||||
@ -47,6 +48,7 @@ import { TMProfile } from "./mongo/profile.js";
|
|||||||
}
|
}
|
||||||
|
|
||||||
ApiModule.load(); // TODO: use fastify
|
ApiModule.load(); // TODO: use fastify
|
||||||
|
GitUtil.getData();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development")
|
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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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 { MProfile } from "../mongo/profile.js";
|
||||||
import { assert } from "node:console";
|
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) =>
|
const steamFetch = (url: string) =>
|
||||||
{
|
{
|
||||||
@ -28,12 +28,13 @@ const steamFetch = (url: string) =>
|
|||||||
{
|
{
|
||||||
const req = () =>
|
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))
|
.then(x => res(x))
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
{
|
{
|
||||||
console.log("STEAM request failed! ", url.replace(STEAM_API_KEY!, "[XXX]"), retries);
|
console.log("STEAM request failed! ", url.replace("[STEAMKEY]", steamKey), retries);
|
||||||
|
|
||||||
retries++;
|
retries++;
|
||||||
setTimeout(() => req(), retries * 1000);
|
setTimeout(() => req(), retries * 1000);
|
||||||
@ -54,7 +55,7 @@ export class PlayerUtil
|
|||||||
|
|
||||||
if (!player)
|
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");
|
assert(data.response.players, "Expected data.response.players to be truthy");
|
||||||
|
|
||||||
@ -149,7 +150,7 @@ export class PlayerUtil
|
|||||||
|
|
||||||
public static async getPlayerSteamData(steamId: string)
|
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)
|
if (!data?.response?.players?.length)
|
||||||
{
|
{
|
||||||
@ -161,7 +162,7 @@ export class PlayerUtil
|
|||||||
|
|
||||||
public static async getPlayerStats(steamId: string)
|
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)
|
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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -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 wcmatch from "wildcard-match";
|
import wcmatch from "wildcard-match";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -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
|
* 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
|
* 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
|
* 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
|
* 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
|
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
|
return process.env.CURRENT_VERSION;
|
||||||
{
|
// try
|
||||||
const data = execSync("git describe --tags --exact-match").toString();
|
// {
|
||||||
return data.replace("\n", "");
|
// const data = execSync("git describe --tags --exact-match").toString();
|
||||||
} catch
|
// return data.replace("\n", "");
|
||||||
{
|
// } catch
|
||||||
return undefined;
|
// {
|
||||||
}
|
// return undefined;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getLatestCommit()
|
private static getLatestCommit()
|
||||||
{
|
{
|
||||||
try
|
return process.env.CURRENT_COMMIT;
|
||||||
{
|
// try
|
||||||
const data = execSync("git rev-parse --short HEAD").toString();
|
// {
|
||||||
return data.replace("\n", "");
|
// const data = execSync("git rev-parse --short HEAD").toString();
|
||||||
} catch
|
// return data.replace("\n", "");
|
||||||
{
|
// } catch
|
||||||
return undefined;
|
// {
|
||||||
}
|
// return undefined;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static getData()
|
public static getData()
|
||||||
{
|
{
|
||||||
if (this.cache && (this.cache.lastUpdated - Date.now()) < 30_000)
|
if (this.cache)
|
||||||
{
|
{
|
||||||
return this.cache;
|
return this.cache;
|
||||||
}
|
}
|
||||||
@ -55,7 +57,6 @@ export class GitUtil
|
|||||||
const data = {
|
const data = {
|
||||||
version: this.getLatestVersion(),
|
version: this.getLatestVersion(),
|
||||||
commit: this.getLatestCommit(),
|
commit: this.getLatestCommit(),
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.cache = data;
|
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
|
* 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
|
* 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. */
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
// "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. */
|
// "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. */
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
// "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. */
|
// "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
|
~ 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
|
~ it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,6 +19,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<link rel="icon" type="image/jpg" href="/favicon.png"/>
|
<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"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>simrail.pro | Simrail Logs</title>
|
<title>simrail.pro | Simrail Logs</title>
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"rawbuild": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"build": "docker build --progress=plain -t simrailpro:frontend ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
@ -35,7 +36,7 @@
|
|||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"vite": "^4.4.7",
|
"vite": "^6.2.0",
|
||||||
"webpack": "^5.88.2"
|
"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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -21,8 +21,7 @@ import { Loader } from "./components/mini/loaders/PageLoader.tsx";
|
|||||||
import { Home } from "./pages/Home";
|
import { Home } from "./pages/Home";
|
||||||
import DefaultLayout from "./layout/DefaultLayout";
|
import DefaultLayout from "./layout/DefaultLayout";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
import { TrainLeaderboard } from "./pages/leaderboard/TrainLeaderboard.tsx";
|
import { Leaderboard } from "./pages/leaderboard/Leaderboard.tsx";
|
||||||
import { StationLeaderboard } from "./pages/leaderboard/StationsLeaderboard.tsx";
|
|
||||||
import { TrainLogs } from "./pages/logs/TrainLogs.tsx";
|
import { TrainLogs } from "./pages/logs/TrainLogs.tsx";
|
||||||
import { StationLogs } from "./pages/logs/StationLogs.tsx";
|
import { StationLogs } from "./pages/logs/StationLogs.tsx";
|
||||||
import { Profile } from "./pages/profiles/Profile.tsx";
|
import { Profile } from "./pages/profiles/Profile.tsx";
|
||||||
@ -83,12 +82,12 @@ function App()
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/leaderboard/trains"
|
path="/leaderboard/"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Train Leaderboard"
|
<PageMeta title="simrail.pro | Train Leaderboard"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<TrainLeaderboard/>
|
<Leaderboard/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -115,18 +114,6 @@ function App()
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/leaderboard/stations"
|
|
||||||
element={
|
|
||||||
<>
|
|
||||||
<PageMeta title="simrail.pro | Station Leaderboard"
|
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
|
||||||
<StationLeaderboard/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/active/trains"
|
path="/active/trains"
|
||||||
element={
|
element={
|
||||||
|
@ -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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
export const SuccessAlert = ({ title, description }: { title: string, description: string }) =>
|
|
||||||
<div
|
|
||||||
className="flex w-full border-l-6 border-[#34D399] bg-[#34D399] bg-opacity-[15%] dark:bg-[#1B1B24] px-7 py-8 shadow-md dark:bg-opacity-30 md:p-9">
|
|
||||||
<div className="mr-5 flex h-9 w-full max-w-[36px] items-center justify-center rounded-lg bg-[#34D399]">
|
|
||||||
<SuccessAlertIcon/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<h5 className="mb-3 text-lg font-semibold text-black dark:text-[#34D399] ">
|
|
||||||
{ title }
|
|
||||||
</h5>
|
|
||||||
<p className="text-base leading-relaxed text-body">
|
|
||||||
{ description }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
@ -1,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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -45,10 +45,10 @@ const DarkModeSwitcher = () =>
|
|||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<span className="dark:hidden">
|
<span className="dark:hidden">
|
||||||
<LightIcon/>
|
<LightIcon />
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden dark:inline-block">
|
<span className="hidden dark:inline-block">
|
||||||
<DarkIcon/>
|
<DarkIcon />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</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
|
* 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
|
* 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">
|
<div className="flex items-center gap-3 2xsm:gap-7">
|
||||||
<ul className="flex items-center gap-2 2xsm:gap-4">
|
<ul className="flex items-center gap-2 2xsm:gap-4">
|
||||||
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("pl") }>
|
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("pl") }>
|
||||||
<ReactCountryFlag countryCode={ "PL" } svg/>
|
<ReactCountryFlag countryCode={ "PL" } svg alt={ "PL" }/>
|
||||||
</a>
|
</a>
|
||||||
<a className="cursor-pointer" onClick={ () => i18n.changeLanguage("en") }>
|
<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>
|
</a>
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="flex items-center gap-2 2xsm:gap-4">
|
<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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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 { Link } from "react-router-dom";
|
||||||
import { ErrorAlertIcon } from "../icons/AlertIcons.tsx";
|
import { ErrorAlertIcon } from "../icons/AlertIcons.tsx";
|
||||||
|
|
||||||
|
|
||||||
export const LoadError = () =>
|
export const LoadError = () =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -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 { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
export const ConfirmModal = ({ showModal, setShowModal, onConfirm, title, description }: {
|
export const ConfirmModal = ({ showModal, setShowModal, onConfirm, title, description }: {
|
||||||
|
@ -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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -153,70 +153,17 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<SidebarLinkGroup
|
<li>
|
||||||
isOpen={ true }
|
<NavLink
|
||||||
|
to="/leaderboard"
|
||||||
activeCondition={
|
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") &&
|
||||||
pathname === "/leaderboard" || pathname.includes("leaderboard")
|
"bg-graydark dark:bg-meta-4"
|
||||||
}
|
}` }
|
||||||
>
|
>
|
||||||
{ (handleClick, open) =>
|
<FaChartSimple/>
|
||||||
{
|
{ t("sidebar.leaderboard") }
|
||||||
return (
|
</NavLink>
|
||||||
<React.Fragment>
|
</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 === "/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>
|
|
||||||
|
|
||||||
<SidebarLinkGroup
|
<SidebarLinkGroup
|
||||||
isOpen={ true }
|
isOpen={ true }
|
||||||
|
@ -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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -14,9 +14,60 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChangeEventHandler } from "react";
|
import { ChangeEventHandler, Dispatch, ReactNode, SetStateAction } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FcInfo } from "react-icons/fc";
|
|
||||||
|
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 }: {
|
export const Search = ({ searchItem, handleInputChange }: {
|
||||||
searchItem: string;
|
searchItem: string;
|
||||||
@ -24,25 +75,48 @@ export const Search = ({ searchItem, handleInputChange }: {
|
|||||||
}) =>
|
}) =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div
|
<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">
|
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 justify-center items-center">
|
<div className="flex flex-col gap-6 xl:flex-row items-center justify-center">
|
||||||
<input
|
<div className="w-full xl:w-1/2 grow">
|
||||||
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"
|
<input onChange={ handleInputChange }
|
||||||
type="text"
|
value={ searchItem }
|
||||||
onChange={ handleInputChange }
|
type="text" placeholder={ t("search.placeholder") }
|
||||||
value={ searchItem }
|
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"/>
|
||||||
placeholder={ t("search.placeholder") }
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex pt-2 gap-1 align-center">
|
|
||||||
<FcInfo/>
|
|
||||||
<p className="text-sm text-black dark:text-white">
|
|
||||||
{ t("search.tip") }
|
|
||||||
</p>
|
|
||||||
</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>
|
</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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -18,76 +18,51 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TActiveStationPlayersData } from "../../../types/active.ts";
|
import { TActiveStationPlayersData } from "../../../types/active.ts";
|
||||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export const ActiveStationTable = ({ stations }: { stations: TActiveStationPlayersData[] }) =>
|
export const ActiveStationTable = ({ stations }: { stations: TActiveStationPlayersData[] }) =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div
|
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||||
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">
|
{ stations.map((station) =>
|
||||||
<div className="flex flex-col">
|
{
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
return <div
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
key={ station.server + station.stationShort }
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
className="flex flex-col align-center items-center rounded-sm border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
{ t("active.server") }
|
<div className="p-4">
|
||||||
</h5>
|
<img className="rounded-full" src={ station.player.avatar } alt="Player"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="flex flex-col p-2 align-center items-center">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||||
{ t("active.user") }
|
{ station.player.username }
|
||||||
</h5>
|
<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>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap mt-auto mb-2">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<Link to={ "/profile/" + (station.steam ?? station.player.id) }
|
||||||
{ t("active.station") }
|
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" : "" }` }
|
||||||
</h5>
|
style={ station.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||||
</div>
|
<a
|
||||||
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
onClick={ () => report(station) }
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
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"
|
||||||
{ 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="flex items-center justify-center p-2.5 lg:p-5">
|
{ t("log.buttons.report") }
|
||||||
<p className="text-meta-6">{ station.server.toUpperCase() }</p>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</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>
|
|
||||||
</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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TActiveTrainPlayersData } from "../../../types/active.ts";
|
import { TActiveTrainPlayersData } from "../../../types/active.ts";
|
||||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export const ActiveTrainTable = ({ trains }: {
|
export const ActiveTrainTable = ({ trains }: {
|
||||||
trains: TActiveTrainPlayersData[],
|
trains: TActiveTrainPlayersData[],
|
||||||
@ -25,74 +26,48 @@ export const ActiveTrainTable = ({ trains }: {
|
|||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div
|
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||||
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">
|
{ trains.map((train) =>
|
||||||
<div className="flex flex-col">
|
{
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
return <div
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
key={ train.server + train.trainNumber }
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
className="flex flex-col align-center items-center rounded-sm border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
{ t("active.server") }
|
<div className="p-4">
|
||||||
</h5>
|
<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>
|
||||||
|
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap mt-auto mb-2">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<Link to={ "/profile/" + (train.steam ?? train.player.id) }
|
||||||
{ t("active.user") }
|
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" : "" }` }
|
||||||
</h5>
|
style={ train.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>{ t("log.buttons.profile") }</Link>
|
||||||
</div>
|
<a
|
||||||
|
onClick={ () => report(train) }
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
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"
|
||||||
<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="flex items-center justify-center p-2.5 lg:p-5">
|
{ t("log.buttons.report") }
|
||||||
<p className="text-meta-6">{ train.server.toUpperCase() }</p>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
;
|
|
||||||
};
|
};
|
@ -1,87 +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/icons/UserIcons.tsx";
|
|
||||||
|
|
||||||
export const LeaderboardStationTable = ({ 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>
|
|
||||||
|
|
||||||
|
|
||||||
);
|
|
||||||
};
|
|
@ -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,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/icons/UserIcons.tsx";
|
|
||||||
|
|
||||||
export const LeaderboardTrainTable = ({ trains, setSortBy, sortBy }: {
|
|
||||||
trains: TLeaderboardRecord[],
|
|
||||||
setSortBy: Dispatch<SetStateAction<string>>
|
|
||||||
sortBy: string
|
|
||||||
}) =>
|
|
||||||
{
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
|
||||||
{ t("leaderboard.user") }
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
|
||||||
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
|
||||||
onClick={ () => setSortBy("distance") }>
|
|
||||||
{ t("leaderboard.distance") }
|
|
||||||
</h5>
|
|
||||||
<FlexArrowIcon rotated={ sortBy === "distance" || !sortBy }/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
|
||||||
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
|
||||||
onClick={ () => setSortBy("points") }>
|
|
||||||
{ t("leaderboard.points") }
|
|
||||||
</h5>
|
|
||||||
<FlexArrowIcon rotated={ sortBy === "points" }/>
|
|
||||||
</div>
|
|
||||||
{/*<div className="hidden 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-4 ${ trains.length === (key + 1)
|
|
||||||
? ""
|
|
||||||
: "border-b border-stroke dark:border-strokedark"
|
|
||||||
}` }
|
|
||||||
key={ train.id }
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
|
||||||
<Link to={ "/profile/" + train.id }
|
|
||||||
className="color-orchid">{ train.username }</Link> <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,87 +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,115 +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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -44,9 +44,11 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
|||||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
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="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||||
<div
|
<div
|
||||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
className="mx-auto max-w-44 rounded-full">
|
||||||
<div className="relative drop-shadow-2">
|
<div className="relative">
|
||||||
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
<img className="rounded-full"
|
||||||
|
src={ data.player.avatar }
|
||||||
|
alt="Player"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -44,9 +44,11 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
|||||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
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="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||||
<div
|
<div
|
||||||
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
className="mx-auto max-w-44 rounded-full">
|
||||||
<div className="relative drop-shadow-2">
|
<div className="relative">
|
||||||
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
<img className="rounded-full"
|
||||||
|
src={ data.player.avatar }
|
||||||
|
alt="Player"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,6 +19,8 @@ import { Link } from "react-router-dom";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { TStationRecord } from "../../../types/station.ts";
|
import { TStationRecord } from "../../../types/station.ts";
|
||||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const StationTable = ({ stations }: {
|
export const StationTable = ({ stations }: {
|
||||||
@ -27,76 +29,59 @@ export const StationTable = ({ stations }: {
|
|||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
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 (
|
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-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||||
<div
|
{ stations.map((station) =>
|
||||||
className={ `grid grid-cols-3 sm:grid-cols-4 ${ stations.length === (key + 1)
|
{
|
||||||
? ""
|
return <div
|
||||||
: "border-b border-stroke dark:border-strokedark"
|
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">
|
||||||
key={ station.id }
|
<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">
|
{ t("log.buttons.report") }
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
</a>
|
||||||
<Link to={ "/profile/" + (station.steam ?? station.player.id) }
|
</div>
|
||||||
className="color-orchid">{ station.username ?? station.player.username }</Link>
|
</div>;
|
||||||
<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>
|
|
||||||
</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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,6 +19,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { TTrainRecord } from "../../../types/train.ts";
|
import { TTrainRecord } from "../../../types/train.ts";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const TrainTable = ({ trains }: {
|
export const TrainTable = ({ trains }: {
|
||||||
@ -27,95 +28,61 @@ export const TrainTable = ({ trains }: {
|
|||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div
|
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||||
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">
|
{ trains.map((train) =>
|
||||||
<div className="flex flex-col">
|
{
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-6">
|
return <div
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
key={ train.id }
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
className="flex flex-col align-center items-center rounded-sm border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
{ t("logs.user") }
|
<div className="p-4">
|
||||||
</h5>
|
<img className="rounded-full"
|
||||||
|
src={ train.player.avatar }
|
||||||
|
alt="Player"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="flex flex-col p-2 align-center items-center">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||||
{ t("logs.train") }
|
{ train.player.username }
|
||||||
</h5>
|
<UserIcons flags={ train.player.flags }/>
|
||||||
</div>
|
</h4>
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
<p>{ t("log.train.train", { name: train.trainName, number: train.trainNumber }) }</p>
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<p>{ t("log.train.server", { server: train.server.toUpperCase() }) }</p>
|
||||||
{ t("logs.points") }
|
{ train.joinedDate &&
|
||||||
</h5>
|
<p>{ t("log.train.joined", { date: dayjs(train.joinedDate).format("HH:mm DD/MM/YYYY") }) }</p> }
|
||||||
</div>
|
<p>{ t("log.train.left", { date: dayjs(train.leftDate).format("HH:mm DD/MM/YYYY") }) }</p>
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
{ train.joinedDate &&
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<p>{ t("log.train.spent", { date: dayjs.duration(train.leftDate - train.joinedDate).format("H[h] m[m]") }) }</p> }
|
||||||
{ t("logs.distance") }
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:block p-2.5 text-center xl:p-5">
|
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
|
||||||
{ t("logs.time") }
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
|
||||||
{ t("logs.actions") }
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ trains.map((train, key) => (
|
<p>{ t("log.train.distance", { distance: train.distance ? (train.distance / 1000).toFixed(2) : "--" }) }</p>
|
||||||
<div
|
|
||||||
className={ `grid grid-cols-3 sm:grid-cols-6 ${ trains.length === (key + 1)
|
<p>{ t("log.train.points", { points: train.points || "--" }) }</p>
|
||||||
? ""
|
</div>
|
||||||
: "border-b border-stroke dark:border-strokedark"
|
|
||||||
}` }
|
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap mt-auto mb-2">
|
||||||
key={ train.id }
|
<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"
|
||||||
>
|
>
|
||||||
|
{ t("log.buttons.report") }
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
</a>
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
</div>
|
||||||
<Link to={ "/profile/" + (train.steam ?? train.player.id) }
|
</div>;
|
||||||
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>
|
|
||||||
</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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -87,7 +87,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
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="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||||
<div
|
<div
|
||||||
className="mx-auto w-full max-w-30 rounded-full p-1 sm:h-44 sm:max-w-44">
|
className="mx-auto max-w-44 rounded-full">
|
||||||
<div className="relative rounded-full">
|
<div className="relative rounded-full">
|
||||||
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||||
{ data.active &&
|
{ data.active &&
|
||||||
|
@ -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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -18,60 +18,36 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
import { TProfilePlayer } from "../../../types/profile.ts";
|
import { TProfilePlayer } from "../../../types/profile.ts";
|
||||||
|
// import { formatTime } from "../../../util/time.ts";
|
||||||
|
|
||||||
export const ProfilesTable = ({ profiles }: { profiles: TProfilePlayer[] }) =>
|
export const ProfilesTable = ({ profiles }: { profiles: TProfilePlayer[] }) =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4">
|
||||||
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">
|
{ profiles.map((player) =>
|
||||||
|
{
|
||||||
<div className="flex flex-col">
|
return <div
|
||||||
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
|
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-2.5 text-center xl:p-5">
|
<div className="p-4">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<img className="rounded-full" src={ player.avatar } alt="Player"/>
|
||||||
{ t("profiles.user") }
|
|
||||||
</h5>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
<div className="flex flex-col p-2 align-center items-center">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h4 className="mb-3 text-xl font-semibold text-black dark:text-white">
|
||||||
{ t("profiles.actions") }
|
{ player.username }
|
||||||
</h5>
|
<UserIcons flags={ player.flags }/>
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{ profiles.map((profile, key) => (
|
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap mt-auto mb-2">
|
||||||
<div
|
<Link to={ "/profile/" + player.id }
|
||||||
className={ `grid grid-cols-2 ${ profiles.length === (key + 1) // todo: ...
|
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>
|
||||||
: "border-b border-stroke dark:border-strokedark"
|
|
||||||
}` }
|
|
||||||
key={ profile.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/" + profile.id }
|
|
||||||
className="color-orchid">{ profile.username }</Link> <UserIcons
|
|
||||||
flags={ profile.flags }/>
|
|
||||||
|
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>;
|
||||||
|
}) }
|
||||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
|
||||||
<Link
|
|
||||||
to={ "/profile/" + profile.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 ${ profile.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
|
||||||
style={ profile.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
|
||||||
>
|
|
||||||
{ t("profiles.profile") }
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</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
|
* 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
|
* 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
|
* 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
|
* 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;
|
@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] {
|
[x-cloak] {
|
||||||
display: none !important;
|
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 { useContext, createContext, ReactNode } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { get } from "../util/fetcher.ts";
|
import { get } from "../util/fetcher.ts";
|
||||||
|
@ -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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -35,7 +35,7 @@ function useLocalStorage<T>(
|
|||||||
return item ? JSON.parse(item) : initialValue;
|
return item ? JSON.parse(item) : initialValue;
|
||||||
} catch
|
} catch
|
||||||
{
|
{
|
||||||
return item ? item : initialValue;
|
return (item && item !== "undefined") ? item : initialValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error)
|
} 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
|
* 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
|
* 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 LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import translationsInEng from "./languages/en.json";
|
import translationsInEng from "./languages/en.json";
|
||||||
import translationsInPl from "./languages/pl.json";
|
import translationsInPl from "./languages/pl.json";
|
||||||
|
import translationsInCs from "./languages/cs.json";
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: {
|
en: {
|
||||||
@ -27,6 +28,9 @@ const resources = {
|
|||||||
pl: {
|
pl: {
|
||||||
translation: translationsInPl,
|
translation: translationsInPl,
|
||||||
},
|
},
|
||||||
|
cs: {
|
||||||
|
translation: translationsInCs,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void i18n
|
void i18n
|
||||||
@ -35,10 +39,11 @@ void i18n
|
|||||||
.init({
|
.init({
|
||||||
resources,
|
resources,
|
||||||
debug: false,
|
debug: false,
|
||||||
fallbackLng: {
|
fallbackLng: (code: string) => {
|
||||||
"pl-PL": [ "pl" ],
|
if (code.includes('pl')) return 'pl'; // Polish
|
||||||
|
if (resources as any['cs'] && code.includes('cs')) return 'cs'; // Czech
|
||||||
|
|
||||||
default: [ "en" ],
|
return 'en'; // English
|
||||||
},
|
},
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
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,17 +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": {
|
"search": {
|
||||||
"placeholder": "Type to search.",
|
"placeholder": "ex. alekswilc",
|
||||||
"tip": "You can search for multiple data using a comma, ex. pl2,alekswilc (server name, username)"
|
"placeholder_with_station": "ex. alekswilc,Katowice",
|
||||||
|
"none": "None"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"stats": {
|
"stats": {
|
||||||
@ -23,7 +14,8 @@
|
|||||||
"description": "Simrail Stats - The best SimRail logs and statistics site!",
|
"description": "Simrail Stats - The best SimRail logs and statistics site!",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"project": "Project page",
|
"project": "Project page",
|
||||||
"forum": "Forum Page"
|
"forum": "Forum page",
|
||||||
|
"discord": "Discord server"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"license": "License:",
|
"license": "License:",
|
||||||
@ -37,37 +29,19 @@
|
|||||||
"description": "It seems you're lost.",
|
"description": "It seems you're lost.",
|
||||||
"button": "Return to homepage."
|
"button": "Return to homepage."
|
||||||
},
|
},
|
||||||
"profiles": {
|
|
||||||
"user": "Player",
|
|
||||||
"profile": "Profile",
|
|
||||||
"actions": "Actions"
|
|
||||||
},
|
|
||||||
"leaderboard": {
|
"leaderboard": {
|
||||||
"user": "Player",
|
"train": {
|
||||||
"time": "Time",
|
"distance": "Driven distance: {{distance}}km",
|
||||||
"distance": "Distance",
|
"points": "Earned points: {{points}}"
|
||||||
"points": "Points",
|
},
|
||||||
"profile": "Profile",
|
"station": {
|
||||||
"actions": "Actions"
|
"time": "Time spent as a dispatcher: {{time}}"
|
||||||
},
|
},
|
||||||
"active": {
|
"buttons": {
|
||||||
"server": "Server",
|
"trainDistance": "Longest distance traveled",
|
||||||
"user": "Player",
|
"trainPoints": "Most points",
|
||||||
"train": "Train",
|
"dispatcherTime": "Most hours as dispatcher"
|
||||||
"station": "Station",
|
}
|
||||||
"profile": "Profile",
|
|
||||||
"actions": "Actions"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"user": "Player",
|
|
||||||
"time": "Time",
|
|
||||||
"distance": "Distance",
|
|
||||||
"points": "Points",
|
|
||||||
"profile": "Profile",
|
|
||||||
"record": "Record",
|
|
||||||
"train": "Train",
|
|
||||||
"actions": "Actions",
|
|
||||||
"station": "Station"
|
|
||||||
},
|
},
|
||||||
"content_loader": {
|
"content_loader": {
|
||||||
"error": {
|
"error": {
|
||||||
@ -145,12 +119,13 @@
|
|||||||
},
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"copied": "Link copied to clipboard!",
|
"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": {
|
"buttons": {
|
||||||
"report": "Report",
|
"report": "Report",
|
||||||
"copy": "Copy link",
|
"copy": "Copy link",
|
||||||
"profile": "Profile"
|
"profile": "Profile",
|
||||||
|
"record": "Record"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
@ -164,7 +139,7 @@
|
|||||||
"admin": "ADMIN",
|
"admin": "ADMIN",
|
||||||
"logged": "Logged as {{username}}",
|
"logged": "Logged as {{username}}",
|
||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
"profiles": "Profiles"
|
"profiles": "Players"
|
||||||
},
|
},
|
||||||
"icons": {
|
"icons": {
|
||||||
"admin": "Administrator",
|
"admin": "Administrator",
|
||||||
|
@ -1,17 +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": {
|
"search": {
|
||||||
"placeholder": "Wpisz, aby wyszukać",
|
"placeholder": "np. alekswilc",
|
||||||
"tip": "Możesz wyszukiwać wiele danych używając przecinka, np pl2,alekswilc (nazwa serwera, nazwa użytkownika)"
|
"placeholder_with_station": "np. alekswilc,Katowice",
|
||||||
|
"none": "Brak"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"stats": {
|
"stats": {
|
||||||
@ -23,7 +14,8 @@
|
|||||||
"description": "Najbardziej rozbudowana strona z logami i statystykami gry SimRail!",
|
"description": "Najbardziej rozbudowana strona z logami i statystykami gry SimRail!",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"project": "Strona projektu",
|
"project": "Strona projektu",
|
||||||
"forum": "Strona na forum"
|
"forum": "Strona na forum",
|
||||||
|
"discord": "Serwer discord"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"license": "Licencja:",
|
"license": "Licencja:",
|
||||||
@ -38,36 +30,18 @@
|
|||||||
"button": "Wróć do strony głównej"
|
"button": "Wróć do strony głównej"
|
||||||
},
|
},
|
||||||
"leaderboard": {
|
"leaderboard": {
|
||||||
"user": "Gracz",
|
"train": {
|
||||||
"time": "Czas",
|
"distance": "Przejechany dystans: {{distance}}km",
|
||||||
"distance": "Dystans",
|
"points": "Zdobyte punkty: {{points}}"
|
||||||
"points": "Punkty",
|
},
|
||||||
"profile": "Profil",
|
"station": {
|
||||||
"actions": "Akcje"
|
"time": "Spędzony czas jako dyżurny ruchu: {{time}}"
|
||||||
},
|
},
|
||||||
"profiles": {
|
"buttons": {
|
||||||
"user": "Gracz",
|
"trainDistance": "Największy przejechany dystans",
|
||||||
"profile": "Profil",
|
"trainPoints": "Najwięcej punktów",
|
||||||
"actions": "Akcje"
|
"dispatcherTime": "Najwięcej godzin na nastawni"
|
||||||
},
|
}
|
||||||
"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",
|
|
||||||
"station": "Stacja"
|
|
||||||
},
|
},
|
||||||
"content_loader": {
|
"content_loader": {
|
||||||
"error": {
|
"error": {
|
||||||
@ -145,19 +119,15 @@
|
|||||||
},
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"copied": "Skopiowano link do schowka!",
|
"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": {
|
"buttons": {
|
||||||
"report": "Zgłoś",
|
"report": "Zgłoś",
|
||||||
"copy": "Kopiuj link",
|
"copy": "Kopiuj link",
|
||||||
"profile": "Profil"
|
"profile": "Profil",
|
||||||
|
"record": "Rekord"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"icons": {
|
|
||||||
"admin": "Administrator",
|
|
||||||
"leaderboard_hidden": "Tablica wyników ukryta",
|
|
||||||
"hidden": "Profil ukryty"
|
|
||||||
},
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"home": "Strona główna",
|
"home": "Strona główna",
|
||||||
"logs": "Logi",
|
"logs": "Logi",
|
||||||
@ -169,7 +139,12 @@
|
|||||||
"admin": "ADMIN",
|
"admin": "ADMIN",
|
||||||
"logged": "Zalogowano jako {{username}}",
|
"logged": "Zalogowano jako {{username}}",
|
||||||
"logout": "Wyloguj",
|
"logout": "Wyloguj",
|
||||||
"profiles": "Profile"
|
"profiles": "Gracze"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"admin": "Administrator",
|
||||||
|
"leaderboard_hidden": "Tablica wyników ukryta",
|
||||||
|
"hidden": "Profil ukryty"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"header": "Akcje moderacyjne",
|
"header": "Akcje moderacyjne",
|
||||||
|
@ -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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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/style.css";
|
||||||
import "./css/satoshi.css";
|
import "./css/satoshi.css";
|
||||||
import "flatpickr/dist/flatpickr.min.css";
|
import "flatpickr/dist/flatpickr.min.css";
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime.js";
|
import relativeTime from "dayjs/plugin/relativeTime.js";
|
||||||
import duration from "dayjs/plugin/duration.js";
|
import duration from "dayjs/plugin/duration.js";
|
||||||
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
dayjs.extend(relativeTime);
|
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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { useDebounce } from "use-debounce";
|
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 { useSearchParams } from "react-router-dom";
|
||||||
import { get } from "../../util/fetcher.ts";
|
import { get } from "../../util/fetcher.ts";
|
||||||
import useSWR from "swr";
|
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 { data, error, isLoading } = useSWR(`/active/station/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||||
|
const [ server, setServer ] = useState(searchParams.get("server") ?? "");
|
||||||
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
searchValue && params.set("q", searchValue);
|
searchValue && params.set("query", searchValue);
|
||||||
|
server && params.set("server", server);
|
||||||
|
|
||||||
setSearchParams(params.toString());
|
setSearchParams(params.toString());
|
||||||
setParams(params);
|
setParams(params);
|
||||||
}, [ searchValue ]);
|
}, [ searchValue, server ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setSearchItem(searchParams.get("q") ?? "");
|
setSearchItem(searchParams.get("query") ?? "");
|
||||||
}, [ searchParams ]);
|
setServer(searchParams.get("server") ?? "");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
setSearchItem(e.target.value);
|
setSearchItem(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
<SearchWithServerSelector handleInputChange={ handleInputChange } searchItem={ searchItem }
|
||||||
<>
|
servers={ data?.code === 200 ? data?.data?.servers : [] }
|
||||||
{ error && <LoadError/> }
|
server={ server } setServer={ setServer }/>
|
||||||
|
{ error && <LoadError/> }
|
||||||
|
|
||||||
{ isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
{ data && (data && data.code === 404) || (data && !data?.data?.records?.length) &&
|
{ data && (data && data.code === 404) || (data && !data?.data?.records?.length) &&
|
||||||
<WarningAlert title={ t("content_loader.notfound.header") }
|
<WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
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>
|
</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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { useDebounce } from "use-debounce";
|
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 { useSearchParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { get } from "../../util/fetcher.ts";
|
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 { data, error, isLoading } = useSWR(`/active/train/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||||
|
const [ server, setServer ] = useState(searchParams.get("server") ?? "");
|
||||||
|
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
searchValue && params.set("q", searchValue);
|
searchValue && params.set("query", searchValue);
|
||||||
|
server && params.set("server", server);
|
||||||
|
|
||||||
setSearchParams(params.toString());
|
setSearchParams(params.toString());
|
||||||
setParams(params);
|
setParams(params);
|
||||||
}, [ searchValue ]);
|
}, [ searchValue, server ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setSearchItem(searchParams.get("q") ?? "");
|
setSearchItem(searchParams.get("query") ?? "");
|
||||||
}, [ searchParams ]);
|
setServer(searchParams.get("server") ?? "");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
@ -64,7 +64,9 @@ export const ActiveTrainPlayers = () =>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<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/> }
|
||||||
|
|
||||||
|
@ -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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
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,84 +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 { LeaderboardStationTable } from "../../components/pages/leaderboard/LeaderboardStationTable.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 &&
|
|
||||||
<LeaderboardStationTable stations={ data.data.records }/> }
|
|
||||||
</>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,88 +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 { LeaderboardTrainTable } from "../../components/pages/leaderboard/LeaderboardTrainTable.tsx";
|
|
||||||
import { useDebounce } from "use-debounce";
|
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { 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";
|
|
||||||
|
|
||||||
export const TrainLeaderboard = () =>
|
|
||||||
{
|
|
||||||
const [ params, setParams ] = useState(new URLSearchParams());
|
|
||||||
|
|
||||||
const { data, error, isLoading } = useSWR(`/leaderboard/train/?${ 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 [ searchValue ] = useDebounce(searchItem, 500);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
searchValue && params.set("q", searchValue);
|
|
||||||
sortBy && params.set("s", sortBy);
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(params.toString());
|
|
||||||
setParams(params);
|
|
||||||
}, [ searchValue, sortBy ]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
setSearchItem(searchParams.get("q") ?? "");
|
|
||||||
setSortBy(searchParams.get("s") ?? "distance");
|
|
||||||
}, [ searchParams ]);
|
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
{
|
|
||||||
setSearchItem(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-10">
|
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
|
||||||
<>
|
|
||||||
{ error && <LoadError/> }
|
|
||||||
|
|
||||||
{ isLoading && <ContentLoader/> }
|
|
||||||
|
|
||||||
{ (data && data.code === 404) || (data && !data?.data?.records?.length) &&
|
|
||||||
<WarningAlert title={ t("content_loader.notfound.header") }
|
|
||||||
description={ t("content_loader.notfound.description") }/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
|
||||||
<LeaderboardTrainTable trains={ data?.data?.records } setSortBy={ setSortBy }
|
|
||||||
sortBy={ sortBy }/> }
|
|
||||||
</>
|
|
||||||
</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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -17,13 +17,14 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { StationTable } from "../../components/pages/logs/StationTable.tsx";
|
import { StationTable } from "../../components/pages/logs/StationTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { SearchWithServerSelector } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { get } from "../../util/fetcher.ts";
|
import { get } from "../../util/fetcher.ts";
|
||||||
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.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 = () =>
|
export const StationLogs = () =>
|
||||||
{
|
{
|
||||||
@ -31,25 +32,28 @@ export const StationLogs = () =>
|
|||||||
const { data, error, isLoading } = useSWR(`/stations/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
const { data, error, isLoading } = useSWR(`/stations/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("query") ?? "");
|
||||||
|
const [ server, setServer ] = useState(searchParams.get("server") ?? "");
|
||||||
|
const [ page, setPage ] = useState(parseInt(searchParams.get("page") as string) || 1);
|
||||||
const [ searchValue ] = useDebounce(searchItem, 500);
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
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());
|
setSearchParams(params.toString());
|
||||||
setParams(params);
|
setParams(params);
|
||||||
}, [ searchValue ]);
|
}, [ searchValue, server, page ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setSearchItem(searchParams.get("q") ?? "");
|
setSearchItem(searchParams.get("query") ?? "");
|
||||||
}, [ searchParams ]);
|
setServer(searchParams.get("server") ?? "");
|
||||||
|
setPage(parseInt(searchParams.get("page") as string) || 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
@ -61,7 +65,10 @@ export const StationLogs = () =>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-10">
|
<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/> }
|
||||||
|
|
||||||
@ -72,7 +79,11 @@ export const StationLogs = () =>
|
|||||||
description={ t("content_loader.notfound.description") }/> }
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||||
<StationTable stations={ data.data.records }/> }
|
<>
|
||||||
|
<StationTable stations={ data.data.records }/>
|
||||||
|
<Paginator page={ page } pages={ data.data.pages } setPage={ setPage }/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
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