Compare commits

..

101 Commits
v3.3.1 ... main

Author SHA1 Message Date
6d752c99d7
Merge pull request 'new-build' () from new-build into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-27 15:55:55 +02:00
d516d22411
build(): new build system 2025-04-27 15:55:32 +02:00
9833bcc09a
build(): new build system 2025-04-27 15:54:53 +02:00
0f2d0bcf44
Merge pull request 'new-build' () from new-build into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-27 15:52:44 +02:00
35bbbd902c
build(): new build system, fix(): remove gf 2025-04-27 15:52:16 +02:00
4b301c9644
build(): test new build sys, add script 2025-04-27 15:44:55 +02:00
69d174e5c4
build(): test new build sys, add script 2025-04-27 15:41:40 +02:00
8d2b870d54
build(): test new build sys, add script 2025-04-27 15:25:42 +02:00
4b6f41a6c7
build(): test new build sys, add script 2025-04-27 15:24:18 +02:00
b41a0a9127
build(): test new build sys 2025-04-27 15:24:09 +02:00
418d5122bc
Merge pull request 'fix(frontend): profile sorting' () from fix-sort into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-27 00:29:48 +02:00
94903146c4
fix(frontend): profile sorting 2025-04-27 00:28:31 +02:00
fb806b0991
Merge pull request 'fix: fix profile' () from fix-profile into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 21:16:25 +02:00
361fc9fb97
fix: fix profile 2025-04-21 21:15:33 +02:00
e14399d04b
Merge pull request 'fix: fix profile' () from fix-profile into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 21:12:53 +02:00
b19b18da5b
fix: fix profile 2025-04-21 21:12:18 +02:00
837e3de296
Merge pull request 'fix: fix build' () from fix-build into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 21:06:22 +02:00
3cfd4752a1
fix: fix build 2025-04-21 21:05:57 +02:00
b6956bfcfc
Merge pull request 'fix: fix build' () from fix-build into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 21:03:35 +02:00
1947859e0d
fix: fix build 2025-04-21 21:03:08 +02:00
a628ad5d56
Merge pull request 'fix: fix build' () from fix-build into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 21:02:30 +02:00
9deb009d82
fix: fix build 2025-04-21 21:02:15 +02:00
2ead40d6ae
Merge pull request 'fix: fix build' () from fix-build into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 20:59:56 +02:00
9b63300a0a
fix: fix build 2025-04-21 20:59:34 +02:00
d51d7d36a8
Merge pull request 'fix: fix build' () from fix-build into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 20:52:59 +02:00
f8afea20df
fix: fix build 2025-04-21 20:52:33 +02:00
68436020c1
Merge pull request 'fix: add required files' () from new-profiles into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 20:48:39 +02:00
f4561e41c3
fix: add required files 2025-04-21 20:48:21 +02:00
1e01260e82
Merge pull request 'feat: rewrite profile section, some minor chaanges' () from new-profiles into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-21 20:47:11 +02:00
82397be39c
fix: use optionalstyle in imgproxy#generateUrl 2025-04-21 20:45:29 +02:00
99dc69ee8f
feat: rewrite profile section, some minor chaanges 2025-04-21 20:41:45 +02:00
996f16a313
Merge pull request 'update license year.' () from license-year into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-04-11 16:52:41 +02:00
0f8b1a4729
update license year. 2025-04-11 16:48:47 +02:00
badefb0f7f
Merge pull request 'build(): drone CI build' () from drone-ci into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-03-27 16:57:27 +01:00
2ba037fab5
build(): drone CI build 2025-03-27 16:56:14 +01:00
637134081a
Merge pull request 'build(): drone CI' () from drone-ci into main
Reviewed-on: 
2025-03-27 16:54:39 +01:00
96c17ffe85
build(): drone CI 2025-03-27 16:54:21 +01:00
8db158afe9
Merge pull request 'build(): drone CI' () from drone-ci into main
Reviewed-on: 
2025-03-27 16:51:58 +01:00
6015349c6f
build(): drone CI 2025-03-27 16:51:18 +01:00
cd191a6f61
Merge pull request 'build(): drone CI' () from drone-ci into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-03-27 16:50:37 +01:00
08bfe767bb
build(): drone CI 2025-03-27 16:49:36 +01:00
6a4ebf56c4
Merge pull request 'fix(): refactor frontend build process to optimize site performance' () from feat-1 into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-03-05 14:40:36 +01:00
1e4cafc07f
fix(): refactor frontend build process to optimize site performance 2025-03-05 14:37:07 +01:00
623bdd8d42
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 16:02:11 +01:00
51b3ff2e22
fix(): rollbacl 2025-03-04 16:01:56 +01:00
816ee5c454
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 16:01:23 +01:00
9a3f004c72
fix(): rollbacl 2025-03-04 16:01:07 +01:00
f528da171b
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 16:00:00 +01:00
b23921a28c
fix(): rollbacl 2025-03-04 15:59:42 +01:00
e045ded046
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:59:19 +01:00
859b7ab3cc
fix(): rollbacl 2025-03-04 15:58:58 +01:00
34432e9622
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:54:47 +01:00
3af371703b
fix(): rollbacl 2025-03-04 15:54:28 +01:00
fcef4e428e
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:49:46 +01:00
ca8e270c5e
fix(): rollbacl 2025-03-04 15:49:30 +01:00
a9d35c604e
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:48:40 +01:00
8c24a29de2
fix(): rollbacl 2025-03-04 15:48:17 +01:00
e079a1177e
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:46:47 +01:00
7bf053e452
fix(): fix nginx 2025-03-04 15:46:09 +01:00
328f665c5c
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:45:21 +01:00
05fe82eff1
fix(): fix nginx 2025-03-04 15:45:05 +01:00
3143ce1058
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:44:06 +01:00
85d18190a7
fix(): fix nginx 2025-03-04 15:43:51 +01:00
45434f5a2d
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:41:16 +01:00
922b7ea633
fix(): fix nginx 2025-03-04 15:40:57 +01:00
f0f01ccda1
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:38:43 +01:00
947bd5dedc
fix(): fix nginx 2025-03-04 15:38:21 +01:00
a2a9fd25b5
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:36:10 +01:00
afdc700a64
fix(): fix nginx 2025-03-04 15:35:54 +01:00
f5ba173b24
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:34:53 +01:00
7459649829
fix(): fix nginx 2025-03-04 15:34:37 +01:00
31c1f3fc40
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:31:35 +01:00
df1d9df212
fix(): fix nginx 2025-03-04 15:31:03 +01:00
680a18d15a
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:30:06 +01:00
40f31ba723
fix(): fix nginx 2025-03-04 15:29:41 +01:00
f9974a3430
Merge pull request 'fix(): rollbacl' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:20:36 +01:00
c5249da57e
fix(): rollbacl 2025-03-04 15:20:19 +01:00
85e00e8d52
Merge pull request 'fix(): fix nginx' () from feat-1 into main
Reviewed-on: 
2025-03-04 15:16:27 +01:00
5841c77913
fix(): fix nginx 2025-03-04 15:16:03 +01:00
84d2972869
Merge pull request 'fix(): nginx config for spa' () from feat-1 into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-03-04 15:09:30 +01:00
94407bc7c4
Merge branch 'main' of https://git.alekswilc.dev/simrail/simrail.pro 2025-03-04 15:06:46 +01:00
47695d7e4f
fix(): nginx config for SPA 2025-03-04 15:06:23 +01:00
529d8b8020
Merge pull request 'feat(): docker, fix darkmode' () from feat-1 into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-02-20 01:39:15 +01:00
3db6ced4d2
feat(): docker, fix darkmode 2025-02-20 01:36:13 +01:00
e5614da846
Merge pull request 'Update packages/backend/src/util/imgproxy.ts' () from alekswilc-patch-1 into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-02-11 03:15:58 +01:00
71a5b77235
Update packages/backend/src/util/imgproxy.ts 2025-02-11 03:14:44 +01:00
7617738ab2
Merge pull request 'fix(): active trains search' () from v3.0.1 into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2025-01-13 02:19:08 +01:00
9c3d3e0767
fix(): fix workflows 2025-01-13 02:18:21 +01:00
294068ee97
fix(): fix workflows 2025-01-13 02:16:42 +01:00
29a7ab3a6b
fix(): fix runner 2025-01-13 02:12:38 +01:00
b4cee2447d
fix(): active trains search 2025-01-13 02:08:56 +01:00
a4091c92e4
Merge pull request 'Update .gitea/workflows/build.yaml' () from alekswilc-patch-1 into main
Reviewed-on: 
Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2024-12-31 02:29:47 +01:00
77a9540be4
Update .gitea/workflows/build.yaml 2024-12-31 02:29:04 +01:00
f8f5a38add
Update .gitea/workflows/build.yaml 2024-12-31 02:26:37 +01:00
4c7482b919
Update .gitea/workflows/build.yaml 2024-12-31 02:25:37 +01:00
f10c623aa8
Update .gitea/workflows/build.yaml 2024-12-31 02:23:29 +01:00
4e090ed281
Update .gitea/workflows/build.yaml 2024-12-31 02:12:33 +01:00
1b9c616e16
Update .gitea/workflows/build.yaml 2024-12-31 02:07:55 +01:00
7fe575a651
Update .gitea/workflows/build.yaml 2024-12-31 02:07:05 +01:00
f33c54ba26
Update .gitea/workflows/build.yaml 2024-12-31 02:05:54 +01:00
a9094fd1ed
Add .gitea/workflows/build.yaml 2024-12-31 02:04:04 +01:00
108 changed files with 1099 additions and 692 deletions

49
.drone.yml Normal file

@ -0,0 +1,49 @@
---
kind: pipeline
type: docker
name: build
steps:
- name: build-backend
image: plugins/docker
environment:
TAG:
${DRONE_TAG}
COMMIT:
${DRONE_COMMIT}
settings:
insecure: true
repo: 10.5.0.103:1222/simrail-backend
registry: 10.5.0.103:1222
context: './packages/backend'
dockerfile: './packages/backend/Dockerfile'
build_args_from_env:
- TAG
- COMMIT
tags:
- latest
- ${DRONE_TAG##v}
- name: build-frontend
image: plugins/docker
environment:
VITE_API_URL:
from_secret: VITE_API_URL
VITE_STATS_KEY:
from_secret: VITE_STATS_KEY
settings:
insecure: true
repo: 10.5.0.103:1222/simrail-frontend
registry: 10.5.0.103:1222
context: './packages/frontend'
dockerfile: './packages/frontend/Dockerfile'
build_args_from_env:
- VITE_API_URL
- VITE_STATS_KEY
tags:
- latest
- ${DRONE_TAG##v}
trigger:
event:
- tag

@ -4,7 +4,6 @@
"main": "dist/index.js",
"scripts": {
"build": "yarn workspace backend build && yarn workspace frontend build",
"postbuild": "copyfiles --error ./LICENSE.txt ./dist && copyfiles --error ./LICENSE.txt ./dist/frontend/",
"start": "concurrently --kill-others-on-fail \"yarn workspace backend start\" \"yarn workspace frontend dev\""
},
"workspaces": [

@ -0,0 +1,24 @@
FROM node:21-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN yarn add -D typescript
RUN yarn rawbuild
ARG COMMIT
ENV COMMIT $COMMIT
ARG TAG
ENV TAG $TAG
RUN yarn make-git-info
# 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/src"]

@ -0,0 +1,4 @@
{
"tag": "",
"commit": ""
}

@ -4,8 +4,10 @@
"main": "../../dist/backend/index.js",
"version": "3.0.0",
"scripts": {
"build": "tsc",
"start": "yarn build && doppler run node ../../dist/backend/index.js"
"build": "docker build --progress=plain -t simrailpro:backend .",
"rawbuild": "yarn tsc",
"start": "yarn rawbuild && node --env-file=.env dist/index.js",
"make-git-info": "node scripts/make-git.js"
},
"author": "Aleksander <alekswilc> Wilczyński",
"license": "AGPL-3.0-only",
@ -15,6 +17,7 @@
"@types/node": "^22.10.1",
"@types/uuid": "^10.0.0",
"copyfiles": "^2.4.1",
"tsc": "^2.0.4",
"typescript": "^5.5.4"
},
"type": "module",

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -0,0 +1,24 @@
/*
* 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 fs from 'node:fs/promises';
(async () => {
await fs.writeFile('dist/git.json', JSON.stringify({
tag: process.env.TAG ?? "",
commit: process.env.COMMIT?.substring(0, 7) ?? "",
}))
})();

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -66,10 +66,9 @@ export class ActivePlayersRoute
app.get("/train", async (req, res) =>
{
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
const s = req.query.query?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
const sserver = req.query.server?.toString();
let a: ActiveTrain[] = [];
for (const data of sserver ? [ client.trains[ sserver as Server["ServerCode"] ] ] : Object.values(client.trains))

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -0,0 +1,53 @@
/*
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* See LICENSE for more.
*/
import { Router } from "express";
import { SuccessResponseBuilder } from '../responseBuilder.js';
import { generateUrl } from '../../util/imgproxy.js';
import { trainsMap, stationsMap } from '../../util/contants.js';
export class ImagesRoute {
static load() {
const app = Router();
app.get("/", async (req, res) => {
const trains: Record<string, string> = {};
Object.keys(trainsMap).forEach(x => {
trains[x] = generateUrl(trainsMap[x], "f:png/q:50/rs:auto:512:1024:1");
})
const stations: Record<string, string> = {};
Object.keys(stationsMap).forEach(x => {
stations[x] = generateUrl(stationsMap[x], "f:png/q:50/rs:auto:512:1024:1");
})
res.json(
new SuccessResponseBuilder()
.setCode(200)
.setData({
trains,
stations
})
.toJSON(),
);
});
return app;
}
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -67,7 +67,7 @@ export class LogRoute
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()));
res.status(200).json(new SuccessResponseBuilder().setCode(200).setData(log.toJSON()).toJSON());
});
return app;

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -24,6 +24,7 @@ import { StatsRoute } from "./routes/stats.js";
import { LogRoute } from "./routes/log.js";
import { ActivePlayersRoute } from "./routes/activePlayer.js";
import { AdminRoute } from "./routes/admin.js";
import { ImagesRoute } from './routes/images.js';
export class ApiModule
{
@ -39,6 +40,8 @@ export class ApiModule
router.use("/leaderboard/", LeaderboardRoute.load());
router.use("/active/", ActivePlayersRoute.load());
router.use("/admin/", AdminRoute.load());
router.use("/images/", ImagesRoute.load());
router.use("/stats/", StatsRoute.load());
router.use("/log/", LogRoute.load());

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,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";
/*
@ -120,10 +137,80 @@ export const trainsList = [
"4E/EU07-*",
],
},
{
train: 'Ty2',
pattern: [
'Ty2/*'
]
}
];
export const stationsMap: Record<string, string> = {
"Grodzisk Mazowiecki": "https://api.simrail.eu:8083/Thumbnails/Stations/gr1m.jpg",
"Korytów": "https://api.simrail.eu:8083/Thumbnails/Stations/kr1m.jpg",
"Szeligi": "https://api.simrail.eu:8083/Thumbnails/Stations/sz1m.jpg",
"Włoszczowa Północ": "https://api.simrail.eu:8083/Thumbnails/Stations/wp1m.jpg",
"Knapówka": "https://api.simrail.eu:8083/Thumbnails/Stations/kn1m.jpg",
"Psary": "https://api.simrail.eu:8083/Thumbnails/Stations/ps1m.jpg",
"Góra Włodowska": "https://api.simrail.eu:8083/Thumbnails/Stations/gw1m.jpg",
"Idzikowice": "https://api.simrail.eu:8083/Thumbnails/Stations/id1m.jpg",
"Katowice Zawodzie": "https://api.simrail.eu:8083/Thumbnails/Stations/kz1m.jpg",
"Sosnowiec Główny": "https://api.simrail.eu:8083/Thumbnails/Stations/sg1m.jpg",
"Dąbrowa Górnicza": "https://api.simrail.eu:8083/Thumbnails/Stations/dg1m.jpg",
"Zawiercie": "https://api.simrail.eu:8083/Thumbnails/Stations/zw1m.jpg",
"Będzin": "https://api.simrail.eu:8083/Thumbnails/Stations/b1m.jpg",
"Sosnowiec Południowy": "https://api.simrail.eu:8083/Thumbnails/Stations/spl1m.jpg",
"Opoczno Południe": "https://api.simrail.eu:8083/Thumbnails/Stations/op1m.jpg",
"Dąbrowa Górnicza Wschodnia": "https://api.simrail.eu:8083/Thumbnails/Stations/dws1m.jpg",
"Dorota": "https://api.simrail.eu:8083/Thumbnails/Stations/dra1m.jpg",
"Łazy Ła": "https://api.simrail.eu:8083/Thumbnails/Stations/la1m.jpg",
"Łazy": "https://api.simrail.eu:8083/Thumbnails/Stations/lb1m.jpg",
"Juliusz": "https://api.simrail.eu:8083/Thumbnails/Stations/ju1m.jpg",
"Łazy Łc": "https://api.simrail.eu:8083/Thumbnails/Stations/lc1m.jpg",
"Katowice": "https://api.simrail.eu:8083/Thumbnails/Stations/ko1m.jpg",
"Dąbrowa Górnicza Ząbkowice": "https://api.simrail.eu:8083/Thumbnails/Stations/dz1m.jpg",
"Sławków": "https://api.simrail.eu:8083/Thumbnails/Stations/sl1m.jpg",
"Starzyny": "https://api.simrail.eu:8083/Thumbnails/Stations/str1m.jpg",
"Bukowno": "https://api.simrail.eu:8083/Thumbnails/Stations/bo1m.jpg",
"Tunel": "https://api.simrail.eu:8083/Thumbnails/Stations/tl1m.jpg",
"Dąbrowa Górnicza Huta Katowice": "https://api.simrail.eu:8083/Thumbnails/Stations/dghk1m.jpg",
"Sosnowiec Kazimierz": "https://api.simrail.eu:8083/Thumbnails/Stations/skz1m.jpg",
"Pruszków": "https://api.simrail.eu:8083/Thumbnails/Stations/pr1m.jpg",
"Strzałki": "https://api.simrail.eu:8083/Thumbnails/Stations/st1m.jpg",
"Olszamowice": "https://api.simrail.eu:8083/Thumbnails/Stations/ol1m.jpg",
"Miechów": "https://api.simrail.eu:8083/Thumbnails/Stations/mi1m.jpg",
"Kraków Przedmieście": "https://api.simrail.eu:8083/Thumbnails/Stations/kpm1m.jpg",
"Kraków Batowice": "https://api.simrail.eu:8083/Thumbnails/Stations/kb1m.jpg",
"Raciborowice": "https://api.simrail.eu:8083/Thumbnails/Stations/ra1m.jpg",
"Zastów": "https://api.simrail.eu:8083/Thumbnails/Stations/zs1m.jpg",
"Niedźwiedź": "https://api.simrail.eu:8083/Thumbnails/Stations/nd1m.jpg",
"Słomniki": "https://api.simrail.eu:8083/Thumbnails/Stations/sm1m.jpg",
"Kozłów": "https://api.simrail.eu:8083/Thumbnails/Stations/koz1m.jpg",
"N/A": 'https://shared.steamstatic.com/store_item_assets/steam/apps/1422130/header.jpg'
};
export const trainsMap: Record<string, string> = {
"Traxx (E186)": "https://wiki.simrail.eu/vehicle/poland/trains/elec-loco/traxx/20241029163359_1.jpg",
"Dragon2 (E6ACTa, E6ACTadb)": "https://wiki.simrail.eu/vehicle/e6acta-016.jpg",
"Dragon2 (ET25)": "https://wiki.simrail.eu/vehicle/et25-002.jpg",
"Pendolino (ED250)": "https://wiki.simrail.eu/vehicle/ed250-001.png",
"EN57": "https://wiki.simrail.eu/vehicle/en57-009.png",
"EN71": "https://wiki.simrail.eu/vehicle/en71-002.png",
"EN76": "https://wiki.simrail.eu/vehicle/en76-006.jpg",
"EN96": "https://wiki.simrail.eu/vehicle/en96-001.jpg",
"EP07": "https://wiki.simrail.eu/vehicle/ep07-174.jpg",
"EP08": "https://wiki.simrail.eu/vehicle/poland/trains/elec-loco/ep08/20241106002003_1.jpg",
"ET22": "https://wiki.simrail.eu/vehicle/et22-243.png",
"EU07": "https://wiki.simrail.eu/vehicle/eu07-005.jpg",
"Ty2": "https://wiki.simrail.eu/vehicle/ty2-70.png",
"N/A": 'https://shared.steamstatic.com/store_item_assets/steam/apps/1422130/header.jpg'
};
export const getVehicle = (name: string) =>
{
return trainsList.find(x => wcmatch(x.pattern)(name))?.train;
};
};

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -14,7 +14,7 @@
* See LICENSE for more.
*/
import { execSync } from "child_process";
import gitInfo from '../../git.json' with { type: "json" };
export class GitUtil
{
@ -22,26 +22,12 @@ export class GitUtil
private static getLatestVersion()
{
try
{
const data = execSync("git describe --tags --exact-match").toString();
return data.replace("\n", "");
} catch
{
return undefined;
}
return gitInfo.tag;
}
private static getLatestCommit()
{
try
{
const data = execSync("git rev-parse --short HEAD").toString();
return data.replace("\n", "");
} catch
{
return undefined;
}
return gitInfo.commit;
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -28,7 +28,7 @@ export const imgProxySign = (target: string) =>
export const generateUrl = (url: string, options: string = "rs:auto:128:128:1/f:png") =>
{
if (url.includes('https://imgproxy.alekswilc.dev/')) return url;
if (url.includes('https://proxy.cdn.alekswilc.dev/')) return url;
if (process.env.NODE_ENV === "development")
{
@ -38,5 +38,5 @@ export const generateUrl = (url: string, options: string = "rs:auto:128:128:1/f:
}
const signature = imgProxySign(`/${ options }/plain/${ url }`);
return `https://imgproxy.alekswilc.dev/${ signature }/${ options }/plain/${ url }`;
return `https://proxy.cdn.alekswilc.dev/${ signature }/${ options }/plain/${ url }`;
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -39,7 +39,7 @@
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
@ -55,7 +55,7 @@
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "../../dist/backend", /* Specify an output folder for all emitted files. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */

@ -0,0 +1,23 @@
FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install
RUN npm i -g serve vite
ARG VITE_API_URL
ENV VITE_API_URL $VITE_API_URL
ARG VITE_STATS_KEY
ENV VITE_STATS_KEY $VITE_STATS_KEY
COPY . .
RUN npm run rawbuild
EXPOSE 3000
CMD [ "serve", "-s", "dist" ]

@ -1,5 +1,5 @@
<!--
~ Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
~ Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published

@ -5,8 +5,9 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"rawbuild": "vite build",
"preview": "vite preview",
"build": "docker build --progress=plain -t simrailpro:frontend ."
},
"dependencies": {
"dayjs": "^1.11.13",
@ -35,7 +36,7 @@
"prettier": "^3.0.0",
"prettier-plugin-tailwindcss": "^0.4.1",
"tailwindcss": "^3.4.1",
"vite": "^4.4.7",
"vite": "^6.2.0",
"webpack": "^5.88.2"
}
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,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";
export const ConfirmModal = ({ showModal, setShowModal, onConfirm, title, description }: {

@ -0,0 +1,41 @@
/*
* 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 { formatTime } from '../../../util/time.ts'
export const StationStat = ({ stationName, time, image }: { stationName: string, time: number, image: string }) => {
const { t } = useTranslation();
stationName = stationName === 'N/A'
? "Untracked" : stationName
return <div
key={stationName}
className="flex flex-col align-center items-center rounded border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
<div className="p-4">
<img className=""
src={image}
alt="station icon" />
</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">
{stationName}
</h4>
<p className={'break-words'}>{t('profile.stations.time', { time: formatTime(time) })}</p>
</div>
</div>
}

@ -0,0 +1,43 @@
/*
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* See LICENSE for more.
*/
import { useTranslation } from 'react-i18next'
import { formatTime } from '../../../util/time.ts'
export const TrainStat = ({ trainName, time, distance, score, image }: { trainName: string, time: number, distance: number, score: number, image: string }) => {
const { t } = useTranslation();
trainName = trainName === 'N/A'
? "Untracked" : trainName
return <div
key={trainName}
className="flex flex-col align-center items-center rounded border border-stroke bg-stroke shadow-default dark:border-strokedark dark:bg-boxdark">
<div className="p-4">
<img className=""
src={image}
alt="train icon" />
</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">
{trainName}
</h4>
<p className={'break-words'}>{t('profile.trains.time', { time: formatTime(time) })}</p>
<p className={'break-words'}>{t('profile.trains.distance', { distance: Math.floor(distance / 1000) })}m</p>
<p className={'break-words pb-4'}>{t('profile.trains.score', { score: score })}</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
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -16,59 +16,58 @@
import { Dispatch, SetStateAction } from "react";
const getPaginationNums = (page: number, pages: number) => {
if (pages <= 5)
return Array.from({ length: pages }, (_, i) => i + 1);
const numbers = [1];
if (page <= 3) {
numbers.push(2, 3, 4);
} else if (page >= pages - 2) {
numbers.push(pages - 3, pages - 2, pages - 1);
} else {
numbers.push(page - 1, page, page + 1);
}
numbers.push(pages);
return [...new Set(numbers)].sort((a, b) => a - b);
}
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);
}) => {
// todo: rewrite this shit XDDDDDDDD
const numbers = getPaginationNums(page, pages);
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">
className="rounded-sm 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) }>
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">
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>
fill=""></path>
</svg>
</a></li>
{ numbers.map(num =>
{
{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>
<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) }>
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">
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="">
fill="">
</path>
</svg>
</a>

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -15,9 +15,8 @@
*/
import { useState } from "react";
import { TProfileData } from "../../../types/profile.ts";
import { TProfileData, TProfilePlayer } from "../../../types/profile.ts";
import { useTranslation } from "react-i18next";
import { ArrowIcon, FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
import { formatTime } from "../../../util/time.ts";
import { useAuth } from "../../../hooks/useAuth.tsx";
import { ConfirmModal } from "../../mini/modal/ConfirmModal.tsx";
@ -25,258 +24,253 @@ import { post } from "../../../util/fetcher.ts";
import { toast } from "react-toastify";
import dayjs from "dayjs";
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
import { StationStat } from '../../mini/profile/StationStat.tsx';
import { chunk } from '../../../util/chunk.ts';
import { Paginator } from '../../mini/util/Paginator.tsx';
import { TrainStat } from '../../mini/profile/TrainStat.tsx';
import { TImagesData } from '../../../types/images.ts';
export const ProfileCard = ({ data }: { data: TProfileData }) =>
{
const sortTrainsByList: Record<number, string> = {
[0]: 'time',
[1]: 'score',
[2]: 'distance',
}
const [ showTrains, setShowTrains ] = useState(false);
const [ showStations, setShowStations ] = useState(false);
const [ sortTrainsBy, setSortTrainsBy ] = useState<"time" | "score" | "distance">("distance");
const [ hideLeaderboardStatsModal, setHideLeaderboardStatsModal ] = useState(false);
const [ hideProfileModal, setHideProfileModal ] = useState(false);
export const ProfileCard = ({ data, images }: { data: TProfileData, images: TImagesData }) => {
const [sortTrainsBy, setSortTrainsBy] = useState(2);
const [sortTrainsBy2, setSortTrainsBy2] = useState(0);
const [sortStationsBy, setSortStationsBy] = useState(0);
const [hideLeaderboardStatsModal, setHideLeaderboardStatsModal] = useState(false);
const [hideProfileModal, setHideProfileModal] = useState(false);
const { isAdmin, token } = useAuth();
const adminToggleHideLeaderboardPlayerProfile = () =>
{
post(`/admin/profile/${ data.player.id }/${ data.player.flags.includes("leaderboard_hidden") ? "showLeaderboard" : "hideLeaderboard" }`, {}, { "X-Auth-Token": token })
.then((response) =>
{
if (response.code === 200)
{
toast.success(t("admin.hideLeaderboard.alert"));
}
});
// #region ADMIN
const adminToggleHideLeaderboardPlayerProfile = () => {
post(`/admin/profile/${data.player.id}/${data.player.flags.includes("leaderboard_hidden") ? "showLeaderboard" : "hideLeaderboard"}`, {}, { "X-Auth-Token": token })
.then((response) => {
if (response.code === 200) {
toast.success(t("admin.hideLeaderboard.alert"));
}
});
};
const adminHidePlayerProfile = () =>
{
post(`/admin/profile/${ data.player.id }/hide`, {}, { "X-Auth-Token": token })
.then((response) =>
{
if (response.code === 200)
{
toast.success(t("admin.hide.alert"));
}
});
const adminHidePlayerProfile = () => {
post(`/admin/profile/${data.player.id}/hide`, {}, { "X-Auth-Token": token })
.then((response) => {
if (response.code === 200) {
toast.success(t("admin.hide.alert"));
}
});
};
const adminForceUpdate = () =>
{
post(`/admin/profile/${ data.player.id }/forceUpdate`, {}, { "X-Auth-Token": token })
.then((response) =>
{
if (response.code === 200)
{
toast.success(t("admin.update.alert"));
}
});
const adminForceUpdate = () => {
post(`/admin/profile/${data.player.id}/forceUpdate`, {}, { "X-Auth-Token": token })
.then((response) => {
if (response.code === 200) {
toast.success(t("admin.update.alert"));
}
});
};
// #endregion
const sortStations = (a: keyof TProfilePlayer['dispatcherStats'], b: keyof TProfilePlayer['dispatcherStats']) => {
if (sortStationsBy) {
const _a = a;
a = b;
b = _a;
}
return data.player.dispatcherStats[b].time - data.player.dispatcherStats[a].time
}
const sortTrains = (a: keyof TProfilePlayer['trainStats'], b: keyof TProfilePlayer['trainStats']) => {
if (sortTrainsBy2) {
const _a = a;
a = b;
b = _a;
}
return data.player.trainStats[b][(sortTrainsByList[sortTrainsBy] ?? 'distance') as 'distance'] - data.player.trainStats[a][(sortTrainsByList[sortTrainsBy] ?? 'distance') as 'distance'];
}
const dispatcherStats = [...chunk(Object.keys(data.player.dispatcherStats).sort(sortStations), 8)];
const [dispatcherPage, setDispatcherPage] = useState(1);
const trainStats = [...chunk(Object.keys(data.player.trainStats).sort(sortTrains), 8)];
const [trainPage, setTrainPage] = useState(1);
const { t } = useTranslation();
return <>
<ConfirmModal showModal={ hideLeaderboardStatsModal } setShowModal={ setHideLeaderboardStatsModal }
onConfirm={ adminToggleHideLeaderboardPlayerProfile }
title={ t("admin.hideLeaderboard.modal.title") }
description={ t("admin.hideLeaderboard.modal.description") }/>
<ConfirmModal showModal={ hideProfileModal } setShowModal={ setHideProfileModal }
onConfirm={ adminHidePlayerProfile } title={ t("admin.hide.modal.title") }
description={ t("admin.hide.modal.description") }/>
<ConfirmModal showModal={hideLeaderboardStatsModal} setShowModal={setHideLeaderboardStatsModal}
onConfirm={adminToggleHideLeaderboardPlayerProfile}
title={t("admin.hideLeaderboard.modal.title")}
description={t("admin.hideLeaderboard.modal.description")} />
<ConfirmModal showModal={hideProfileModal} setShowModal={setHideProfileModal}
onConfirm={adminHidePlayerProfile} title={t("admin.hide.modal.title")}
description={t("admin.hide.modal.description")} />
<div
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
className="overflow-hidden ">
<div className="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
<div
className="mx-auto max-w-44 rounded-full">
className="mx-auto max-w-44 rounded-full">
<div className="relative rounded-full">
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
{ data.active &&
<span className="absolute w-full rounded-full border-white bg-[#219653] dark:border-black max-w-5.5 right-0 top-0 h-5.5 border-[3px]"></span> }
<img className="rounded-full" src={data.player.avatar} alt="profile" />
{data.active &&
<span className="absolute w-full rounded-full border-white bg-[#219653] dark:border-black max-w-5.5 right-0 top-0 h-5.5 border-[3px]"></span>}
</div>
</div>
<div className="mt-4">
<h3 className="text-2xl font-semibold text-black dark:text-white">
{ data.player.username } <UserIcons flags={ data.player.flags }/>
{data.player.username} <UserIcons flags={data.player.flags} />
</h3>
<div
className="mx-auto mt-4.5 mb-5.5 grid max-w-94 grid-cols-2 rounded-md border border-stroke py-2.5 shadow-1 dark:border-strokedark dark:bg-[#37404F]">
className="mx-auto mt-4.5 mb-5.5 grid max-w-94 grid-cols-2 rounded-md border border-stroke py-2.5 shadow-1 dark:border-strokedark dark:bg-[#37404F]">
<div
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
<span className="font-semibold text-black dark:text-white">
{ Math.floor(data.player.trainDistance / 1000) }km
{Math.floor(data.player.trainDistance / 1000)}km
</span>
<span className="text-sm text-wrap">{ t("profile.stats.distance") }</span>
<span className="text-sm text-wrap">{t("profile.stats.distance")}</span>
</div>
<div
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
<span className="font-semibold text-black dark:text-white">
{ formatTime(data.player.dispatcherTime) }
{formatTime(data.player.dispatcherTime)}
</span>
<span className="text-sm text-wrap">{ t("profile.stats.time") }</span>
<span className="text-sm text-wrap">{t("profile.stats.time")}</span>
</div>
</div>
</div>
{ data.active && data.active.type === "train" &&
<div className="mx-auto text-center">
<h4 className="font-semibold text-black dark:text-white">{ t("profile.active.train", { train: `${ data.active.trainName } - ${ data.active.trainNumber }`, server: data.active.server.toUpperCase() }) }</h4>
</div> }
{data.active && data.active.type === "train" &&
<div className="mx-auto text-center">
<h4 className="font-semibold text-black dark:text-white">{t("profile.active.train", { train: `${data.active.trainName} - ${data.active.trainNumber}`, server: data.active.server.toUpperCase() })}</h4>
</div>}
{ data.active && data.active.type === "station" &&
<div className="mx-auto text-center">
<h4 className="font-semibold text-black dark:text-white">{ t("profile.active.station", { station: `${ data.active.stationName } - ${ data.active.stationShort }`, server: data.active.server.toUpperCase() }) }</h4>
</div> }
{data.active && data.active.type === "station" &&
<div className="mx-auto text-center">
<h4 className="font-semibold text-black dark:text-white">{t("profile.active.station", { station: `${data.active.stationName} - ${data.active.stationShort}`, server: data.active.server.toUpperCase() })}</h4>
</div>}
</div>
<div className="px-5 pt-6 pb-5 sm:px-7.5 rounded-md">
<h1 className="text-xl text-black dark:text-white pb-5">{t("profile.stations.header")}</h1>
<div className="flex flex-row gap-4">
<a className='cursor-pointer' onClick={() => setSortStationsBy((prev) => prev === 0 ? 1 : 0)}><p className='text-base'>
<strong>{t('profile.stations.sortby.title')}</strong> {sortStationsBy === 0 ? t('profile.stations.sortby.max') : t('profile.stations.sortby.min')}
</p></a>
</div>
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4 pt-4">
{dispatcherStats[dispatcherPage - 1].map(stationName => {
const station = data.player.dispatcherStats[stationName];
{ Object.keys(data.player.trainStats || {}).length > 0 &&
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
<div className="group relative cursor-pointer" onClick={ () => setShowTrains(val => !val) }>
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.trains.header") }</h1>
<ArrowIcon rotated={ showTrains }/>
return <StationStat stationName={stationName} time={station.time} image={images.stations[stationName] ?? images.stations['N/A']} />
})}
</div>
<div className="flex flex-col pt-4">
<Paginator setPage={setDispatcherPage} page={dispatcherPage} pages={dispatcherStats.length} />
</div>
</div>
<div className="px-5 pt-6 pb-5 sm:px-7.5 rounded-md">
<h1 className="text-xl text-black dark:text-white pb-5">{t("profile.trains.header")}</h1>
<div className="flex flex-col">
<a className='cursor-pointer' onClick={() => setSortTrainsBy2((prev) => prev === 0 ? 1 : 0)}><p className='text-base'>
<strong>{t('profile.trains.sortby.title')}</strong> {sortTrainsBy2 === 0 ? t('profile.trains.sortby.max') : t('profile.trains.sortby.min')}
</p></a>
<a className='cursor-pointer' onClick={() => setSortTrainsBy((prev) => {
prev++;
if (prev > 2) prev = 0;
return prev;
})}><p className='text-base'>
<strong>{t('profile.trains.sortby.title')}</strong> {t('profile.trains.sortby.' + sortTrainsByList[sortTrainsBy])}
</p></a>
</div>
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4 pt-4">
{trainStats[trainPage - 1].map(trainName => {
const train = data.player.trainStats[trainName];
return <TrainStat trainName={trainName} time={train.time} distance={train.distance} score={train.score} image={images.trains[trainName] ?? images.trains['N/A']} />
})}
</div>
<div className="flex flex-col pt-4">
<Paginator setPage={setTrainPage} page={trainPage} pages={trainStats.length} />
</div>
</div>
{/* <div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t("profile.stations.station")}
</h5>
</div>
{ showTrains &&
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-3">
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{ t("profile.trains.train") }
</h5>
</div>
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
onClick={ () => setSortTrainsBy("distance") }>
<h5 className="text-sm font-medium uppercase xsm:text-base">
{ t("profile.trains.distance") }
</h5>
<FlexArrowIcon rotated={ sortTrainsBy === "distance" || !sortTrainsBy }/>
</div>
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
onClick={ () => setSortTrainsBy("score") }>
<h5 className="text-sm font-medium uppercase xsm:text-base">
{ t("profile.trains.points") }
</h5>
<FlexArrowIcon rotated={ sortTrainsBy === "score" }/>
</div>
{/*<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"*/ }
{/* onClick={ () => setSortTrainsBy("time") }>*/ }
{/* <h5 className="text-sm font-medium uppercase xsm:text-base">*/ }
{/* { t("profile.trains.time") }*/ }
{/* </h5>*/ }
{/* <FlexArrowIcon rotated={ sortTrainsBy === "time" }/>*/ }
{/*</div>*/ }
</div>
{ Object.keys(data.player.trainStats).sort((a, b) => data.player.trainStats[ b ][ sortTrainsBy ] - data.player.trainStats[ a ][ sortTrainsBy ]).map(trainName =>
{
const train = data.player.trainStats[ trainName ];
return <div
className={ `grid grid-cols-3 sm:grid-cols-3 border-t border-t-stroke dark:border-t-strokedark` }
key={ trainName }
>
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
{ trainName }
</p>
</div>
<div className="flex items-center justify-center p-2.5 lg:p-5">
<p className="text-meta-6 sm:block break-all">{ Math.floor(train.distance / 1000) }km</p>
</div>
<div className="flex items-center justify-center p-2.5 lg:p-5">
<p className="text-meta-3">{ train.score }</p>
</div>
{/*<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">*/ }
{/* <p className="text-meta-3">{ formatTime(train.time) }</p>*/ }
{/*</div>*/ }
</div>;
}) }
</div> }
</div> }
{ Object.keys(data.player.dispatcherStats || {}).length > 0 &&
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
<div className="group relative cursor-pointer" onClick={ () => setShowStations(val => !val) }>
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.stations.header") }</h1>
<ArrowIcon rotated={ showStations }/>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{t("profile.stations.time")}
</h5>
</div>
{ showStations &&
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{ t("profile.stations.station") }
</h5>
</div>
</div>
{Object.keys(data.player.dispatcherStats).sort((a, b) => data.player.dispatcherStats[b].time - data.player.dispatcherStats[a].time).map(stationName => {
const station = data.player.dispatcherStats[stationName];
return <div
className={`grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark`}
key={stationName}
>
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
{stationName}
</p>
</div>
<div className="p-2.5 text-center xl:p-5">
<h5 className="text-sm font-medium uppercase xsm:text-base">
{ t("profile.stations.time") }
</h5>
</div>
</div>
{ Object.keys(data.player.dispatcherStats).sort((a, b) => data.player.dispatcherStats[ b ].time - data.player.dispatcherStats[ a ].time).map(stationName =>
{
const station = data.player.dispatcherStats[ stationName ];
return <div
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
key={ stationName }
>
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
<p className="text-black dark:text-white sm:block break-all">
{ stationName }
</p>
</div>
<div className="flex items-center justify-center p-2.5 lg:p-5">
<p className="text-meta-3">{formatTime(station.time)}</p>
</div>
</div>;
})}
<div className="flex items-center justify-center p-2.5 lg:p-5">
<p className="text-meta-3">{ formatTime(station.time) }</p>
</div>
</div>;
}) }
</div>
</div> }
</div> }
{ isAdmin && <>
<div className="shadow-default dark:bg-boxdark items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
<h1 className="text-xl text-black dark:text-white">{ t("admin.header") }</h1>
</div> */}
{isAdmin && <>
<div className="shadow-default items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
<h1 className="text-xl text-black dark:text-white">{t("admin.header")}</h1>
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap">
{ data.player.flags.includes("leaderboard_hidden") ?
<button className={ "inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-success" }
onClick={ () => adminToggleHideLeaderboardPlayerProfile() }>
{ t("admin.hideLeaderboard.button2") }
</button> :
<button className={ "inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-danger" }
onClick={ () => setHideLeaderboardStatsModal(true) }>
{ t("admin.hideLeaderboard.button") }
</button> }
{data.player.flags.includes("leaderboard_hidden") ?
<button className={"inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-success"}
onClick={() => adminToggleHideLeaderboardPlayerProfile()}>
{t("admin.hideLeaderboard.button2")}
</button> :
<button className={"inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-danger"}
onClick={() => setHideLeaderboardStatsModal(true)}>
{t("admin.hideLeaderboard.button")}
</button>}
<button className="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"
onClick={ () => setHideProfileModal(true) }>
{ t("admin.hide.button") }
onClick={() => setHideProfileModal(true)}>
{t("admin.hide.button")}
</button>
<button className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
onClick={ () => adminForceUpdate() }>
{ t("admin.update.button") }
onClick={() => adminForceUpdate()}>
{t("admin.update.button")}
</button>
</div>
</div>
</> }
</>}
<div className="shadow-default dark:bg-boxdark items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
<div className="items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
<h1 className="text-sm text-black dark:text-white">
{ t("profile.info", { date: dayjs(data.player.createdAt).format("DD/MM/YYYY") }) }
{t("profile.info", { date: dayjs(data.player.createdAt).format("DD/MM/YYYY") })}
</h1>
</div>

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,3 +1,20 @@
/*
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* See LICENSE for more.
*/
import { useContext, createContext, ReactNode } from "react";
import useSWR from "swr";
import { get } from "../util/fetcher.ts";

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -35,7 +35,7 @@ function useLocalStorage<T>(
return item ? JSON.parse(item) : initialValue;
} catch
{
return item ? item : initialValue;
return (item && item !== "undefined") ? item : initialValue;
}
}
} catch (error)

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -20,7 +20,7 @@
"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ě",
"thanks": "Speciální poděkování: <bahu>BAHU.PRO hosting</bahu>, <simrailelite>Simrail ELITE discord</simrailelite>, komunita SimRail",
"author": "Pro komunitu SimRail vytvořeno s ❤️ uživatelem <anchor>{{author}}</anchor> "
}
},
@ -63,14 +63,27 @@
"trains": {
"header": "Statistiky vlaků",
"train": "Vlak",
"distance": "Vzdálenost",
"points": "Body",
"time": "Čas"
"distance": "Vzdálenost: {{distance}}km",
"score": "Body: {{score}",
"time": "Čas: {{time}}",
"sortby": {
"title": "Řadit podle: ",
"min": "Nejnižší",
"max": "Nejvyšší",
"time": "Čas",
"distance": "Vzdálenost",
"score": "Body"
}
},
"stations": {
"header": "Statistiky stanic",
"station": "Stanice",
"time": "Čas"
"time": "Čas: {{time}}",
"sortby": {
"title": "Řadit podle: ",
"min": "Nejnižší",
"max": "Nejvyšší"
}
},
"errors": {
"notfound": {

@ -20,7 +20,7 @@
"footer": {
"license": "License:",
"powered": "Based on:",
"thanks": "Special thanks to <bahu>BAHU.PRO hosting</bahu>, <simrailelite>Simrail ELITE discord</simrailelite>, Simrail community and my girlfriend",
"thanks": "Special thanks to <bahu>BAHU.PRO hosting</bahu>, <simrailelite>Simrail ELITE discord</simrailelite> and Simrail community",
"author": "Created by <anchor>{{author}}</anchor> with ❤️ for the Simrail community"
}
},
@ -63,14 +63,27 @@
"trains": {
"header": "Train Statistics",
"train": "Train",
"distance": "Distance",
"points": "Points",
"time": "Time"
"distance": "Distance: {{distance}}km",
"score": "Points: {{score}}",
"time": "Time: {{time}}",
"sortby": {
"title": "Sort: ",
"min": "Lowest",
"max": "Highest",
"time": "Time",
"distance": "Distance",
"score": "Points"
}
},
"stations": {
"header": "Station Statistics",
"station": "Station",
"time": "Time"
"time": "Time: {{time}}",
"sortby": {
"title": "Sort: ",
"min": "Lowest",
"max": "Highest"
}
},
"errors": {
"notfound": {

@ -20,7 +20,7 @@
"footer": {
"license": "Licencja:",
"powered": "Oparte na:",
"thanks": "Specjalne podziękowania dla <bahu>serwerowni BAHU.PRO</bahu>, <simrailelite>discorda Simrail ELITE</simrailelite>, społeczności Simrail i mojej dziewczyny",
"thanks": "Specjalne podziękowania dla <bahu>serwerowni BAHU.PRO</bahu>, <simrailelite>discorda Simrail ELITE</simrailelite> i społeczności Simrail",
"author": "Stworzone przez <anchor>{{author}}</anchor> z ❤️ dla społeczności Simrail"
}
},
@ -63,14 +63,27 @@
"trains": {
"header": "Statystyki pociągów",
"train": "Pociąg",
"distance": "Dystans",
"points": "Punkty",
"time": "Czas"
"distance": "Dystans: {{distance}}km",
"score": "Punkty: {{score}}",
"time": "Czas: {{time}}",
"sortby": {
"title": "Sortowanie: ",
"min": "Od najniższej",
"max": "Od najwyższej",
"time": "Czas",
"distance": "Dystans",
"score": "Punkty"
}
},
"stations": {
"header": "Statystyki stacji",
"station": "Stacja",
"time": "Czas"
"time": "Czas: {{time}}",
"sortby": {
"title": "Sortowanie: ",
"min": "Od najniższej",
"max": "Od najwyższej"
}
},
"errors": {
"notfound": {

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
@ -27,37 +27,37 @@ import useSWR from "swr";
import { get } from "../../util/fetcher.ts";
export const Profile = () =>
{
export const Profile = () => {
const { id } = useParams();
const { data, error, isLoading } = useSWR(`/profiles/${ id }`, get, { refreshInterval: 5_000, errorRetryCount: 5 });
const { data, error, isLoading } = useSWR(`/profiles/${id}`, get, { refreshInterval: 5_000, errorRetryCount: 5 });
const images = useSWR(`/images/`, get);
const { t } = useTranslation();
return (
<>
{/* LOADING */ }
{ isLoading && <ContentLoader/> }
{/* ERROR */ }
{ error && <LoadError/> }
{/* BLACKLISTED */ }
{ data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
description="The player's profile could not be displayed due to active moderator actions."/> }
{ data && data.code === 403 && <WarningAlert title={ t("profile.errors.blacklist.title") }
description={ t("profile.errors.blacklist.description") }/> }
{/* NOT FOUND */ }
{ data && data.code === 404 && <PageMeta title="simrail.pro | Profile not found"
description="Player's profile could not be found or the player has a private Steam profile."/> }
{ data && data.code === 404 && <WarningAlert title={ t("profile.errors.notfound.title") }
description={ t("profile.errors.notfound.description") }/> }
<>
{/* LOADING */}
{(isLoading || images.isLoading) && <ContentLoader />}
{/* ERROR */}
{(error || images.error) && <LoadError />}
{/* BLACKLISTED */}
{data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
description="The player's profile could not be displayed due to active moderator actions." />}
{data && data.code === 403 && <WarningAlert title={t("profile.errors.blacklist.title")}
description={t("profile.errors.blacklist.description")} />}
{/* NOT FOUND */}
{data && data.code === 404 && <PageMeta title="simrail.pro | Profile not found"
description="Player's profile could not be found or the player has a private Steam profile." />}
{data && data.code === 404 && <WarningAlert title={t("profile.errors.notfound.title")}
description={t("profile.errors.notfound.description")} />}
{/* SUCCESS */ }
{ data && data.code === 200 && <PageMeta image={ data.data.player.username }
title={ `simrail.pro | ${ data.data.player.username }'s profile` }
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
${ data.data.player.dispatcherTime ? 0 : formatTime(data.data.player.dispatcherTime) } dispatcher experience` }/> }
{ data && data.code === 200 && <ProfileCard data={ data.data }/> }
</>
{/* SUCCESS */}
{data && data.code === 200 && images.data && images.data.code === 200 && <PageMeta image={data.data.player.username}
title={`simrail.pro | ${data.data.player.username}'s profile`}
description={`${data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2))} driving experience |
${data.data.player.dispatcherTime ? 0 : formatTime(data.data.player.dispatcherTime)} dispatcher experience`} />}
{data && data.code === 200 && images.data && images.data.code === 200 && <ProfileCard data={data.data} images={images.data.data} />}
</>
);
};

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -0,0 +1,10 @@
export interface TImagesResponse {
success: boolean;
data: TImagesData;
code: number;
}
export interface TImagesData {
trains: Record<string, string>
stations: Record<string, string>
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
* Copyright (C) 2025 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published

Some files were not shown because too many files have changed in this diff Show More